&$block) { if ($found !== null) { return; } $name = $block['blockName'] ?? ''; if ($name === '') { if (!empty($block['innerBlocks'])) { $walk($block['innerBlocks'], array_merge($pathSoFar, [$i, 'innerBlocks'])); } continue; } if ($name === 'core/template-part') { continue; } $counter++; $visited[] = ['c' => $counter, 'n' => $name]; if ($counter === $targetId) { $found = ['block' => $block, 'path' => array_merge($pathSoFar, [$i])]; return; } // Dynamic self-rendering blocks (loops, woocommerce/mini-cart, …) // count as one leaf but are not descended into: TagTemplateParts // skips their render-injected subtree, so descending here would // drift every later id. Shares the one $ignored list. if (in_array($name, TagTemplateParts::$ignored, true)) { continue; } if ($name === 'core/navigation' && !empty($block['attrs']['ref'])) { $navPost = get_post((int) $block['attrs']['ref']); if ($navPost) { $navBlocks = parse_blocks($navPost->post_content); $before = $counter; self::countBlocksPreorder($navBlocks, $counter); for ($j = $before + 1; $j <= $counter; $j++) { $visited[] = ['c' => $j, 'n' => '(nav-ref-item)']; } } // Ref navs have no innerBlocks in the parsed tree — items // live in the wp_navigation post and can't be replaced from // here anyway (WPNavigationController owns that). continue; } if (!empty($block['innerBlocks'])) { $walk($block['innerBlocks'], array_merge($pathSoFar, [$i, 'innerBlocks'])); } } unset($block); }; $walk($blocks, []); $maxCounter = $counter; return $found; } private static function countBlocksPreorder(array $blocks, int &$counter) { foreach ($blocks as $block) { $name = $block['blockName'] ?? ''; if ($name === '') { if (!empty($block['innerBlocks'])) { self::countBlocksPreorder($block['innerBlocks'], $counter); } continue; } if ($name === 'core/template-part') { continue; } $counter++; if (in_array($name, TagTemplateParts::$ignored, true)) { continue; } if (!empty($block['innerBlocks'])) { self::countBlocksPreorder($block['innerBlocks'], $counter); } } } }