: str_replace(): Passing null to parameter #2 ($replace) of type array|string is deprecated in
function block_core_navigation_render_submenu_icon() {
return '<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 12 12" fill="none" aria-hidden="true" focusable="false"><path d="M1.50002 4L6.00002 8L10.5 4" stroke-width="1.5"></path></svg>';
* Filter out empty "null" blocks from the block list.
* 'parse_blocks' includes a null block with '\n\n' as the content when
* it encounters whitespace. This is not a bug but rather how the parser
* @param array $parsed_blocks the parsed blocks to be normalized.
* @return array the normalized parsed blocks.
function block_core_navigation_filter_out_empty_blocks( $parsed_blocks ) {
$filtered = array_filter(
static function ( $block ) {
return isset( $block['blockName'] );
return array_values( $filtered );
* Returns true if the navigation block contains a nested navigation block.
* @param WP_Block_List $inner_blocks Inner block instance to be normalized.
* @return bool true if the navigation block contains a nested navigation block.
function block_core_navigation_block_contains_core_navigation( $inner_blocks ) {
foreach ( $inner_blocks as $block ) {
if ( 'core/navigation' === $block->name ) {
if ( $block->inner_blocks && block_core_navigation_block_contains_core_navigation( $block->inner_blocks ) ) {
* Retrieves the appropriate fallback to be used on the front of the
* site when there is no menu assigned to the Nav block.
* This aims to mirror how the fallback mechanic for wp_nav_menu works.
* See https://developer.wordpress.org/reference/functions/wp_nav_menu/#more-information.
* @return array the array of blocks to be used as a fallback.
function block_core_navigation_get_fallback_blocks() {
$page_list_fallback = array(
'blockName' => 'core/page-list',
'innerContent' => array(),
$registry = WP_Block_Type_Registry::get_instance();
// If `core/page-list` is not registered then return empty blocks.
$fallback_blocks = $registry->is_registered( 'core/page-list' ) ? $page_list_fallback : array();
$navigation_post = WP_Navigation_Fallback::get_fallback();
// Use the first non-empty Navigation as fallback if available.
if ( $navigation_post ) {
$parsed_blocks = parse_blocks( $navigation_post->post_content );
$maybe_fallback = block_core_navigation_filter_out_empty_blocks( $parsed_blocks );
// Normalizing blocks may result in an empty array of blocks if they were all `null` blocks.
// In this case default to the (Page List) fallback.
$fallback_blocks = ! empty( $maybe_fallback ) ? $maybe_fallback : $fallback_blocks;
if ( function_exists( 'set_ignored_hooked_blocks_metadata' ) ) {
// Run Block Hooks algorithm to inject hooked blocks.
// We have to run it here because we need the post ID of the Navigation block to track ignored hooked blocks.
$markup = block_core_navigation_insert_hooked_blocks( $fallback_blocks, $navigation_post );
$blocks = parse_blocks( $markup );
if ( isset( $blocks[0]['innerBlocks'] ) ) {
$fallback_blocks = $blocks[0]['innerBlocks'];
* Filters the fallback experience for the Navigation block.
* Returning a falsey value will opt out of the fallback and cause the block not to render.
* To customise the blocks provided return an array of blocks - these should be valid
* children of the `core/navigation` block.
* @param array[] $fallback_blocks default fallback blocks provided by the default block mechanic.
return apply_filters( 'block_core_navigation_render_fallback', $fallback_blocks );
* Iterate through all inner blocks recursively and get navigation link block's post IDs.
* @param WP_Block_List $inner_blocks Block list class instance.
* @return array Array of post IDs.
function block_core_navigation_get_post_ids( $inner_blocks ) {
$post_ids = array_map( 'block_core_navigation_from_block_get_post_ids', iterator_to_array( $inner_blocks ) );
return array_unique( array_merge( ...$post_ids ) );
* Get post IDs from a navigation link block instance.
* @param WP_Block $block Instance of a block.
* @return array Array of post IDs.
function block_core_navigation_from_block_get_post_ids( $block ) {
if ( $block->inner_blocks ) {
$post_ids = block_core_navigation_get_post_ids( $block->inner_blocks );
if ( 'core/navigation-link' === $block->name || 'core/navigation-submenu' === $block->name ) {
if ( $block->attributes && isset( $block->attributes['kind'] ) && 'post-type' === $block->attributes['kind'] && isset( $block->attributes['id'] ) ) {
$post_ids[] = $block->attributes['id'];
* Renders the `core/navigation` block on server.
* @param array $attributes The block attributes.
* @param string $content The saved content.
* @param WP_Block $block The parsed block.
* @return string Returns the navigation block markup.
function render_block_core_navigation( $attributes, $content, $block ) {
return WP_Navigation_Block_Renderer::render( $attributes, $content, $block );
* Register the navigation block.
* @uses render_block_core_navigation()
* @throws WP_Error An WP_Error exception parsing the block definition.
function register_block_core_navigation() {
register_block_type_from_metadata(
'render_callback' => 'render_block_core_navigation',
add_action( 'init', 'register_block_core_navigation' );
* Filter that changes the parsed attribute values of navigation blocks contain typographic presets to contain the values directly.
* @param array $parsed_block The block being rendered.
* @return array The block being rendered without typographic presets.
function block_core_navigation_typographic_presets_backcompatibility( $parsed_block ) {
if ( 'core/navigation' === $parsed_block['blockName'] ) {
$attribute_to_prefix_map = array(
'fontStyle' => 'var:preset|font-style|',
'fontWeight' => 'var:preset|font-weight|',
'textDecoration' => 'var:preset|text-decoration|',
'textTransform' => 'var:preset|text-transform|',
foreach ( $attribute_to_prefix_map as $style_attribute => $prefix ) {
if ( ! empty( $parsed_block['attrs']['style']['typography'][ $style_attribute ] ) ) {
$prefix_len = strlen( $prefix );
$attribute_value = &$parsed_block['attrs']['style']['typography'][ $style_attribute ];
if ( 0 === strncmp( $attribute_value, $prefix, $prefix_len ) ) {
$attribute_value = substr( $attribute_value, $prefix_len );
if ( 'textDecoration' === $style_attribute && 'strikethrough' === $attribute_value ) {
$attribute_value = 'line-through';
add_filter( 'render_block_data', 'block_core_navigation_typographic_presets_backcompatibility' );
* Turns menu item data into a nested array of parsed blocks
* @deprecated 6.3.0 Use WP_Navigation_Fallback::parse_blocks_from_menu_items() instead.
* @param array $menu_items An array of menu items that represent
* an individual level of a menu.
* @param array $menu_items_by_parent_id An array keyed by the id of the
* parent menu where each element is an
* array of menu items that belong to
* @return array An array of parsed block data.
function block_core_navigation_parse_blocks_from_menu_items( $menu_items, $menu_items_by_parent_id ) {
_deprecated_function( __FUNCTION__, '6.3.0', 'WP_Navigation_Fallback::parse_blocks_from_menu_items' );
if ( empty( $menu_items ) ) {
foreach ( $menu_items as $menu_item ) {
$class_name = ! empty( $menu_item->classes ) ? implode( ' ', (array) $menu_item->classes ) : null;
$id = ( null !== $menu_item->object_id && 'custom' !== $menu_item->object ) ? $menu_item->object_id : null;
$opens_in_new_tab = null !== $menu_item->target && '_blank' === $menu_item->target;
$rel = ( null !== $menu_item->xfn && '' !== $menu_item->xfn ) ? $menu_item->xfn : null;
$kind = null !== $menu_item->type ? str_replace( '_', '-', $menu_item->type ) : 'custom';
'blockName' => isset( $menu_items_by_parent_id[ $menu_item->ID ] ) ? 'core/navigation-submenu' : 'core/navigation-link',
'className' => $class_name,
'description' => $menu_item->description,
'label' => $menu_item->title,
'opensInNewTab' => $opens_in_new_tab,
'title' => $menu_item->attr_title,
'type' => $menu_item->object,
'url' => $menu_item->url,
$block['innerBlocks'] = isset( $menu_items_by_parent_id[ $menu_item->ID ] )
? block_core_navigation_parse_blocks_from_menu_items( $menu_items_by_parent_id[ $menu_item->ID ], $menu_items_by_parent_id )
$block['innerContent'] = array_map( 'serialize_block', $block['innerBlocks'] );
* Get the classic navigation menu to use as a fallback.
* @deprecated 6.3.0 Use WP_Navigation_Fallback::get_classic_menu_fallback() instead.
* @return object WP_Term The classic navigation.
function block_core_navigation_get_classic_menu_fallback() {
_deprecated_function( __FUNCTION__, '6.3.0', 'WP_Navigation_Fallback::get_classic_menu_fallback' );
$classic_nav_menus = wp_get_nav_menus();
if ( $classic_nav_menus && ! is_wp_error( $classic_nav_menus ) ) {
// Handles simple use case where user has a classic menu and switches to a block theme.
// Returns the menu assigned to location `primary`.
$locations = get_nav_menu_locations();
if ( isset( $locations['primary'] ) ) {
$primary_menu = wp_get_nav_menu_object( $locations['primary'] );
// Returns a menu if `primary` is its slug.
foreach ( $classic_nav_menus as $classic_nav_menu ) {
if ( 'primary' === $classic_nav_menu->slug ) {
return $classic_nav_menu;
// Otherwise return the most recently created classic menu.
static function ( $a, $b ) {
return $b->term_id - $a->term_id;
return $classic_nav_menus[0];
* Converts a classic navigation to blocks.
* @deprecated 6.3.0 Use WP_Navigation_Fallback::get_classic_menu_fallback_blocks() instead.
* @param object $classic_nav_menu WP_Term The classic navigation object to convert.
* @return array the normalized parsed blocks.
function block_core_navigation_get_classic_menu_fallback_blocks( $classic_nav_menu ) {
_deprecated_function( __FUNCTION__, '6.3.0', 'WP_Navigation_Fallback::get_classic_menu_fallback_blocks' );
// BEGIN: Code that already exists in wp_nav_menu().
$menu_items = wp_get_nav_menu_items( $classic_nav_menu->term_id, array( 'update_post_term_cache' => false ) );
// Set up the $menu_item variables.
_wp_menu_item_classes_by_context( $menu_items );
$sorted_menu_items = array();
foreach ( (array) $menu_items as $menu_item ) {
$sorted_menu_items[ $menu_item->menu_order ] = $menu_item;
unset( $menu_items, $menu_item );
// END: Code that already exists in wp_nav_menu().
$menu_items_by_parent_id = array();
foreach ( $sorted_menu_items as $menu_item ) {
$menu_items_by_parent_id[ $menu_item->menu_item_parent ][] = $menu_item;
$inner_blocks = block_core_navigation_parse_blocks_from_menu_items(
isset( $menu_items_by_parent_id[0] )
? $menu_items_by_parent_id[0]
return serialize_blocks( $inner_blocks );
* If there's a classic menu then use it as a fallback.
* @deprecated 6.3.0 Use WP_Navigation_Fallback::create_classic_menu_fallback() instead.
* @return array the normalized parsed blocks.
function block_core_navigation_maybe_use_classic_menu_fallback() {
_deprecated_function( __FUNCTION__, '6.3.0', 'WP_Navigation_Fallback::create_classic_menu_fallback' );
// See if we have a classic menu.
$classic_nav_menu = block_core_navigation_get_classic_menu_fallback();
if ( ! $classic_nav_menu ) {
// If we have a classic menu then convert it to blocks.
$classic_nav_menu_blocks = block_core_navigation_get_classic_menu_fallback_blocks( $classic_nav_menu );
if ( empty( $classic_nav_menu_blocks ) ) {
// Create a new navigation menu from the classic menu.
$wp_insert_post_result = wp_insert_post(
'post_content' => $classic_nav_menu_blocks,
'post_title' => $classic_nav_menu->name,
'post_name' => $classic_nav_menu->slug,
'post_status' => 'publish',
'post_type' => 'wp_navigation',
true // So that we can check whether the result is an error.
if ( is_wp_error( $wp_insert_post_result ) ) {
// Fetch the most recently published navigation which will be the classic one created above.
return block_core_navigation_get_most_recently_published_navigation();
* Finds the most recently published `wp_navigation` Post.
* @deprecated 6.3.0 Use WP_Navigation_Fallback::get_most_recently_published_navigation() instead.
* @return WP_Post|null the first non-empty Navigation or null.
function block_core_navigation_get_most_recently_published_navigation() {
_deprecated_function( __FUNCTION__, '6.3.0', 'WP_Navigation_Fallback::get_most_recently_published_navigation' );
// Default to the most recently created menu.
'post_type' => 'wp_navigation',
'update_post_meta_cache' => false,
'update_post_term_cache' => false,
'post_status' => 'publish',
'posts_per_page' => 1, // get only the most recent.
$navigation_post = new WP_Query( $parsed_args );
if ( count( $navigation_post->posts ) > 0 ) {
return $navigation_post->posts[0];
* 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.
function block_core_navigation_remove_serialized_parent_block( $serialized_block ) {
$start = strpos( $serialized_block, '-->' ) + strlen( '-->' );
$end = strrpos( $serialized_block, '<!--' );
return substr( $serialized_block, $start, $end - $start );
* Mock a parsed block for the Navigation block given its inner blocks and the `wp_navigation` post object.
* The `wp_navigation` post's `_wp_ignored_hooked_blocks` meta is queried to add the `metadata.ignoredHookedBlocks` attribute.
* @param array $inner_blocks Parsed inner blocks of a Navigation block.
* @param WP_Post $post `wp_navigation` post object corresponding to the block.
* @return array the normalized parsed blocks.
function block_core_navigation_mock_parsed_block( $inner_blocks, $post ) {
if ( isset( $post->ID ) ) {
$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,
$mock_anchor_parent_block = array(
'blockName' => 'core/navigation',
'innerBlocks' => $inner_blocks,
'innerContent' => array_fill( 0, count( $inner_blocks ), null ),
return $mock_anchor_parent_block;
* Insert hooked blocks into a Navigation block.