[ Index ]

PHP Cross Reference of WordPress Trunk (Updated Daily)

Search

title

Body

[close]

/wp-includes/blocks/ -> navigation.php (source)

   1  <?php
   2  /**
   3   * Server-side rendering of the `core/navigation` block.
   4   *
   5   * @package WordPress
   6   */
   7  
   8  /**
   9   * Returns the submenu visibility value with backward compatibility
  10   * for the deprecated openSubmenusOnClick attribute.
  11   *
  12   * This function centralizes the migration logic from the boolean
  13   * openSubmenusOnClick to the new submenuVisibility enum.
  14   *
  15   * Backward compatibility: WordPress applies default attribute values, so submenuVisibility
  16   * will always have a value even for legacy blocks. We check the legacy openSubmenusOnClick
  17   * attribute first to preserve original behavior for blocks saved before the migration.
  18   *
  19   * @since 7.0.0
  20   *
  21   * @param array $attributes Block attributes containing submenuVisibility and/or openSubmenusOnClick.
  22   * @return string The visibility mode: 'hover', 'click', or 'always'.
  23   */
  24  function block_core_navigation_get_submenu_visibility( $attributes ) {
  25      $deprecated_open_submenus_on_click = $attributes['openSubmenusOnClick'] ?? null;
  26  
  27      // For backward compatibility, prioritize the legacy attribute if present.
  28      // Legacy blocks have openSubmenusOnClick in the database. Since WordPress applies
  29      // default values, submenuVisibility will also have a value, but we check the legacy
  30      // attribute first to preserve the original behavior. If the block has been updated
  31      // and saved in the editor, then the deprecated attribute will be replaced by submenuVisibility.
  32      if ( null !== $deprecated_open_submenus_on_click ) {
  33          // Convert boolean to string: true -> 'click', false -> 'hover'.
  34          return ! empty( $deprecated_open_submenus_on_click ) ? 'click' : 'hover';
  35      }
  36  
  37      $submenu_visibility = $attributes['submenuVisibility'] ?? null;
  38  
  39      // Use submenuVisibility for migrated/new blocks (where openSubmenusOnClick is null).
  40      return $submenu_visibility ?? 'hover';
  41  }
  42  
  43  /**
  44   * Returns the custom properties used by the Navigation block for a layout.
  45   *
  46   * @since 7.1.0
  47   *
  48   * @param array $layout Layout configuration.
  49   * @return array Navigation layout custom property declarations.
  50   */
  51  function block_core_navigation_get_layout_custom_property_declarations( $layout ) {
  52      $justification_values = array(
  53          'left'          => 'flex-start',
  54          'center'        => 'center',
  55          'right'         => 'flex-end',
  56          'space-between' => 'space-between',
  57      );
  58      $justify_content      = is_array( $layout ) ? ( $layout['justifyContent'] ?? 'left' ) : 'left';
  59      if ( ! is_string( $justify_content ) || ! isset( $justification_values[ $justify_content ] ) ) {
  60          $justify_content = 'left';
  61      }
  62  
  63      $justification = $justification_values[ $justify_content ];
  64      $is_vertical   = is_array( $layout ) && 'vertical' === ( $layout['orientation'] ?? null );
  65      $align         = 'center';
  66      $justify       = $justification;
  67  
  68      if ( $is_vertical ) {
  69          $align   = in_array( $justify_content, array( 'center', 'right' ), true ) ? $justification : 'flex-start';
  70          $justify = 'left' === $justify_content ? 'initial' : $justification;
  71      }
  72  
  73      return array(
  74          '--navigation-layout-justification-setting' => $justification,
  75          '--navigation-layout-direction'             => $is_vertical ? 'column' : 'row',
  76          '--navigation-layout-wrap'                  => is_array( $layout ) && 'nowrap' === ( $layout['flexWrap'] ?? null ) ? 'nowrap' : 'wrap',
  77          '--navigation-layout-justify'               => $justify,
  78          '--navigation-layout-align'                 => $align,
  79      );
  80  }
  81  
  82  /**
  83   * Helper functions used to render the navigation block.
  84   *
  85   * @since 6.5.0
  86   */
  87  class WP_Navigation_Block_Renderer {
  88  
  89      /**
  90       * Used to determine whether or not a navigation has submenus.
  91       *
  92       * @since 6.5.0
  93       */
  94      private static $has_submenus = false;
  95  
  96      /**
  97       * Used to determine which blocks need an <li> wrapper.
  98       *
  99       * @since 6.5.0
 100       *
 101       * @var array
 102       */
 103      private static $needs_list_item_wrapper = array(
 104          'core/site-title',
 105          'core/site-logo',
 106          'core/social-links',
 107      );
 108  
 109      /**
 110       * Keeps track of all the navigation names that have been seen.
 111       *
 112       * @since 6.5.0
 113       *
 114       * @var array
 115       */
 116      private static $seen_menu_names = array();
 117  
 118  
 119      /**
 120       * Returns whether or not this is responsive navigation.
 121       *
 122       * @since 6.5.0
 123       *
 124       * @param array $attributes The block attributes.
 125       * @return bool Returns whether or not this is responsive navigation.
 126       */
 127  	private static function is_responsive( $attributes ) {
 128          /**
 129           * This is for backwards compatibility after the `isResponsive` attribute was been removed.
 130           */
 131  
 132          $has_old_responsive_attribute = ! empty( $attributes['isResponsive'] ) && $attributes['isResponsive'];
 133          return isset( $attributes['overlayMenu'] ) && 'never' !== $attributes['overlayMenu'] || $has_old_responsive_attribute;
 134      }
 135  
 136      /**
 137       * Returns whether or not a navigation has a submenu.
 138       *
 139       * @since 6.5.0
 140       *
 141       * @param WP_Block_List $inner_blocks The list of inner blocks.
 142       * @return bool Returns whether or not a navigation has a submenu and also sets the member variable.
 143       */
 144  	private static function has_submenus( $inner_blocks ) {
 145          if ( true === static::$has_submenus ) {
 146              return static::$has_submenus;
 147          }
 148  
 149          foreach ( $inner_blocks as $inner_block ) {
 150              // If this is a page list then work out if any of the pages have children.
 151              if ( 'core/page-list' === $inner_block->name ) {
 152                  $all_pages = get_pages(
 153                      array(
 154                          'sort_column' => 'menu_order,post_title',
 155                          'order'       => 'asc',
 156                      )
 157                  );
 158                  foreach ( (array) $all_pages as $page ) {
 159                      if ( $page->post_parent ) {
 160                          static::$has_submenus = true;
 161                          break;
 162                      }
 163                  }
 164              }
 165              // If this is a navigation submenu then we know we have submenus.
 166              if ( 'core/navigation-submenu' === $inner_block->name ) {
 167                  static::$has_submenus = true;
 168                  break;
 169              }
 170          }
 171  
 172          return static::$has_submenus;
 173      }
 174  
 175      /**
 176       * Determine whether the navigation blocks is interactive.
 177       *
 178       * @since 6.5.0
 179       *
 180       * @param array         $attributes   The block attributes.
 181       * @param WP_Block_List $inner_blocks The list of inner blocks.
 182       * @return bool Returns whether or not to load the view script.
 183       */
 184  	private static function is_interactive( $attributes, $inner_blocks ) {
 185          $has_submenus        = static::has_submenus( $inner_blocks );
 186          $is_responsive_menu  = static::is_responsive( $attributes );
 187          $computed_visibility = block_core_navigation_get_submenu_visibility( $attributes );
 188          $open_on_click       = 'click' === $computed_visibility;
 189          $show_submenu_icon   = ! empty( $attributes['showSubmenuIcon'] );
 190          return ( $has_submenus && ( $open_on_click || $show_submenu_icon ) ) || $is_responsive_menu;
 191      }
 192  
 193      /**
 194       * Returns whether or not a block needs a list item wrapper.
 195       *
 196       * @since 6.5.0
 197       *
 198       * @param WP_Block $block The block.
 199       * @return bool Returns whether or not a block needs a list item wrapper.
 200       */
 201  	private static function does_block_need_a_list_item_wrapper( $block ) {
 202  
 203          /**
 204           * Filter the list of blocks that need a list item wrapper.
 205           *
 206           * Affords the ability to customize which blocks need a list item wrapper when rendered
 207           * within a core/navigation block.
 208           * This is useful for blocks that are not list items but should be wrapped in a list
 209           * item when used as a child of a navigation block.
 210           *
 211           * @since 6.5.0
 212           *
 213           * @param array $needs_list_item_wrapper The list of blocks that need a list item wrapper.
 214           */
 215          $needs_list_item_wrapper = apply_filters( 'block_core_navigation_listable_blocks', static::$needs_list_item_wrapper );
 216  
 217          return in_array( $block->name, $needs_list_item_wrapper, true );
 218      }
 219  
 220      /**
 221       * Returns the markup for a single inner block.
 222       *
 223       * @since 6.5.0
 224       *
 225       * @param WP_Block $inner_block The inner block.
 226       * @return string Returns the markup for a single inner block.
 227       */
 228  	private static function get_markup_for_inner_block( $inner_block ) {
 229          $inner_block_content = $inner_block->render();
 230          if ( ! empty( $inner_block_content ) ) {
 231              if ( static::does_block_need_a_list_item_wrapper( $inner_block ) ) {
 232                  return '<li class="wp-block-navigation-item">' . $inner_block_content . '</li>';
 233              }
 234          }
 235  
 236          return $inner_block_content;
 237      }
 238  
 239      /**
 240       * Returns the html for blocks from a template part (without navigation container wrapper).
 241       *
 242       * @since 6.5.0
 243       *
 244       * @param WP_Block_List $blocks The list of blocks to render.
 245       * @return string Returns the html for the template part blocks.
 246       */
 247  	private static function get_template_part_blocks_html( $blocks ) {
 248          $html = '';
 249          foreach ( $blocks as $block ) {
 250              $html .= $block->render();
 251          }
 252          return $html;
 253      }
 254  
 255      /**
 256       * Returns the html for the inner blocks of the navigation block.
 257       *
 258       * @since 6.5.0
 259       *
 260       * @param array         $attributes   The block attributes.
 261       * @param WP_Block_List $inner_blocks The list of inner blocks.
 262       * @return string Returns the html for the inner blocks of the navigation block.
 263       */
 264  	private static function get_inner_blocks_html( $attributes, $inner_blocks ) {
 265          $has_submenus   = static::has_submenus( $inner_blocks );
 266          $is_interactive = static::is_interactive( $attributes, $inner_blocks );
 267  
 268          $style                = static::get_styles( $attributes );
 269          $class                = static::get_classes( $attributes );
 270          $container_attributes = get_block_wrapper_attributes(
 271              array(
 272                  'class' => 'wp-block-navigation__container ' . $class,
 273                  'style' => $style,
 274              )
 275          );
 276  
 277          $inner_blocks_html = '';
 278          $is_list_open      = false;
 279  
 280          foreach ( $inner_blocks as $inner_block ) {
 281              $inner_block_markup = static::get_markup_for_inner_block( $inner_block );
 282              $p                  = new WP_HTML_Tag_Processor( $inner_block_markup );
 283              $is_list_item       = $p->next_tag( 'LI' );
 284  
 285              if ( $is_list_item && ! $is_list_open ) {
 286                  $is_list_open       = true;
 287                  $inner_blocks_html .= sprintf(
 288                      '<ul %1$s>',
 289                      $container_attributes
 290                  );
 291              }
 292  
 293              if ( ! $is_list_item && $is_list_open ) {
 294                  $is_list_open       = false;
 295                  $inner_blocks_html .= '</ul>';
 296              }
 297  
 298              $inner_blocks_html .= $inner_block_markup;
 299          }
 300  
 301          if ( $is_list_open ) {
 302              $inner_blocks_html .= '</ul>';
 303          }
 304  
 305          // Add directives to the submenu if needed.
 306          if ( $has_submenus && $is_interactive ) {
 307              $tags              = new WP_HTML_Tag_Processor( $inner_blocks_html );
 308              $inner_blocks_html = block_core_navigation_add_directives_to_submenu( $tags, $attributes );
 309          }
 310  
 311          return $inner_blocks_html;
 312      }
 313  
 314      /**
 315       * Gets the inner blocks for the navigation block from the navigation post.
 316       *
 317       * @since 6.5.0
 318       *
 319       * @param array $attributes The block attributes.
 320       * @return WP_Block_List Returns the inner blocks for the navigation block.
 321       */
 322  	private static function get_inner_blocks_from_navigation_post( $attributes ) {
 323          $navigation_post = get_post( $attributes['ref'] );
 324          if ( ! isset( $navigation_post ) ) {
 325              return new WP_Block_List( array(), $attributes );
 326          }
 327  
 328          // Only published posts are valid. If this is changed then a corresponding change
 329          // must also be implemented in `use-navigation-menu.js`.
 330          if ( 'publish' === $navigation_post->post_status ) {
 331              $parsed_blocks = parse_blocks( $navigation_post->post_content );
 332  
 333              // 'parse_blocks' includes a null block with '\n\n' as the content when
 334              // it encounters whitespace. This code strips it.
 335              $blocks = block_core_navigation_filter_out_empty_blocks( $parsed_blocks );
 336  
 337              // Re-serialize, and run Block Hooks algorithm to inject hooked blocks.
 338              // TODO: See if we can move the apply_block_hooks_to_content_from_post_object() call
 339              // before the parse_blocks() call further above, to avoid the extra serialization/parsing.
 340              $markup = serialize_blocks( $blocks );
 341              $markup = apply_block_hooks_to_content_from_post_object( $markup, $navigation_post );
 342              $blocks = parse_blocks( $markup );
 343  
 344              // TODO - this uses the full navigation block attributes for the
 345              // context which could be refined.
 346              return new WP_Block_List( $blocks, $attributes );
 347          }
 348      }
 349  
 350      /**
 351       * Gets the inner blocks for the navigation block from the fallback.
 352       *
 353       * @since 6.5.0
 354       *
 355       * @param array $attributes The block attributes.
 356       * @return WP_Block_List Returns the inner blocks for the navigation block.
 357       */
 358  	private static function get_inner_blocks_from_fallback( $attributes ) {
 359          $fallback_blocks = block_core_navigation_get_fallback_blocks();
 360  
 361          // Fallback my have been filtered so do basic test for validity.
 362          if ( empty( $fallback_blocks ) || ! is_array( $fallback_blocks ) ) {
 363              return new WP_Block_List( array(), $attributes );
 364          }
 365  
 366          return new WP_Block_List( $fallback_blocks, $attributes );
 367      }
 368  
 369      /**
 370       * Recursively disables overlay menu for navigation blocks within overlay blocks.
 371       * Prevents nested overlays (inception).
 372       *
 373       * @since 6.5.0
 374       *
 375       * @param array $blocks Array of parsed block arrays.
 376       * @return array Modified blocks with overlayMenu set to 'never' for navigation blocks.
 377       */
 378      private static function disable_overlay_menu_for_nested_navigation_blocks( $blocks ) {
 379          if ( empty( $blocks ) || ! is_array( $blocks ) ) {
 380              return $blocks;
 381          }
 382  
 383          foreach ( $blocks as &$block ) {
 384              if ( ! isset( $block['blockName'] ) ) {
 385                  continue;
 386              }
 387  
 388              // If this is a navigation block, disable its overlay menu.
 389              if ( 'core/navigation' === $block['blockName'] ) {
 390                  if ( ! isset( $block['attrs'] ) ) {
 391                      $block['attrs'] = array();
 392                  }
 393                  $block['attrs']['overlayMenu'] = 'never';
 394                  // Mark this as a nested navigation within an overlay template part
 395                  // so we can handle its rendering differently.
 396                  $block['attrs']['_isWithinOverlayTemplatePart'] = true;
 397              }
 398  
 399              // Recursively process inner blocks.
 400              if ( ! empty( $block['innerBlocks'] ) && is_array( $block['innerBlocks'] ) ) {
 401                  $block['innerBlocks'] = static::disable_overlay_menu_for_nested_navigation_blocks( $block['innerBlocks'] );
 402              }
 403          }
 404  
 405          return $blocks;
 406      }
 407  
 408      /**
 409       * Gets the inner blocks for the navigation block from an overlay template part.
 410       *
 411       * @since 6.5.0
 412       *
 413       * @param string $overlay_template_part_id The overlay template part ID in format "theme//slug".
 414       * @param array  $attributes                The block attributes.
 415       * @return WP_Block_List Returns the inner blocks for the overlay template part.
 416       */
 417  	private static function get_overlay_blocks_from_template_part( $overlay_template_part_id, $attributes ) {
 418          if ( empty( $overlay_template_part_id ) || ! is_string( $overlay_template_part_id ) ) {
 419              return new WP_Block_List( array(), $attributes );
 420          }
 421  
 422          // Parse the template part ID (format: "theme//slug").
 423          // If it's just a slug, construct the full ID using the current theme.
 424          $parts = explode( '//', $overlay_template_part_id, 2 );
 425          if ( count( $parts ) === 2 ) {
 426              // Already in "theme//slug" format (backward compatibility).
 427              $theme = $parts[0];
 428              $slug  = $parts[1];
 429          } else {
 430              // Just a slug, use current theme.
 431              $theme = get_stylesheet();
 432              $slug  = $overlay_template_part_id;
 433          }
 434  
 435          // Only query for template parts from the active theme.
 436          if ( get_stylesheet() !== $theme ) {
 437              return new WP_Block_List( array(), $attributes );
 438          }
 439  
 440          // Query for the template part post.
 441          $template_part_query = new WP_Query(
 442              array(
 443                  'post_type'           => 'wp_template_part',
 444                  'post_status'         => 'publish',
 445                  'post_name__in'       => array( $slug ),
 446                  'tax_query'           => array(
 447                      array(
 448                          'taxonomy' => 'wp_theme',
 449                          'field'    => 'name',
 450                          'terms'    => $theme,
 451                      ),
 452                  ),
 453                  'posts_per_page'      => 1,
 454                  'no_found_rows'       => true,
 455                  'lazy_load_term_meta' => false, // Do not lazy load term meta, as template parts only have one term.
 456              )
 457          );
 458  
 459          $template_part_post = $template_part_query->have_posts() ? $template_part_query->next_post() : null;
 460  
 461          if ( ! $template_part_post ) {
 462              // Try to get from theme file if not in database.
 463              // Construct the full template part ID for get_block_file_template.
 464              $full_template_part_id = $theme . '//' . $slug;
 465              $block_template        = get_block_file_template( $full_template_part_id, 'wp_template_part' );
 466              if ( isset( $block_template->content ) ) {
 467                  // Expand shortcodes before parsing blocks, matching the order in
 468                  // `render_block_core_template_part()`.
 469                  $content       = shortcode_unautop( $block_template->content );
 470                  $content       = do_shortcode( $content );
 471                  $parsed_blocks = parse_blocks( $content );
 472                  $blocks        = block_core_navigation_filter_out_empty_blocks( $parsed_blocks );
 473                  // Disable overlay menu for any navigation blocks within the overlay to prevent nested overlays.
 474                  $blocks = static::disable_overlay_menu_for_nested_navigation_blocks( $blocks );
 475                  return new WP_Block_List( $blocks, $attributes );
 476              }
 477              return new WP_Block_List( array(), $attributes );
 478          }
 479  
 480          // Get the template part content.
 481          $block_template = _build_block_template_result_from_post( $template_part_post );
 482          if ( ! isset( $block_template->content ) ) {
 483              return new WP_Block_List( array(), $attributes );
 484          }
 485  
 486          $parsed_blocks = parse_blocks( $block_template->content );
 487  
 488          // 'parse_blocks' includes a null block with '\n\n' as the content when
 489          // it encounters whitespace. This code strips it.
 490          $blocks = block_core_navigation_filter_out_empty_blocks( $parsed_blocks );
 491  
 492          // Re-serialize, and run Block Hooks algorithm to inject hooked blocks.
 493          $markup = serialize_blocks( $blocks );
 494          $markup = apply_block_hooks_to_content_from_post_object( $markup, $template_part_post );
 495  
 496          // Expand shortcodes before parsing blocks, matching the order in
 497          // `render_block_core_template_part()`.
 498          $markup = shortcode_unautop( $markup );
 499          $markup = do_shortcode( $markup );
 500  
 501          $blocks = parse_blocks( $markup );
 502  
 503          // Disable overlay menu for any navigation blocks within the overlay to prevent nested overlays.
 504          $blocks = static::disable_overlay_menu_for_nested_navigation_blocks( $blocks );
 505  
 506          return new WP_Block_List( $blocks, $attributes );
 507      }
 508  
 509      /**
 510       * Gets the inner blocks for the navigation block.
 511       *
 512       * @since 6.5.0
 513       *
 514       * @param array    $attributes The block attributes.
 515       * @param WP_Block $block The parsed block.
 516       * @return WP_Block_List Returns the inner blocks for the navigation block.
 517       */
 518  	private static function get_inner_blocks( $attributes, $block ) {
 519          $inner_blocks = $block->inner_blocks;
 520  
 521          // Ensure that blocks saved with the legacy ref attribute name (navigationMenuId) continue to render.
 522          if ( array_key_exists( 'navigationMenuId', $attributes ) ) {
 523              $attributes['ref'] = $attributes['navigationMenuId'];
 524          }
 525  
 526          // If:
 527          // - the gutenberg plugin is active
 528          // - `__unstableLocation` is defined
 529          // - we have menu items at the defined location
 530          // - we don't have a relationship to a `wp_navigation` Post (via `ref`).
 531          // ...then create inner blocks from the classic menu assigned to that location.
 532          if (
 533              defined( 'IS_GUTENBERG_PLUGIN' ) && IS_GUTENBERG_PLUGIN &&
 534              array_key_exists( '__unstableLocation', $attributes ) &&
 535              ! array_key_exists( 'ref', $attributes ) &&
 536              ! empty( block_core_navigation_get_menu_items_at_location( $attributes['__unstableLocation'] ) )
 537          ) {
 538              $inner_blocks = block_core_navigation_get_inner_blocks_from_unstable_location( $attributes );
 539          }
 540  
 541          // Load inner blocks from the navigation post.
 542          if ( array_key_exists( 'ref', $attributes ) ) {
 543              $inner_blocks = static::get_inner_blocks_from_navigation_post( $attributes );
 544          }
 545  
 546          // If there are no inner blocks then fallback to rendering an appropriate fallback.
 547          if ( empty( $inner_blocks ) ) {
 548              $inner_blocks = static::get_inner_blocks_from_fallback( $attributes );
 549          }
 550  
 551          /**
 552           * Filter navigation block $inner_blocks.
 553           * Allows modification of a navigation block menu items.
 554           *
 555           * @since 6.1.0
 556           *
 557           * @param \WP_Block_List $inner_blocks
 558           */
 559          $inner_blocks = apply_filters( 'block_core_navigation_render_inner_blocks', $inner_blocks );
 560  
 561          $post_ids = block_core_navigation_get_post_ids( $inner_blocks );
 562          if ( $post_ids ) {
 563              _prime_post_caches( $post_ids, false, false );
 564          }
 565  
 566          return $inner_blocks;
 567      }
 568  
 569      /**
 570       * Gets the name of the current navigation, if it has one.
 571       *
 572       * @since 6.5.0
 573       *
 574       * @param array $attributes The block attributes.
 575       * @return string Returns the name of the navigation.
 576       */
 577  	private static function get_navigation_name( $attributes ) {
 578  
 579          $navigation_name = $attributes['ariaLabel'] ?? '';
 580  
 581          if ( ! empty( $navigation_name ) ) {
 582              return $navigation_name;
 583          }
 584  
 585          // Load the navigation post.
 586          if ( array_key_exists( 'ref', $attributes ) ) {
 587              $navigation_post = get_post( $attributes['ref'] );
 588              if ( ! isset( $navigation_post ) ) {
 589                  return $navigation_name;
 590              }
 591  
 592              // Only published posts are valid. If this is changed then a corresponding change
 593              // must also be implemented in `use-navigation-menu.js`.
 594              if ( 'publish' === $navigation_post->post_status ) {
 595                  return $navigation_post->post_title;
 596              }
 597          }
 598  
 599          return $navigation_name;
 600      }
 601  
 602      /**
 603       * Returns the layout class for the navigation block.
 604       *
 605       * @since 6.5.0
 606       *
 607       * @param array $attributes The block attributes.
 608       * @return string Returns the layout class for the navigation block.
 609       */
 610  	private static function get_layout_class( $attributes ) {
 611          $layout_justification = array(
 612              'left'          => 'items-justified-left',
 613              'right'         => 'items-justified-right',
 614              'center'        => 'items-justified-center',
 615              'space-between' => 'items-justified-space-between',
 616          );
 617  
 618          $layout_class = '';
 619          if (
 620              isset( $attributes['layout']['justifyContent'] ) &&
 621              isset( $layout_justification[ $attributes['layout']['justifyContent'] ] )
 622          ) {
 623              $layout_class .= $layout_justification[ $attributes['layout']['justifyContent'] ];
 624          }
 625          if ( isset( $attributes['layout']['orientation'] ) && 'vertical' === $attributes['layout']['orientation'] ) {
 626              $layout_class .= ' is-vertical';
 627          }
 628  
 629          if ( isset( $attributes['layout']['flexWrap'] ) && 'nowrap' === $attributes['layout']['flexWrap'] ) {
 630              $layout_class .= ' no-wrap';
 631          }
 632          return $layout_class;
 633      }
 634  
 635      /**
 636       * Return classes for the navigation block.
 637       *
 638       * @since 6.5.0
 639       *
 640       * @param array $attributes The block attributes.
 641       * @return string Returns the classes for the navigation block.
 642       */
 643  	private static function get_classes( $attributes ) {
 644          // Restore legacy classnames for submenu positioning.
 645          $layout_class       = static::get_layout_class( $attributes );
 646          $colors             = block_core_navigation_build_css_colors( $attributes );
 647          $font_sizes         = block_core_navigation_build_css_font_sizes( $attributes );
 648          $is_responsive_menu = static::is_responsive( $attributes );
 649  
 650          // Manually add block support text decoration as CSS class.
 651          $text_decoration       = $attributes['style']['typography']['textDecoration'] ?? null;
 652          $text_decoration_class = sprintf( 'has-text-decoration-%s', $text_decoration );
 653  
 654          $classes = array_merge(
 655              $colors['css_classes'],
 656              $font_sizes['css_classes'],
 657              $is_responsive_menu ? array( 'is-responsive' ) : array(),
 658              $layout_class ? array( $layout_class ) : array(),
 659              $text_decoration ? array( $text_decoration_class ) : array()
 660          );
 661          return implode( ' ', $classes );
 662      }
 663  
 664      /**
 665       * Get styles for the navigation block.
 666       *
 667       * @since 6.5.0
 668       *
 669       * @param array $attributes The block attributes.
 670       * @return string Returns the styles for the navigation block.
 671       */
 672  	private static function get_styles( $attributes ) {
 673          $colors       = block_core_navigation_build_css_colors( $attributes );
 674          $font_sizes   = block_core_navigation_build_css_font_sizes( $attributes );
 675          $block_styles = $attributes['styles'] ?? '';
 676          return $block_styles . $colors['inline_styles'] . $font_sizes['inline_styles'];
 677      }
 678  
 679      /**
 680       * Get responsive container classes for the navigation block.
 681       *
 682       * @since 7.0.0
 683       *
 684       * @param bool  $is_hidden_by_default Whether the responsive menu is hidden by default.
 685       * @param bool  $has_custom_overlay Whether a custom overlay is used.
 686       * @param array $colors The colors array.
 687       * @return array Returns the responsive container classes.
 688       */
 689  	private static function get_responsive_container_classes( $is_hidden_by_default, $has_custom_overlay, $colors ) {
 690          $responsive_container_classes = array( 'wp-block-navigation__responsive-container' );
 691  
 692          if ( $is_hidden_by_default ) {
 693              $responsive_container_classes[] = 'hidden-by-default';
 694          }
 695  
 696          if ( $has_custom_overlay ) {
 697              $responsive_container_classes[] = 'disable-default-overlay';
 698          } else {
 699              // Don't apply overlay color classes if using a custom overlay template part.
 700              // The custom overlay is responsible for its own styling.
 701              $responsive_container_classes[] = implode( ' ', $colors['overlay_css_classes'] );
 702          }
 703  
 704          return $responsive_container_classes;
 705      }
 706  
 707      /**
 708       * Get overlay inline styles for the navigation block.
 709       *
 710       * @since 7.0.0
 711       *
 712       * @param array $colors The colors array.
 713       * @return string Returns the overlay inline styles.
 714       */
 715  	private static function get_overlay_inline_styles( $has_custom_overlay, $colors ) {
 716          $overlay_inline_styles = $has_custom_overlay ? '' : esc_attr( safecss_filter_attr( $colors['overlay_inline_styles'] ) );
 717          return ( ! empty( $overlay_inline_styles ) ) ? "style=\"$overlay_inline_styles\"" : '';
 718      }
 719  
 720      /**
 721       * Get the responsive container markup
 722       *
 723       * @since 6.5.0
 724       *
 725       * @param array         $attributes The block attributes.
 726       * @param WP_Block_List $inner_blocks The list of inner blocks.
 727       * @param string        $inner_blocks_html The markup for the inner blocks.
 728       * @return string Returns the container markup.
 729       */
 730  	private static function get_responsive_container_markup( $attributes, $inner_blocks, $inner_blocks_html ) {
 731          $is_interactive  = static::is_interactive( $attributes, $inner_blocks );
 732          $colors          = block_core_navigation_build_css_colors( $attributes );
 733          $modal_unique_id = wp_unique_id( 'modal-' );
 734  
 735          $is_hidden_by_default = isset( $attributes['overlayMenu'] ) && 'always' === $attributes['overlayMenu'];
 736  
 737          // Set-up variables for custom overlays.
 738          $has_custom_overlay             = false;
 739          $close_button_markup            = '';
 740          $has_custom_overlay_close_block = false;
 741          $overlay_blocks_html            = '';
 742          $custom_overlay_markup          = '';
 743  
 744          // Check if an overlay template part is selected and render it.
 745          // This needs to happen before building classes so we know if overlay blocks actually exist.
 746          if ( ! empty( $attributes['overlay'] ) ) {
 747              // Get blocks from the overlay template part.
 748              $overlay_blocks = static::get_overlay_blocks_from_template_part( $attributes['overlay'], $attributes );
 749              // Render template part blocks directly without navigation container wrapper.
 750              $overlay_blocks_html = static::get_template_part_blocks_html( $overlay_blocks );
 751              // Check if overlay contains a navigation-overlay-close block (detect in rendered HTML so it works with patterns).
 752              $has_custom_overlay_close_block = block_core_navigation_overlay_html_has_close_block( $overlay_blocks_html );
 753              // Add Interactivity API directives to the overlay close block if present.
 754              if ( $has_custom_overlay_close_block && $is_interactive ) {
 755                  $tags                = new WP_HTML_Tag_Processor( $overlay_blocks_html );
 756                  $overlay_blocks_html = block_core_navigation_add_directives_to_overlay_close( $tags );
 757              }
 758              // Images in the overlay are hidden until the menu is opened. Pre-set
 759              // fetchpriority="low" so that when wp_filter_content_tags() processes the
 760              // parent template part, it sees the attribute already present and calls
 761              // wp_get_loading_optimization_attributes() with fetchpriority="low", which both prevents
 762              // fetchpriority="high" from being added and stops the LCP counter from being incremented.
 763              $overlay_blocks_html = block_core_navigation_set_overlay_image_fetch_priority( $overlay_blocks_html );
 764          }
 765  
 766          $has_custom_overlay = ! empty( $overlay_blocks_html );
 767  
 768          $responsive_container_classes = static::get_responsive_container_classes( $is_hidden_by_default, $has_custom_overlay, $colors );
 769  
 770          $open_button_classes = array(
 771              'wp-block-navigation__responsive-container-open',
 772              $is_hidden_by_default ? 'always-shown' : '',
 773          );
 774  
 775          $should_display_icon_label = isset( $attributes['hasIcon'] ) && true === $attributes['hasIcon'];
 776          $toggle_button_icon        = '<svg width="24" height="24" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M4 7.5h16v1.5H4z"></path><path d="M4 15h16v1.5H4z"></path></svg>';
 777          if ( isset( $attributes['icon'] ) ) {
 778              if ( 'menu' === $attributes['icon'] ) {
 779                  $toggle_button_icon = '<svg width="24" height="24" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M5 5v1.5h14V5H5z"></path><path d="M5 12.8h14v-1.5H5v1.5z"></path><path d="M5 19h14v-1.5H5V19z"></path></svg>';
 780              }
 781          }
 782          $toggle_button_content       = $should_display_icon_label ? $toggle_button_icon : __( 'Menu' );
 783          $toggle_close_button_icon    = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24" aria-hidden="true" focusable="false"><path d="m13.06 12 6.47-6.47-1.06-1.06L12 10.94 5.53 4.47 4.47 5.53 10.94 12l-6.47 6.47 1.06 1.06L12 13.06l6.47 6.47 1.06-1.06L13.06 12Z"></path></svg>';
 784          $toggle_close_button_content = $should_display_icon_label ? $toggle_close_button_icon : __( 'Close' );
 785          $toggle_aria_label_open      = $should_display_icon_label ? 'aria-label="' . __( 'Open menu' ) . '"' : ''; // Open button label.
 786          $toggle_aria_label_close     = $should_display_icon_label ? 'aria-label="' . __( 'Close menu' ) . '"' : ''; // Close button label.
 787  
 788          // Add Interactivity API directives to the markup if needed.
 789          $open_button_directives          = '';
 790          $responsive_container_directives = '';
 791          $responsive_dialog_directives    = '';
 792          $close_button_directives         = '';
 793          if ( $is_interactive ) {
 794              $open_button_directives                  = '
 795                  data-wp-on--click="actions.openMenuOnClick"
 796                  data-wp-on--keydown="actions.handleMenuKeydown"
 797              ';
 798              $responsive_container_directives         = '
 799                  data-wp-class--has-modal-open="state.isMenuOpen"
 800                  data-wp-class--is-menu-open="state.isMenuOpen"
 801                  data-wp-watch="callbacks.initMenu"
 802                  data-wp-on--keydown="actions.handleMenuKeydown"
 803                  data-wp-on--focusout="actions.handleMenuFocusout"
 804                  tabindex="-1"
 805              ';
 806              $responsive_dialog_directives            = '
 807                  data-wp-bind--aria-modal="state.ariaModal"
 808                  data-wp-bind--aria-label="state.ariaLabel"
 809                  data-wp-bind--role="state.roleAttribute"
 810              ';
 811              $close_button_directives                 = '
 812                  data-wp-on--click="actions.closeMenuOnClick"
 813              ';
 814              $responsive_container_content_directives = '
 815                  data-wp-watch="callbacks.focusFirstElement"
 816              ';
 817          }
 818  
 819          // Don't apply overlay inline styles if using a custom overlay template part.
 820          // The custom overlay is responsible for its own styling.
 821          $overlay_inline_styles = static::get_overlay_inline_styles( $has_custom_overlay, $colors );
 822  
 823          if ( $has_custom_overlay ) {
 824              $custom_overlay_markup = sprintf(
 825                  '<div class="wp-block-navigation__overlay-container">%s</div>',
 826                  $overlay_blocks_html
 827              );
 828          }
 829  
 830          // Show default close button for all responsive navigation,
 831          // unless custom overlay has its own close block.
 832          if ( ! $has_custom_overlay_close_block ) {
 833              $close_button_markup = sprintf(
 834                  '<button %1$s class="wp-block-navigation__responsive-container-close" %2$s>%3$s</button>',
 835                  $toggle_aria_label_close,
 836                  $close_button_directives,
 837                  $toggle_close_button_content
 838              );
 839          }
 840  
 841          return sprintf(
 842              '<button aria-haspopup="dialog" %3$s class="%6$s" %10$s>%8$s</button>
 843                  <div class="%5$s" %7$s id="%1$s" %11$s>
 844                      <div class="wp-block-navigation__responsive-close" tabindex="-1">
 845                          <div class="wp-block-navigation__responsive-dialog" %12$s>
 846                              %13$s
 847                              <div class="wp-block-navigation__responsive-container-content" %14$s id="%1$s-content">
 848                                  %2$s
 849                                  %15$s
 850                              </div>
 851                          </div>
 852                      </div>
 853                  </div>',
 854              esc_attr( $modal_unique_id ),
 855              $inner_blocks_html,
 856              $toggle_aria_label_open,
 857              $toggle_aria_label_close,
 858              esc_attr( trim( implode( ' ', $responsive_container_classes ) ) ),
 859              esc_attr( trim( implode( ' ', $open_button_classes ) ) ),
 860              $overlay_inline_styles,
 861              $toggle_button_content,
 862              $toggle_close_button_content,
 863              $open_button_directives,
 864              $responsive_container_directives,
 865              $responsive_dialog_directives,
 866              $close_button_markup,
 867              $responsive_container_content_directives,
 868              $has_custom_overlay ? $custom_overlay_markup : ''
 869          );
 870      }
 871  
 872      /**
 873       * Get the wrapper attributes
 874       *
 875       * @since 6.5.0
 876       *
 877       * @param array         $attributes    The block attributes.
 878       * @param WP_Block_List $inner_blocks  A list of inner blocks.
 879       * @return string Returns the navigation block markup.
 880       */
 881  	private static function get_nav_attributes( $attributes, $inner_blocks ) {
 882          $is_interactive     = static::is_interactive( $attributes, $inner_blocks );
 883          $is_responsive_menu = static::is_responsive( $attributes );
 884          $style              = static::get_styles( $attributes );
 885          $class              = static::get_classes( $attributes );
 886          $extra_attributes   = array(
 887              'class' => $class,
 888              'style' => $style,
 889          );
 890          // Only add aria-label for top-level navigation blocks.
 891          // Skip navigation blocks marked as being within overlay template parts.
 892          $is_within_overlay = $attributes['_isWithinOverlayTemplatePart'] ?? false;
 893          if ( $is_within_overlay ) {
 894              $nav_menu_name = static::get_navigation_name( $attributes );
 895          } else {
 896              $nav_menu_name = static::get_unique_navigation_name( $attributes );
 897          }
 898  
 899          if ( ! empty( $nav_menu_name ) ) {
 900              $extra_attributes['aria-label'] = $nav_menu_name;
 901          }
 902          $wrapper_attributes = get_block_wrapper_attributes( $extra_attributes );
 903  
 904          if ( $is_responsive_menu ) {
 905              $nav_element_directives = static::get_nav_element_directives( $is_interactive );
 906              $wrapper_attributes    .= ' ' . $nav_element_directives;
 907          }
 908  
 909          return $wrapper_attributes;
 910      }
 911  
 912      /**
 913       * Gets the nav element directives.
 914       *
 915       * @since 6.5.0
 916       *
 917       * @param bool $is_interactive Whether the block is interactive.
 918       * @return string the directives for the navigation element.
 919       */
 920  	private static function get_nav_element_directives( $is_interactive ) {
 921          if ( ! $is_interactive ) {
 922              return '';
 923          }
 924          // When adding to this array be mindful of security concerns.
 925          $nav_element_context    = wp_interactivity_data_wp_context(
 926              array(
 927                  'overlayOpenedBy' => array(
 928                      'click' => false,
 929                      'hover' => false,
 930                      'focus' => false,
 931                  ),
 932                  'type'            => 'overlay',
 933                  'roleAttribute'   => '',
 934                  'ariaLabel'       => __( 'Menu' ),
 935              )
 936          );
 937          $nav_element_directives = '
 938           data-wp-interactive="core/navigation" '
 939          . $nav_element_context;
 940  
 941          return $nav_element_directives;
 942      }
 943  
 944      /**
 945       * Handle view script module loading.
 946       *
 947       * @since 6.5.0
 948       *
 949       * @param array         $attributes   The block attributes.
 950       * @param WP_Block      $block        The parsed block.
 951       * @param WP_Block_List $inner_blocks The list of inner blocks.
 952       */
 953  	private static function handle_view_script_module_loading( $attributes, $block, $inner_blocks ) {
 954          if ( static::is_interactive( $attributes, $inner_blocks ) ) {
 955              wp_enqueue_script_module( '@wordpress/block-library/navigation/view' );
 956          }
 957      }
 958  
 959      /**
 960       * Returns the markup for the navigation block.
 961       *
 962       * @since 6.5.0
 963       *
 964       * @param array         $attributes The block attributes.
 965       * @param WP_Block_List $inner_blocks The list of inner blocks.
 966       * @return string Returns the navigation wrapper markup.
 967       */
 968  	private static function get_inner_block_markup( $attributes, $inner_blocks ) {
 969          $inner_blocks_html = static::get_inner_blocks_html( $attributes, $inner_blocks );
 970          if ( static::is_responsive( $attributes ) ) {
 971              return static::get_responsive_container_markup( $attributes, $inner_blocks, $inner_blocks_html );
 972          }
 973          return $inner_blocks_html;
 974      }
 975  
 976      /**
 977       * Returns a unique name for the navigation.
 978       *
 979       * @since 6.5.0
 980       *
 981       * @param array $attributes The block attributes.
 982       * @return string Returns a unique name for the navigation.
 983       */
 984  	private static function get_unique_navigation_name( $attributes ) {
 985          $nav_menu_name = static::get_navigation_name( $attributes );
 986  
 987          // This is used to count the number of times a navigation name has been seen,
 988          // so that we can ensure every navigation has a unique id.
 989          if ( isset( static::$seen_menu_names[ $nav_menu_name ] ) ) {
 990              ++static::$seen_menu_names[ $nav_menu_name ];
 991          } else {
 992              static::$seen_menu_names[ $nav_menu_name ] = 1;
 993          }
 994  
 995          // If the menu name has been used previously then append an ID
 996          // to the name to ensure uniqueness across a given post.
 997          if ( isset( static::$seen_menu_names[ $nav_menu_name ] ) && static::$seen_menu_names[ $nav_menu_name ] > 1 ) {
 998              $count         = static::$seen_menu_names[ $nav_menu_name ];
 999              $nav_menu_name = $nav_menu_name . ' ' . ( $count );
1000          }
1001  
1002          return $nav_menu_name;
1003      }
1004  
1005      /**
1006       * Renders the navigation block.
1007       *
1008       * @since 6.5.0
1009       *
1010       * @param array    $attributes The block attributes.
1011       * @param string   $content    The saved content.
1012       * @param WP_Block $block      The parsed block.
1013       * @return string Returns the navigation block markup.
1014       */
1015  	public static function render( $attributes, $content, $block ) {
1016          /**
1017           * Deprecated:
1018           * The rgbTextColor and rgbBackgroundColor attributes
1019           * have been deprecated in favor of
1020           * customTextColor and customBackgroundColor ones.
1021           * Move the values from old attrs to the new ones.
1022           */
1023          if ( isset( $attributes['rgbTextColor'] ) && empty( $attributes['textColor'] ) ) {
1024              $attributes['customTextColor'] = $attributes['rgbTextColor'];
1025          }
1026  
1027          if ( isset( $attributes['rgbBackgroundColor'] ) && empty( $attributes['backgroundColor'] ) ) {
1028              $attributes['customBackgroundColor'] = $attributes['rgbBackgroundColor'];
1029          }
1030  
1031          unset( $attributes['rgbTextColor'], $attributes['rgbBackgroundColor'] );
1032  
1033          $inner_blocks = static::get_inner_blocks( $attributes, $block );
1034          // Prevent navigation blocks referencing themselves from rendering.
1035          if ( block_core_navigation_block_tree_has_block_type(
1036              $inner_blocks,
1037              'core/navigation'
1038          ) ) {
1039              return '';
1040          }
1041  
1042          static::handle_view_script_module_loading( $attributes, $block, $inner_blocks );
1043  
1044          // Use div wrapper if this navigation block is within an overlay template part.
1045          $is_within_overlay = $attributes['_isWithinOverlayTemplatePart'] ?? false;
1046          $tag_name          = $is_within_overlay ? 'div' : 'nav';
1047  
1048          return sprintf(
1049              '<%1$s %2$s>%3$s</%1$s>',
1050              $tag_name,
1051              static::get_nav_attributes( $attributes, $inner_blocks ),
1052              static::get_inner_block_markup( $attributes, $inner_blocks )
1053          );
1054      }
1055  }
1056  
1057  // These functions are used for the __unstableLocation feature and only active
1058  // when the gutenberg plugin is active.
1059  if ( defined( 'IS_GUTENBERG_PLUGIN' ) && IS_GUTENBERG_PLUGIN ) {
1060      /**
1061       * Returns the menu items for a WordPress menu location.
1062       *
1063       * @since 5.9.0
1064       *
1065       * @param string $location The menu location.
1066       * @return array Menu items for the location.
1067       */
1068      function block_core_navigation_get_menu_items_at_location( $location ) {
1069          if ( empty( $location ) ) {
1070              return;
1071          }
1072  
1073          // Build menu data. The following approximates the code in
1074          // `wp_nav_menu()` and `gutenberg_output_block_nav_menu`.
1075  
1076          // Find the location in the list of locations, returning early if the
1077          // location can't be found.
1078          $locations = get_nav_menu_locations();
1079          if ( ! isset( $locations[ $location ] ) ) {
1080              return;
1081          }
1082  
1083          // Get the menu from the location, returning early if there is no
1084          // menu or there was an error.
1085          $menu = wp_get_nav_menu_object( $locations[ $location ] );
1086          if ( ! $menu || is_wp_error( $menu ) ) {
1087              return;
1088          }
1089  
1090          $menu_items = wp_get_nav_menu_items( $menu->term_id, array( 'update_post_term_cache' => false ) );
1091          _wp_menu_item_classes_by_context( $menu_items );
1092  
1093          return $menu_items;
1094      }
1095  
1096  
1097      /**
1098       * Sorts a standard array of menu items into a nested structure keyed by the
1099       * id of the parent menu.
1100       *
1101       * @since 5.9.0
1102       *
1103       * @param array $menu_items Menu items to sort.
1104       * @return array An array keyed by the id of the parent menu where each element
1105       *               is an array of menu items that belong to that parent.
1106       */
1107      function block_core_navigation_sort_menu_items_by_parent_id( $menu_items ) {
1108          $sorted_menu_items = array();
1109          foreach ( (array) $menu_items as $menu_item ) {
1110              $sorted_menu_items[ $menu_item->menu_order ] = $menu_item;
1111          }
1112          unset( $menu_items, $menu_item );
1113  
1114          $menu_items_by_parent_id = array();
1115          foreach ( $sorted_menu_items as $menu_item ) {
1116              $menu_items_by_parent_id[ $menu_item->menu_item_parent ][] = $menu_item;
1117          }
1118  
1119          return $menu_items_by_parent_id;
1120      }
1121  
1122      /**
1123       * Gets the inner blocks for the navigation block from the unstable location attribute.
1124       *
1125       * @since 6.5.0
1126       *
1127       * @param array $attributes The block attributes.
1128       * @return WP_Block_List Returns the inner blocks for the navigation block.
1129       */
1130      function block_core_navigation_get_inner_blocks_from_unstable_location( $attributes ) {
1131          $menu_items = block_core_navigation_get_menu_items_at_location( $attributes['__unstableLocation'] );
1132          if ( empty( $menu_items ) ) {
1133              return new WP_Block_List( array(), $attributes );
1134          }
1135  
1136          $menu_items_by_parent_id = block_core_navigation_sort_menu_items_by_parent_id( $menu_items );
1137          $parsed_blocks           = block_core_navigation_parse_blocks_from_menu_items( $menu_items_by_parent_id[0], $menu_items_by_parent_id );
1138          return new WP_Block_List( $parsed_blocks, $attributes );
1139      }
1140  }
1141  
1142  /**
1143   * Checks if the overlay HTML contains a navigation-overlay-close block.
1144   *
1145   * Uses WP_HTML_Tag_Processor to detect the close button in rendered output,
1146   * so it works when the overlay uses patterns (pattern content is rendered at
1147   * output time, not in the block tree).
1148   *
1149   * @since 7.0.0
1150   *
1151   * @param string $html The rendered overlay HTML.
1152   * @return bool True if a close button element is found.
1153   */
1154  function block_core_navigation_overlay_html_has_close_block( $html ) {
1155      $tags = new WP_HTML_Tag_Processor( $html );
1156      return $tags->next_tag(
1157          array(
1158              'tag_name'   => 'BUTTON',
1159              'class_name' => 'wp-block-navigation-overlay-close',
1160          )
1161      );
1162  }
1163  
1164  /**
1165   * Add Interactivity API directives to the navigation-overlay-close block
1166   * markup using the Tag Processor.
1167   *
1168   * @since 6.5.0
1169   *
1170   * @param WP_HTML_Tag_Processor $tags Markup of the navigation block.
1171   * @return string Overlay close markup with the directives injected.
1172   */
1173  function block_core_navigation_add_directives_to_overlay_close( $tags ) {
1174      // Find all navigation-overlay-close buttons.
1175      while ( $tags->next_tag(
1176          array(
1177              'tag_name'   => 'BUTTON',
1178              'class_name' => 'wp-block-navigation-overlay-close',
1179          )
1180      ) ) {
1181          // Add the same close directive as the default close button.
1182          $tags->set_attribute( 'data-wp-on--click', 'actions.closeMenuOnClick' );
1183      }
1184      return $tags->get_updated_html();
1185  }
1186  
1187  /**
1188   * Sets fetchpriority="low" on all IMG tags within the navigation overlay.
1189   *
1190   * Images in the overlay are hidden until the menu is opened, so they should
1191   * not compete with any actual LCP element image on the page.
1192   *
1193   * @since 7.0.0
1194   *
1195   * @param string $overlay_blocks_html The rendered HTML of the overlay blocks.
1196   * @return string Modified HTML with fetchpriority="low" on all IMG tags.
1197   */
1198  function block_core_navigation_set_overlay_image_fetch_priority( string $overlay_blocks_html ): string {
1199      $tags = new WP_HTML_Tag_Processor( $overlay_blocks_html );
1200      while ( $tags->next_tag( 'IMG' ) ) {
1201          $tags->set_attribute( 'fetchpriority', 'low' );
1202      }
1203      return $tags->get_updated_html();
1204  }
1205  
1206  /**
1207   * Add Interactivity API directives to the navigation-submenu and page-list
1208   * blocks markup using the Tag Processor.
1209   *
1210   * @since 6.3.0
1211   *
1212   * @param WP_HTML_Tag_Processor $tags             Markup of the navigation block.
1213   * @param array                 $block_attributes Block attributes.
1214   *
1215   * @return string Submenu markup with the directives injected.
1216   */
1217  function block_core_navigation_add_directives_to_submenu( $tags, $block_attributes ) {
1218      while ( $tags->next_tag(
1219          array(
1220              'tag_name'   => 'LI',
1221              'class_name' => 'has-child',
1222          )
1223      ) ) {
1224          // Add directives to the parent `<li>`.
1225          $tags->set_attribute( 'data-wp-interactive', 'core/navigation' );
1226          $tags->set_attribute( 'data-wp-context', '{ "submenuOpenedBy": { "click": false, "hover": false, "focus": false }, "type": "submenu", "modal": null, "previousFocus": null }' );
1227          $tags->set_attribute( 'data-wp-watch', 'callbacks.initMenu' );
1228          $tags->set_attribute( 'data-wp-on--focusout', 'actions.handleMenuFocusout' );
1229          $tags->set_attribute( 'data-wp-on--keydown', 'actions.handleMenuKeydown' );
1230  
1231          // This is a fix for Safari. Without it, Safari doesn't change the active
1232          // element when the user clicks on a button. It can be removed once we add
1233          // an overlay to capture the clicks, instead of relying on the focusout
1234          // event.
1235          $tags->set_attribute( 'tabindex', '-1' );
1236  
1237          $computed_visibility = block_core_navigation_get_submenu_visibility( $block_attributes );
1238          $open_on_hover       = 'hover' === $computed_visibility;
1239  
1240          if ( $open_on_hover ) {
1241              $tags->set_attribute( 'data-wp-on--pointerenter', 'actions.openMenuOnHover' );
1242              $tags->set_attribute( 'data-wp-on--pointerleave', 'actions.closeMenuOnHover' );
1243          }
1244  
1245          // Add directives to the toggle submenu button.
1246          if ( $tags->next_tag(
1247              array(
1248                  'tag_name'   => 'BUTTON',
1249                  'class_name' => 'wp-block-navigation-submenu__toggle',
1250              )
1251          ) ) {
1252              $tags->set_attribute( 'data-wp-on--click', 'actions.toggleMenuOnClick' );
1253              $tags->set_attribute( 'data-wp-bind--aria-expanded', 'state.isSubmenuOpen' );
1254              // The `aria-expanded` attribute for SSR is already added in the submenu block.
1255          }
1256          // Add directives to the submenu.
1257          if ( $tags->next_tag(
1258              array(
1259                  'tag_name'   => 'UL',
1260                  'class_name' => 'wp-block-navigation__submenu-container',
1261              )
1262          ) ) {
1263              $tags->set_attribute( 'data-wp-on--focus', 'actions.openMenuOnFocus' );
1264          }
1265  
1266          // Iterate through subitems if exist.
1267          block_core_navigation_add_directives_to_submenu( $tags, $block_attributes );
1268      }
1269      return $tags->get_updated_html();
1270  }
1271  
1272  /**
1273   * Build an array with CSS classes and inline styles defining the colors
1274   * which will be applied to the navigation markup in the front-end.
1275   *
1276   * @since 5.9.0
1277   *
1278   * @param array $attributes Navigation block attributes.
1279   *
1280   * @return array Colors CSS classes and inline styles.
1281   */
1282  function block_core_navigation_build_css_colors( $attributes ) {
1283      $colors = array(
1284          'css_classes'           => array(),
1285          'inline_styles'         => '',
1286          'overlay_css_classes'   => array(),
1287          'overlay_inline_styles' => '',
1288      );
1289  
1290      // Text color.
1291      $has_named_text_color  = array_key_exists( 'textColor', $attributes );
1292      $has_custom_text_color = array_key_exists( 'customTextColor', $attributes );
1293  
1294      // If has text color.
1295      if ( $has_custom_text_color || $has_named_text_color ) {
1296          // Add has-text-color class.
1297          $colors['css_classes'][] = 'has-text-color';
1298      }
1299  
1300      if ( $has_named_text_color ) {
1301          // Add the color class.
1302          $colors['css_classes'][] = sprintf( 'has-%s-color', $attributes['textColor'] );
1303      } elseif ( $has_custom_text_color ) {
1304          // Add the custom color inline style.
1305          $colors['inline_styles'] .= sprintf( 'color: %s;', $attributes['customTextColor'] );
1306      }
1307  
1308      // Background color.
1309      $has_named_background_color  = array_key_exists( 'backgroundColor', $attributes );
1310      $has_custom_background_color = array_key_exists( 'customBackgroundColor', $attributes );
1311  
1312      // If has background color.
1313      if ( $has_custom_background_color || $has_named_background_color ) {
1314          // Add has-background class.
1315          $colors['css_classes'][] = 'has-background';
1316      }
1317  
1318      if ( $has_named_background_color ) {
1319          // Add the background-color class.
1320          $colors['css_classes'][] = sprintf( 'has-%s-background-color', $attributes['backgroundColor'] );
1321      } elseif ( $has_custom_background_color ) {
1322          // Add the custom background-color inline style.
1323          $colors['inline_styles'] .= sprintf( 'background-color: %s;', $attributes['customBackgroundColor'] );
1324      }
1325  
1326      // Overlay text color.
1327      $has_named_overlay_text_color  = array_key_exists( 'overlayTextColor', $attributes );
1328      $has_custom_overlay_text_color = array_key_exists( 'customOverlayTextColor', $attributes );
1329  
1330      // If has overlay text color.
1331      if ( $has_custom_overlay_text_color || $has_named_overlay_text_color ) {
1332          // Add has-text-color class.
1333          $colors['overlay_css_classes'][] = 'has-text-color';
1334      }
1335  
1336      if ( $has_named_overlay_text_color ) {
1337          // Add the overlay color class.
1338          $colors['overlay_css_classes'][] = sprintf( 'has-%s-color', $attributes['overlayTextColor'] );
1339      } elseif ( $has_custom_overlay_text_color ) {
1340          // Add the custom overlay color inline style.
1341          $colors['overlay_inline_styles'] .= sprintf( 'color: %s;', $attributes['customOverlayTextColor'] );
1342      }
1343  
1344      // Overlay background color.
1345      $has_named_overlay_background_color  = array_key_exists( 'overlayBackgroundColor', $attributes );
1346      $has_custom_overlay_background_color = array_key_exists( 'customOverlayBackgroundColor', $attributes );
1347  
1348      // If has overlay background color.
1349      if ( $has_custom_overlay_background_color || $has_named_overlay_background_color ) {
1350          // Add has-background class.
1351          $colors['overlay_css_classes'][] = 'has-background';
1352      }
1353  
1354      if ( $has_named_overlay_background_color ) {
1355          // Add the overlay background-color class.
1356          $colors['overlay_css_classes'][] = sprintf( 'has-%s-background-color', $attributes['overlayBackgroundColor'] );
1357      } elseif ( $has_custom_overlay_background_color ) {
1358          // Add the custom overlay background-color inline style.
1359          $colors['overlay_inline_styles'] .= sprintf( 'background-color: %s;', $attributes['customOverlayBackgroundColor'] );
1360      }
1361  
1362      return $colors;
1363  }
1364  
1365  /**
1366   * Build an array with CSS classes and inline styles defining the font sizes
1367   * which will be applied to the navigation markup in the front-end.
1368   *
1369   * @since 5.9.0
1370   *
1371   * @param array $attributes Navigation block attributes.
1372   *
1373   * @return array Font size CSS classes and inline styles.
1374   */
1375  function block_core_navigation_build_css_font_sizes( $attributes ) {
1376      // CSS classes.
1377      $font_sizes = array(
1378          'css_classes'   => array(),
1379          'inline_styles' => '',
1380      );
1381  
1382      $has_named_font_size  = array_key_exists( 'fontSize', $attributes );
1383      $has_custom_font_size = array_key_exists( 'customFontSize', $attributes );
1384  
1385      if ( $has_named_font_size ) {
1386          // Add the font size class.
1387          $font_sizes['css_classes'][] = sprintf( 'has-%s-font-size', $attributes['fontSize'] );
1388      } elseif ( $has_custom_font_size ) {
1389          // Add the custom font size inline style.
1390          $font_sizes['inline_styles'] = sprintf( 'font-size: %spx;', $attributes['customFontSize'] );
1391      }
1392  
1393      return $font_sizes;
1394  }
1395  
1396  /**
1397   * Filter out empty "null" blocks from the block list.
1398   * 'parse_blocks' includes a null block with '\n\n' as the content when
1399   * it encounters whitespace. This is not a bug but rather how the parser
1400   * is designed.
1401   *
1402   * @since 5.9.0
1403   *
1404   * @param array $parsed_blocks the parsed blocks to be normalized.
1405   * @return array the normalized parsed blocks.
1406   */
1407  function block_core_navigation_filter_out_empty_blocks( $parsed_blocks ) {
1408      $filtered = array_filter(
1409          $parsed_blocks,
1410          static function ( $block ) {
1411              return isset( $block['blockName'] );
1412          }
1413      );
1414  
1415      // Reset keys.
1416      return array_values( $filtered );
1417  }
1418  
1419  /**
1420   * Recursively checks if blocks contain a specific block type.
1421   *
1422   * @since 7.0.0
1423   *
1424   * @param WP_Block_List $blocks           The list of blocks to check.
1425   * @param string        $block_type       The block type to search for (e.g., 'core/navigation').
1426   * @param array         $skip_block_types Optional. Block types to skip when recursing. Default empty array.
1427   * @return bool Returns true if the specified block type is found.
1428   */
1429  function block_core_navigation_block_tree_has_block_type( $blocks, $block_type, $skip_block_types = array() ) {
1430      if ( empty( $blocks ) ) {
1431          return false;
1432      }
1433  
1434      foreach ( $blocks as $block ) {
1435          if ( $block_type === $block->name ) {
1436              return true;
1437          }
1438  
1439          // Recursively check inner blocks, skipping specified block types.
1440          if ( ! in_array( $block->name, $skip_block_types, true ) && ! empty( $block->inner_blocks ) ) {
1441              if ( block_core_navigation_block_tree_has_block_type( $block->inner_blocks, $block_type, $skip_block_types ) ) {
1442                  return true;
1443              }
1444          }
1445      }
1446  
1447      return false;
1448  }
1449  
1450  /**
1451   * Returns true if the navigation block contains a nested navigation block.
1452   *
1453   * @since 6.2.0
1454   * @deprecated 7.0.0 Use block_core_navigation_block_tree_has_block_type() instead.
1455   *
1456   * @param WP_Block_List $inner_blocks Inner block instance to be normalized.
1457   * @return bool true if the navigation block contains a nested navigation block.
1458   */
1459  function block_core_navigation_block_contains_core_navigation( $inner_blocks ) {
1460      _deprecated_function( __FUNCTION__, '7.0.0', 'block_core_navigation_block_tree_has_block_type()' );
1461  
1462      return block_core_navigation_block_tree_has_block_type(
1463          $inner_blocks,
1464          'core/navigation'
1465      );
1466  }
1467  
1468  /**
1469   * Retrieves the appropriate fallback to be used on the front of the
1470   * site when there is no menu assigned to the Nav block.
1471   *
1472   * This aims to mirror how the fallback mechanic for wp_nav_menu works.
1473   * See https://developer.wordpress.org/reference/functions/wp_nav_menu/#more-information.
1474   *
1475   * @since 5.9.0
1476   *
1477   * @return array the array of blocks to be used as a fallback.
1478   */
1479  function block_core_navigation_get_fallback_blocks() {
1480      $page_list_fallback = array(
1481          array(
1482              'blockName'    => 'core/page-list',
1483              'innerContent' => array(),
1484              'attrs'        => array(),
1485          ),
1486      );
1487  
1488      $registry = WP_Block_Type_Registry::get_instance();
1489  
1490      // If `core/page-list` is not registered then return empty blocks.
1491      $fallback_blocks = $registry->is_registered( 'core/page-list' ) ? $page_list_fallback : array();
1492      $navigation_post = WP_Navigation_Fallback::get_fallback();
1493  
1494      // Use the first non-empty Navigation as fallback if available.
1495      if ( $navigation_post ) {
1496          $parsed_blocks  = parse_blocks( $navigation_post->post_content );
1497          $maybe_fallback = block_core_navigation_filter_out_empty_blocks( $parsed_blocks );
1498  
1499          // Normalizing blocks may result in an empty array of blocks if they were all `null` blocks.
1500          // In this case default to the (Page List) fallback.
1501          $fallback_blocks = ! empty( $maybe_fallback ) ? $maybe_fallback : $fallback_blocks;
1502  
1503          // Run Block Hooks algorithm to inject hooked blocks.
1504          // We have to run it here because we need the post ID of the Navigation block to track ignored hooked blocks.
1505          // TODO: See if we can move the apply_block_hooks_to_content_from_post_object() call
1506          // before the parse_blocks() call further above, to avoid the extra serialization/parsing.
1507          $markup          = serialize_blocks( $fallback_blocks );
1508          $markup          = apply_block_hooks_to_content_from_post_object( $markup, $navigation_post );
1509          $fallback_blocks = parse_blocks( $markup );
1510      }
1511  
1512      /**
1513       * Filters the fallback experience for the Navigation block.
1514       *
1515       * Returning a falsey value will opt out of the fallback and cause the block not to render.
1516       * To customise the blocks provided return an array of blocks - these should be valid
1517       * children of the `core/navigation` block.
1518       *
1519       * @since 5.9.0
1520       *
1521       * @param array[] $fallback_blocks default fallback blocks provided by the default block mechanic.
1522       */
1523      return apply_filters( 'block_core_navigation_render_fallback', $fallback_blocks );
1524  }
1525  
1526  /**
1527   * Iterate through all inner blocks recursively and get navigation link block's post IDs.
1528   *
1529   * @since 6.0.0
1530   *
1531   * @param WP_Block_List $inner_blocks Block list class instance.
1532   *
1533   * @return array Array of post IDs.
1534   */
1535  function block_core_navigation_get_post_ids( $inner_blocks ) {
1536      $post_ids = array_map( 'block_core_navigation_from_block_get_post_ids', iterator_to_array( $inner_blocks ) );
1537      return array_unique( array_merge( ...$post_ids ) );
1538  }
1539  
1540  /**
1541   * Get post IDs from a navigation link block instance.
1542   *
1543   * @since 6.0.0
1544   *
1545   * @param WP_Block $block Instance of a block.
1546   *
1547   * @return array Array of post IDs.
1548   */
1549  function block_core_navigation_from_block_get_post_ids( $block ) {
1550      $post_ids = array();
1551  
1552      if ( $block->inner_blocks ) {
1553          $post_ids = block_core_navigation_get_post_ids( $block->inner_blocks );
1554      }
1555  
1556      if ( 'core/navigation-link' === $block->name || 'core/navigation-submenu' === $block->name ) {
1557          if ( $block->attributes && isset( $block->attributes['kind'] ) && 'post-type' === $block->attributes['kind'] && isset( $block->attributes['id'] ) ) {
1558              $post_ids[] = $block->attributes['id'];
1559          }
1560      }
1561  
1562      return $post_ids;
1563  }
1564  
1565  /**
1566   * Renders the `core/navigation` block on server.
1567   *
1568   * @since 5.9.0
1569   *
1570   * @param array    $attributes The block attributes.
1571   * @param string   $content    The saved content.
1572   * @param WP_Block $block      The parsed block.
1573   *
1574   * @return string Returns the navigation block markup.
1575   */
1576  function render_block_core_navigation( $attributes, $content, $block ) {
1577      return WP_Navigation_Block_Renderer::render( $attributes, $content, $block );
1578  }
1579  
1580  /**
1581   * Register the navigation block.
1582   *
1583   * @since 5.9.0
1584   *
1585   * @uses render_block_core_navigation()
1586   * @throws WP_Error An WP_Error exception parsing the block definition.
1587   */
1588  function register_block_core_navigation() {
1589      register_block_type_from_metadata(
1590          __DIR__ . '/navigation',
1591          array(
1592              'render_callback' => 'render_block_core_navigation',
1593          )
1594      );
1595  }
1596  
1597  add_action( 'init', 'register_block_core_navigation' );
1598  
1599  /**
1600   * Adds Navigation block support classes to inner list containers.
1601   *
1602   * State block support adds the generated `wp-states-*` class to the outer
1603   * block wrapper. The Navigation block renders its menu items inside an inner
1604   * `wp-block-navigation__container` list, so the same state class is also needed
1605   * there for state styles to apply directly to the menu list.
1606   *
1607   * Navigation also uses layout classes on its outer wrapper to define custom
1608   * properties consumed by its inner containers. Viewport layout styles cannot
1609   * change those classes, so equivalent custom properties and a scoping class
1610   * are generated for each configured viewport layout.
1611   *
1612   * Currently this is required as a workaround because of how difficult it is for nav
1613   * child blocks to inherit styles through the complex responsive nav block html. The
1614   * bug in https://github.com/WordPress/gutenberg/issues/62690 also prevents inheritance.
1615   *
1616   * @since 7.1.0
1617   *
1618   * @param string $block_content The block content.
1619   * @param array  $block         The full block, including name and attributes.
1620   * @return string The updated block content.
1621   */
1622  function block_core_navigation_add_support_classes_to_container( $block_content, $block ) {
1623      if ( 'core/navigation' !== ( $block['blockName'] ?? null ) || empty( $block_content ) ) {
1624          return $block_content;
1625      }
1626  
1627      $attributes = is_array( $block['attrs'] ?? null ) ? $block['attrs'] : array();
1628      $style      = is_array( $attributes['style'] ?? null ) ? $attributes['style'] : array();
1629      if (
1630          defined( 'IS_GUTENBERG_PLUGIN' ) &&
1631          IS_GUTENBERG_PLUGIN &&
1632          function_exists( 'gutenberg_resolve_style_state_aliases' )
1633      ) {
1634          $style = gutenberg_resolve_style_state_aliases( $style, 'core/navigation' );
1635      }
1636  
1637      $global_settings          = wp_get_global_settings();
1638      $viewport_settings        = $global_settings['viewport'] ?? null;
1639      $responsive_media_queries = array();
1640      if ( method_exists( 'WP_Theme_JSON_Gutenberg', 'get_viewport_media_queries' ) ) {
1641          $responsive_media_queries = WP_Theme_JSON_Gutenberg::get_viewport_media_queries( $viewport_settings );
1642      } elseif ( method_exists( 'WP_Theme_JSON', 'get_viewport_media_queries' ) ) {
1643          $responsive_media_queries = WP_Theme_JSON::get_viewport_media_queries( $viewport_settings );
1644      }
1645  
1646      $styles      = array();
1647      $base_layout = is_array( $attributes['layout'] ?? null ) ? $attributes['layout'] : array();
1648      foreach ( $responsive_media_queries as $breakpoint => $media_query ) {
1649          $viewport_style  = is_array( $style[ $breakpoint ] ?? null ) ? $style[ $breakpoint ] : array();
1650          $viewport_layout = is_array( $viewport_style['layout'] ?? null ) ? $viewport_style['layout'] : array();
1651          if ( empty( $viewport_layout ) ) {
1652              continue;
1653          }
1654  
1655          $styles[] = array(
1656              'declarations' => block_core_navigation_get_layout_custom_property_declarations(
1657                  array_replace( $base_layout, $viewport_layout )
1658              ),
1659              'rules_group'  => $media_query,
1660          );
1661      }
1662  
1663      $processor = new WP_HTML_Tag_Processor( $block_content );
1664      if ( ! $processor->next_tag() ) {
1665          return $block_content;
1666      }
1667  
1668      $class_attribute = $processor->get_attribute( 'class' );
1669      $state_class     = null;
1670      if ( is_string( $class_attribute ) && preg_match( '/\bwp-states-[a-f0-9]{8}\b/', $class_attribute, $matches ) ) {
1671          $state_class = $matches[0];
1672      }
1673  
1674      $layout_class = null;
1675      if ( ! empty( $styles ) ) {
1676          $layout_class = wp_unique_id( 'wp-block-navigation-' );
1677          // The inner selector includes both Navigation classes so it overrides the
1678          // default layout custom properties set by `.wp-block-navigation.items-*`.
1679          $selector = ".wp-block-navigation.{$layout_class},.wp-block-navigation.wp-block-navigation__container.{$layout_class}";
1680          foreach ( $styles as &$style_rule ) {
1681              $style_rule['selector'] = $selector;
1682          }
1683          unset( $style_rule );
1684  
1685          $processor->add_class( $layout_class );
1686          wp_style_engine_get_stylesheet_from_css_rules(
1687              $styles,
1688              array( 'context' => 'block-supports' )
1689          );
1690      }
1691  
1692      if ( null === $state_class && null === $layout_class ) {
1693          return $block_content;
1694      }
1695  
1696      while ( $processor->next_tag() ) {
1697          // Custom overlay content can include nested Navigation blocks.
1698          // Avoid applying the outer Navigation classes to an inner nav block.
1699          if ( $processor->has_class( 'wp-block-navigation' ) && ! $processor->has_class( 'wp-block-navigation__container' ) ) {
1700              break;
1701          }
1702  
1703          if ( ! $processor->has_class( 'wp-block-navigation__container' ) ) {
1704              continue;
1705          }
1706  
1707          if ( null !== $layout_class ) {
1708              $processor->add_class( $layout_class );
1709          }
1710  
1711          if ( null === $state_class ) {
1712              continue;
1713          }
1714  
1715          $class_attribute = $processor->get_attribute( 'class' );
1716          if ( is_string( $class_attribute ) && preg_match( '/\bwp-states-[a-f0-9]{8}\b/', $class_attribute ) ) {
1717              continue;
1718          }
1719  
1720          $processor->add_class( $state_class );
1721      }
1722  
1723      return $processor->get_updated_html();
1724  }
1725  
1726  add_filter( 'render_block', 'block_core_navigation_add_support_classes_to_container', 11, 2 );
1727  
1728  /**
1729   * Filter that changes the parsed attribute values of navigation blocks contain typographic presets to contain the values directly.
1730   *
1731   * @since 5.9.0
1732   *
1733   * @param array $parsed_block The block being rendered.
1734   *
1735   * @return array The block being rendered without typographic presets.
1736   */
1737  function block_core_navigation_typographic_presets_backcompatibility( $parsed_block ) {
1738      if ( 'core/navigation' === $parsed_block['blockName'] ) {
1739          $attribute_to_prefix_map = array(
1740              'fontStyle'      => 'var:preset|font-style|',
1741              'fontWeight'     => 'var:preset|font-weight|',
1742              'textDecoration' => 'var:preset|text-decoration|',
1743              'textTransform'  => 'var:preset|text-transform|',
1744          );
1745          foreach ( $attribute_to_prefix_map as $style_attribute => $prefix ) {
1746              if ( ! empty( $parsed_block['attrs']['style']['typography'][ $style_attribute ] ) ) {
1747                  $prefix_len      = strlen( $prefix );
1748                  $attribute_value = &$parsed_block['attrs']['style']['typography'][ $style_attribute ];
1749                  if ( 0 === strncmp( $attribute_value, $prefix, $prefix_len ) ) {
1750                      $attribute_value = substr( $attribute_value, $prefix_len );
1751                  }
1752                  if ( 'textDecoration' === $style_attribute && 'strikethrough' === $attribute_value ) {
1753                      $attribute_value = 'line-through';
1754                  }
1755              }
1756          }
1757      }
1758  
1759      return $parsed_block;
1760  }
1761  
1762  add_filter( 'render_block_data', 'block_core_navigation_typographic_presets_backcompatibility' );
1763  
1764  /**
1765   * Turns menu item data into a nested array of parsed blocks
1766   *
1767   * @since 5.9.0
1768   *
1769   * @deprecated 6.3.0 Use WP_Navigation_Fallback::parse_blocks_from_menu_items() instead.
1770   *
1771   * @param array $menu_items               An array of menu items that represent
1772   *                                        an individual level of a menu.
1773   * @param array $menu_items_by_parent_id  An array keyed by the id of the
1774   *                                        parent menu where each element is an
1775   *                                        array of menu items that belong to
1776   *                                        that parent.
1777   * @return array An array of parsed block data.
1778   */
1779  function block_core_navigation_parse_blocks_from_menu_items( $menu_items, $menu_items_by_parent_id ) {
1780  
1781      _deprecated_function( __FUNCTION__, '6.3.0', 'WP_Navigation_Fallback::parse_blocks_from_menu_items' );
1782  
1783      if ( empty( $menu_items ) ) {
1784          return array();
1785      }
1786  
1787      $blocks = array();
1788  
1789      foreach ( $menu_items as $menu_item ) {
1790          $class_name       = ! empty( $menu_item->classes ) ? implode( ' ', (array) $menu_item->classes ) : null;
1791          $id               = ( null !== $menu_item->object_id && 'custom' !== $menu_item->object ) ? $menu_item->object_id : null;
1792          $opens_in_new_tab = null !== $menu_item->target && '_blank' === $menu_item->target;
1793          $rel              = ( null !== $menu_item->xfn && '' !== $menu_item->xfn ) ? $menu_item->xfn : null;
1794          $kind             = null !== $menu_item->type ? str_replace( '_', '-', $menu_item->type ) : 'custom';
1795  
1796          $block = array(
1797              'blockName' => isset( $menu_items_by_parent_id[ $menu_item->ID ] ) ? 'core/navigation-submenu' : 'core/navigation-link',
1798              'attrs'     => array(
1799                  'className'     => $class_name,
1800                  'description'   => $menu_item->description,
1801                  'id'            => $id,
1802                  'kind'          => $kind,
1803                  'label'         => $menu_item->title,
1804                  'opensInNewTab' => $opens_in_new_tab,
1805                  'rel'           => $rel,
1806                  'title'         => $menu_item->attr_title,
1807                  'type'          => $menu_item->object,
1808                  'url'           => $menu_item->url,
1809              ),
1810          );
1811  
1812          $block['innerBlocks']  = isset( $menu_items_by_parent_id[ $menu_item->ID ] )
1813              ? block_core_navigation_parse_blocks_from_menu_items( $menu_items_by_parent_id[ $menu_item->ID ], $menu_items_by_parent_id )
1814              : array();
1815          $block['innerContent'] = array_map( 'serialize_block', $block['innerBlocks'] );
1816  
1817          $blocks[] = $block;
1818      }
1819  
1820      return $blocks;
1821  }
1822  
1823  /**
1824   * Get the classic navigation menu to use as a fallback.
1825   *
1826   * @since 6.2.0
1827   *
1828   * @deprecated 6.3.0 Use WP_Navigation_Fallback::get_classic_menu_fallback() instead.
1829   *
1830   * @return object WP_Term The classic navigation.
1831   */
1832  function block_core_navigation_get_classic_menu_fallback() {
1833  
1834      _deprecated_function( __FUNCTION__, '6.3.0', 'WP_Navigation_Fallback::get_classic_menu_fallback' );
1835  
1836      $classic_nav_menus = wp_get_nav_menus();
1837  
1838      // If menus exist.
1839      if ( $classic_nav_menus && ! is_wp_error( $classic_nav_menus ) ) {
1840          // Handles simple use case where user has a classic menu and switches to a block theme.
1841  
1842          // Returns the menu assigned to location `primary`.
1843          $locations = get_nav_menu_locations();
1844          if ( isset( $locations['primary'] ) ) {
1845              $primary_menu = wp_get_nav_menu_object( $locations['primary'] );
1846              if ( $primary_menu ) {
1847                  return $primary_menu;
1848              }
1849          }
1850  
1851          // Returns a menu if `primary` is its slug.
1852          foreach ( $classic_nav_menus as $classic_nav_menu ) {
1853              if ( 'primary' === $classic_nav_menu->slug ) {
1854                  return $classic_nav_menu;
1855              }
1856          }
1857  
1858          // Otherwise return the most recently created classic menu.
1859          usort(
1860              $classic_nav_menus,
1861              static function ( $a, $b ) {
1862                  return $b->term_id - $a->term_id;
1863              }
1864          );
1865          return $classic_nav_menus[0];
1866      }
1867  }
1868  
1869  /**
1870   * Converts a classic navigation to blocks.
1871   *
1872   * @since 6.2.0
1873   *
1874   * @deprecated 6.3.0 Use WP_Navigation_Fallback::get_classic_menu_fallback_blocks() instead.
1875   *
1876   * @param  object $classic_nav_menu WP_Term The classic navigation object to convert.
1877   * @return array the normalized parsed blocks.
1878   */
1879  function block_core_navigation_get_classic_menu_fallback_blocks( $classic_nav_menu ) {
1880  
1881      _deprecated_function( __FUNCTION__, '6.3.0', 'WP_Navigation_Fallback::get_classic_menu_fallback_blocks' );
1882  
1883      // BEGIN: Code that already exists in wp_nav_menu().
1884      $menu_items = wp_get_nav_menu_items( $classic_nav_menu->term_id, array( 'update_post_term_cache' => false ) );
1885  
1886      // Set up the $menu_item variables.
1887      _wp_menu_item_classes_by_context( $menu_items );
1888  
1889      $sorted_menu_items = array();
1890      foreach ( (array) $menu_items as $menu_item ) {
1891          $sorted_menu_items[ $menu_item->menu_order ] = $menu_item;
1892      }
1893  
1894      unset( $menu_items, $menu_item );
1895  
1896      // END: Code that already exists in wp_nav_menu().
1897  
1898      $menu_items_by_parent_id = array();
1899      foreach ( $sorted_menu_items as $menu_item ) {
1900          $menu_items_by_parent_id[ $menu_item->menu_item_parent ][] = $menu_item;
1901      }
1902  
1903      $inner_blocks = block_core_navigation_parse_blocks_from_menu_items(
1904          $menu_items_by_parent_id[0] ?? array(),
1905          $menu_items_by_parent_id
1906      );
1907  
1908      return serialize_blocks( $inner_blocks );
1909  }
1910  
1911  /**
1912   * If there's a classic menu then use it as a fallback.
1913   *
1914   * @since 6.2.0
1915   *
1916   * @deprecated 6.3.0 Use WP_Navigation_Fallback::create_classic_menu_fallback() instead.
1917   *
1918   * @return array the normalized parsed blocks.
1919   */
1920  function block_core_navigation_maybe_use_classic_menu_fallback() {
1921  
1922      _deprecated_function( __FUNCTION__, '6.3.0', 'WP_Navigation_Fallback::create_classic_menu_fallback' );
1923  
1924      // See if we have a classic menu.
1925      $classic_nav_menu = block_core_navigation_get_classic_menu_fallback();
1926  
1927      if ( ! $classic_nav_menu ) {
1928          return;
1929      }
1930  
1931      // If we have a classic menu then convert it to blocks.
1932      $classic_nav_menu_blocks = block_core_navigation_get_classic_menu_fallback_blocks( $classic_nav_menu );
1933  
1934      if ( empty( $classic_nav_menu_blocks ) ) {
1935          return;
1936      }
1937  
1938      // Create a new navigation menu from the classic menu.
1939      $wp_insert_post_result = wp_insert_post(
1940          array(
1941              'post_content' => $classic_nav_menu_blocks,
1942              'post_title'   => $classic_nav_menu->name,
1943              'post_name'    => $classic_nav_menu->slug,
1944              'post_status'  => 'publish',
1945              'post_type'    => 'wp_navigation',
1946          ),
1947          true // So that we can check whether the result is an error.
1948      );
1949  
1950      if ( is_wp_error( $wp_insert_post_result ) ) {
1951          return;
1952      }
1953  
1954      // Fetch the most recently published navigation which will be the classic one created above.
1955      return block_core_navigation_get_most_recently_published_navigation();
1956  }
1957  
1958  /**
1959   * Finds the most recently published `wp_navigation` Post.
1960   *
1961   * @since 6.1.0
1962   *
1963   * @deprecated 6.3.0 Use WP_Navigation_Fallback::get_most_recently_published_navigation() instead.
1964   *
1965   * @return WP_Post|null the first non-empty Navigation or null.
1966   */
1967  function block_core_navigation_get_most_recently_published_navigation() {
1968  
1969      _deprecated_function( __FUNCTION__, '6.3.0', 'WP_Navigation_Fallback::get_most_recently_published_navigation' );
1970  
1971      // Default to the most recently created menu.
1972      $parsed_args = array(
1973          'post_type'              => 'wp_navigation',
1974          'no_found_rows'          => true,
1975          'update_post_meta_cache' => false,
1976          'update_post_term_cache' => false,
1977          'order'                  => 'DESC',
1978          'orderby'                => 'date',
1979          'post_status'            => 'publish',
1980          'posts_per_page'         => 1, // get only the most recent.
1981      );
1982  
1983      $navigation_post = new WP_Query( $parsed_args );
1984      if ( count( $navigation_post->posts ) > 0 ) {
1985          return $navigation_post->posts[0];
1986      }
1987  
1988      return null;
1989  }


Generated : Sat Aug 8 08:20:21 2026 Cross-referenced by PHPXref