: str_replace(): Passing null to parameter #2 ($replace) of type array|string is deprecated in
// Markup for the hooked blocks has already been created (in `insert_hooked_blocks`).
* Runs the hooked blocks algorithm on the given content.
* @param string $content Serialized content.
* @param WP_Block_Template|WP_Post|array $context A block template, template part, `wp_navigation` post object,
* or pattern that the blocks belong to.
* @param callable $callback A function that will be called for each block to generate
* the markup for a given list of blocks that are hooked to it.
* Default: 'insert_hooked_blocks'.
* @return string The serialized markup.
function apply_block_hooks_to_content( $content, $context, $callback = 'insert_hooked_blocks' ) {
$hooked_blocks = get_hooked_blocks();
if ( empty( $hooked_blocks ) && ! has_filter( 'hooked_block_types' ) ) {
$blocks = parse_blocks( $content );
$before_block_visitor = make_before_block_visitor( $hooked_blocks, $context, $callback );
$after_block_visitor = make_after_block_visitor( $hooked_blocks, $context, $callback );
return traverse_and_serialize_blocks( $blocks, $before_block_visitor, $after_block_visitor );
* Accepts the serialized markup of a block and its inner blocks, and returns serialized markup of the inner blocks.
* @param string $serialized_block The serialized markup of a block and its inner blocks.
* @return string The serialized markup of the inner blocks.
function remove_serialized_parent_block( $serialized_block ) {
$start = strpos( $serialized_block, '-->' ) + strlen( '-->' );
$end = strrpos( $serialized_block, '<!--' );
return substr( $serialized_block, $start, $end - $start );
* Updates the wp_postmeta with the list of ignored hooked blocks where the inner blocks are stored as post content.
* Currently only supports `wp_navigation` post types.
* @param stdClass $post Post object.
* @return stdClass The updated post object.
function update_ignored_hooked_blocks_postmeta( $post ) {
* In this scenario the user has likely tried to create a navigation via the REST API.
* In which case we won't have a post ID to work with and store meta against.
if ( empty( $post->ID ) ) {
* Skip meta generation when consumers intentionally update specific Navigation fields
* and omit the content update.
if ( ! isset( $post->post_content ) ) {
* Skip meta generation when the post content is not a navigation block.
if ( ! isset( $post->post_type ) || 'wp_navigation' !== $post->post_type ) {
$ignored_hooked_blocks = get_post_meta( $post->ID, '_wp_ignored_hooked_blocks', true );
if ( ! empty( $ignored_hooked_blocks ) ) {
$ignored_hooked_blocks = json_decode( $ignored_hooked_blocks, true );
$attributes['metadata'] = array(
'ignoredHookedBlocks' => $ignored_hooked_blocks,
$markup = get_comment_delimited_block_content(
$serialized_block = apply_block_hooks_to_content( $markup, get_post( $post->ID ), 'set_ignored_hooked_blocks_metadata' );
$root_block = parse_blocks( $serialized_block )[0];
$ignored_hooked_blocks = isset( $root_block['attrs']['metadata']['ignoredHookedBlocks'] )
? $root_block['attrs']['metadata']['ignoredHookedBlocks']
if ( ! empty( $ignored_hooked_blocks ) ) {
$existing_ignored_hooked_blocks = get_post_meta( $post->ID, '_wp_ignored_hooked_blocks', true );
if ( ! empty( $existing_ignored_hooked_blocks ) ) {
$existing_ignored_hooked_blocks = json_decode( $existing_ignored_hooked_blocks, true );
$ignored_hooked_blocks = array_unique( array_merge( $ignored_hooked_blocks, $existing_ignored_hooked_blocks ) );
update_post_meta( $post->ID, '_wp_ignored_hooked_blocks', json_encode( $ignored_hooked_blocks ) );
$post->post_content = remove_serialized_parent_block( $serialized_block );
* Returns the markup for blocks hooked to the given anchor block in a specific relative position and then
* adds a list of hooked block types to an anchor block's ignored hooked block types.
* This function is meant for internal use only.
* @param array $parsed_anchor_block The anchor block, in parsed block array format.
* @param string $relative_position The relative position of the hooked blocks.
* Can be one of 'before', 'after', 'first_child', or 'last_child'.
* @param array $hooked_blocks An array of hooked block types, grouped by anchor block and relative position.
* @param WP_Block_Template|WP_Post|array $context The block template, template part, or pattern that the anchor block belongs to.
function insert_hooked_blocks_and_set_ignored_hooked_blocks_metadata( &$parsed_anchor_block, $relative_position, $hooked_blocks, $context ) {
$markup = insert_hooked_blocks( $parsed_anchor_block, $relative_position, $hooked_blocks, $context );
$markup .= set_ignored_hooked_blocks_metadata( $parsed_anchor_block, $relative_position, $hooked_blocks, $context );
* Hooks into the REST API response for the core/navigation block and adds the first and last inner blocks.
* @param WP_REST_Response $response The response object.
* @param WP_Post $post Post object.
* @return WP_REST_Response The response object.
function insert_hooked_blocks_into_rest_response( $response, $post ) {
if ( ! isset( $response->data['content']['raw'] ) || ! isset( $response->data['content']['rendered'] ) ) {
$ignored_hooked_blocks = get_post_meta( $post->ID, '_wp_ignored_hooked_blocks', true );
if ( ! empty( $ignored_hooked_blocks ) ) {
$ignored_hooked_blocks = json_decode( $ignored_hooked_blocks, true );
$attributes['metadata'] = array(
'ignoredHookedBlocks' => $ignored_hooked_blocks,
$content = get_comment_delimited_block_content(
$response->data['content']['raw']
$content = apply_block_hooks_to_content( $content, $post );
// Remove mock Navigation block wrapper.
$content = remove_serialized_parent_block( $content );
$response->data['content']['raw'] = $content;
/** This filter is documented in wp-includes/post-template.php */
$response->data['content']['rendered'] = apply_filters( 'the_content', $content );
* Returns a function that injects the theme attribute into, and hooked blocks before, a given block.
* The returned function can be used as `$pre_callback` argument to `traverse_and_serialize_block(s)`,
* where it will inject the `theme` attribute into all Template Part blocks, and prepend the markup for
* any blocks hooked `before` the given block and as its parent's `first_child`, respectively.
* This function is meant for internal use only.
* @since 6.5.0 Added $callback argument.
* @param array $hooked_blocks An array of blocks hooked to another given block.
* @param WP_Block_Template|WP_Post|array $context A block template, template part, `wp_navigation` post object,
* or pattern that the blocks belong to.
* @param callable $callback A function that will be called for each block to generate
* the markup for a given list of blocks that are hooked to it.
* Default: 'insert_hooked_blocks'.
* @return callable A function that returns the serialized markup for the given block,
* including the markup for any hooked blocks before it.
function make_before_block_visitor( $hooked_blocks, $context, $callback = 'insert_hooked_blocks' ) {
* Injects hooked blocks before the given block, injects the `theme` attribute into Template Part blocks, and returns the serialized markup.
* If the current block is a Template Part block, inject the `theme` attribute.
* Furthermore, prepend the markup for any blocks hooked `before` the given block and as its parent's
* `first_child`, respectively, to the serialized markup for the given block.
* @param array $block The block to inject the theme attribute into, and hooked blocks before. Passed by reference.
* @param array $parent_block The parent block of the given block. Passed by reference. Default null.
* @param array $prev The previous sibling block of the given block. Default null.
* @return string The serialized markup for the given block, with the markup for any hooked blocks prepended to it.
return function ( &$block, &$parent_block = null, $prev = null ) use ( $hooked_blocks, $context, $callback ) {
_inject_theme_attribute_in_template_part_block( $block );
if ( $parent_block && ! $prev ) {
// Candidate for first-child insertion.
$markup .= call_user_func_array(
array( &$parent_block, 'first_child', $hooked_blocks, $context )
$markup .= call_user_func_array(
array( &$block, 'before', $hooked_blocks, $context )
* Returns a function that injects the hooked blocks after a given block.
* The returned function can be used as `$post_callback` argument to `traverse_and_serialize_block(s)`,
* where it will append the markup for any blocks hooked `after` the given block and as its parent's
* `last_child`, respectively.
* This function is meant for internal use only.
* @since 6.5.0 Added $callback argument.
* @param array $hooked_blocks An array of blocks hooked to another block.
* @param WP_Block_Template|WP_Post|array $context A block template, template part, `wp_navigation` post object,
* or pattern that the blocks belong to.
* @param callable $callback A function that will be called for each block to generate
* the markup for a given list of blocks that are hooked to it.
* Default: 'insert_hooked_blocks'.
* @return callable A function that returns the serialized markup for the given block,
* including the markup for any hooked blocks after it.
function make_after_block_visitor( $hooked_blocks, $context, $callback = 'insert_hooked_blocks' ) {
* Injects hooked blocks after the given block, and returns the serialized markup.
* Append the markup for any blocks hooked `after` the given block and as its parent's
* `last_child`, respectively, to the serialized markup for the given block.
* @param array $block The block to inject the hooked blocks after. Passed by reference.
* @param array $parent_block The parent block of the given block. Passed by reference. Default null.
* @param array $next The next sibling block of the given block. Default null.
* @return string The serialized markup for the given block, with the markup for any hooked blocks appended to it.
return function ( &$block, &$parent_block = null, $next = null ) use ( $hooked_blocks, $context, $callback ) {
$markup = call_user_func_array(
array( &$block, 'after', $hooked_blocks, $context )
if ( $parent_block && ! $next ) {
// Candidate for last-child insertion.
$markup .= call_user_func_array(
array( &$parent_block, 'last_child', $hooked_blocks, $context )
* Given an array of attributes, returns a string in the serialized attributes
* format prepared for post content.
* The serialized result is a JSON-encoded string, with unicode escape sequence
* substitution for characters which might otherwise interfere with embedding
* the result in an HTML comment.
* This function must produce output that remains in sync with the output of
* the serializeAttributes JavaScript function in the block editor in order
* to ensure consistent operation between PHP and JavaScript.
* @param array $block_attributes Attributes object.
* @return string Serialized attributes.
function serialize_block_attributes( $block_attributes ) {
$encoded_attributes = wp_json_encode( $block_attributes, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE );
$encoded_attributes = preg_replace( '/--/', '\\u002d\\u002d', $encoded_attributes );
$encoded_attributes = preg_replace( '/</', '\\u003c', $encoded_attributes );
$encoded_attributes = preg_replace( '/>/', '\\u003e', $encoded_attributes );
$encoded_attributes = preg_replace( '/&/', '\\u0026', $encoded_attributes );
$encoded_attributes = preg_replace( '/\\\\"/', '\\u0022', $encoded_attributes );
return $encoded_attributes;
* Returns the block name to use for serialization. This will remove the default
* "core/" namespace from a block name.
* @param string|null $block_name Optional. Original block name. Null if the block name is unknown,
* e.g. Classic blocks have their name set to null. Default null.
* @return string Block name to use for serialization.
function strip_core_block_namespace( $block_name = null ) {
if ( is_string( $block_name ) && str_starts_with( $block_name, 'core/' ) ) {
return substr( $block_name, 5 );
* Returns the content of a block, including comment delimiters.
* @param string|null $block_name Block name. Null if the block name is unknown,
* e.g. Classic blocks have their name set to null.
* @param array $block_attributes Block attributes.
* @param string $block_content Block save content.
* @return string Comment-delimited block content.
function get_comment_delimited_block_content( $block_name, $block_attributes, $block_content ) {
if ( is_null( $block_name ) ) {
$serialized_block_name = strip_core_block_namespace( $block_name );
$serialized_attributes = empty( $block_attributes ) ? '' : serialize_block_attributes( $block_attributes ) . ' ';
if ( empty( $block_content ) ) {
return sprintf( '<!-- wp:%s %s/-->', $serialized_block_name, $serialized_attributes );
'<!-- wp:%s %s-->%s<!-- /wp:%s -->',
* Returns the content of a block, including comment delimiters, serializing all
* attributes from the given parsed block.
* This should be used when preparing a block to be saved to post content.
* Prefer `render_block` when preparing a block for display. Unlike
* `render_block`, this does not evaluate a block's `render_callback`, and will
* instead preserve the markup as parsed.
* A representative array of a single parsed block object. See WP_Block_Parser_Block.
* @type string $blockName Name of block.
* @type array $attrs Attributes from block comment delimiters.
* @type array[] $innerBlocks List of inner blocks. An array of arrays that
* have the same structure as this one.
* @type string $innerHTML HTML from inside block comment delimiters.
* @type array $innerContent List of string fragments and null markers where
* inner blocks were found.
* @return string String of rendered HTML.
function serialize_block( $block ) {
foreach ( $block['innerContent'] as $chunk ) {
$block_content .= is_string( $chunk ) ? $chunk : serialize_block( $block['innerBlocks'][ $index++ ] );
if ( ! is_array( $block['attrs'] ) ) {
$block['attrs'] = array();
return get_comment_delimited_block_content(
* Returns a joined string of the aggregate serialization of the given
* @param array[] $blocks {
* Array of block structures.
* A representative array of a single parsed block object. See WP_Block_Parser_Block.
* @type string $blockName Name of block.
* @type array $attrs Attributes from block comment delimiters.
* @type array[] $innerBlocks List of inner blocks. An array of arrays that
* have the same structure as this one.
* @type string $innerHTML HTML from inside block comment delimiters.
* @type array $innerContent List of string fragments and null markers where
* inner blocks were found.
* @return string String of rendered HTML.
function serialize_blocks( $blocks ) {
return implode( '', array_map( 'serialize_block', $blocks ) );
* Traverses a parsed block tree and applies callbacks before and after serializing it.
* Recursively traverses the block and its inner blocks and applies the two callbacks provided as
* arguments, the first one before serializing the block, and the second one after serializing it.
* If either callback returns a string value, it will be prepended and appended to the serialized
* block markup, respectively.
* The callbacks will receive a reference to the current block as their first argument, so that they
* can also modify it, and the current block's parent block as second argument. Finally, the
* `$pre_callback` receives the previous block, whereas the `$post_callback` receives
* the next block as third argument.
* Serialized blocks are returned including comment delimiters, and with all attributes serialized.
* This function should be used when there is a need to modify the saved block, or to inject markup
* into the return value. Prefer `serialize_block` when preparing a block to be saved to post content.
* This function is meant for internal use only.
* @param array $block A representative array of a single parsed block object. See WP_Block_Parser_Block.
* @param callable $pre_callback Callback to run on each block in the tree before it is traversed and serialized.
* It is called with the following arguments: &$block, $parent_block, $previous_block.
* Its string return value will be prepended to the serialized block markup.
* @param callable $post_callback Callback to run on each block in the tree after it is traversed and serialized.
* It is called with the following arguments: &$block, $parent_block, $next_block.
* Its string return value will be appended to the serialized block markup.
* @return string Serialized block markup.
function traverse_and_serialize_block( $block, $pre_callback = null, $post_callback = null ) {
foreach ( $block['innerContent'] as $chunk ) {
if ( is_string( $chunk ) ) {
$block_content .= $chunk;
$inner_block = $block['innerBlocks'][ $block_index ];
if ( is_callable( $pre_callback ) ) {
$prev = 0 === $block_index
: $block['innerBlocks'][ $block_index - 1 ];
$block_content .= call_user_func_array(
array( &$inner_block, &$block, $prev )
if ( is_callable( $post_callback ) ) {
$next = count( $block['innerBlocks'] ) - 1 === $block_index
: $block['innerBlocks'][ $block_index + 1 ];
$post_markup = call_user_func_array(