| @@ -48,8 +48,36 @@ | ||
| 48 | 48 | { |
| 49 | 49 | return (($b['blockName'] ?? '') === 'core/template-part'); |
| 50 | 50 | } |
| 51 | 51 | |
| 52 | + private static function isSyncedPattern(array $b): bool | |
| 53 | + { | |
| 54 | + return (($b['blockName'] ?? '') === 'core/block'); | |
| 55 | + } | |
| 56 | + | |
| 57 | + private static function inFrame(string $prefix): bool | |
| 58 | + { | |
| 59 | + return (self::currentFrame()['prefix'] ?? '') === $prefix; | |
| 60 | + } | |
| 61 | + | |
| 62 | + // A prefix opens an id space that belongs to another post; `nav` marks the | |
| 63 | + // one whose items are stamped from the finished html instead of counted. | |
| 64 | + private static function newFrame( | |
| 65 | + string $label, | |
| 66 | + string $slug, | |
| 67 | + string $prefix = '', | |
| 68 | + bool $nav = false | |
| 69 | + ): array { | |
| 70 | + return [ | |
| 71 | + 'label' => $label, | |
| 72 | + 'slug' => $slug, | |
| 73 | + 'prefix' => $prefix, | |
| 74 | + 'nav' => $nav, | |
| 75 | + 'seq' => 0, | |
| 76 | + 'skip_depth' => 0, | |
| 77 | + ]; | |
| 78 | + } | |
| 79 | + | |
| 52 | 80 | private static function currentFrame() |
| 53 | 81 | { |
| 54 | 82 | return self::$frames ? self::$frames[count(self::$frames) - 1] : null; |
| 55 | 83 | } |
| @@ -77,14 +105,12 @@ | ||
| 77 | 105 | return $block; |
| 78 | 106 | } |
| 79 | 107 | |
| 80 | 108 | if (self::isTemplatePart($block)) { |
| 81 | - self::$frames[] = [ | |
| 82 | - 'label' => self::labelForPart($block), | |
| 83 | - 'slug' => $block['attrs']['slug'] ?? '', | |
| 84 | - 'seq' => 0, | |
| 85 | - 'skip_depth' => 0, | |
| 86 | - ]; | |
| 109 | + self::$frames[] = self::newFrame( | |
| 110 | + self::labelForPart($block), | |
| 111 | + $block['attrs']['slug'] ?? '' | |
| 112 | + ); | |
| 87 | 113 | $block['attrs']['__extendify_scope_open'] = 1; |
| 88 | 114 | return $block; |
| 89 | 115 | } |
| 90 | 116 | |
| @@ -96,8 +122,25 @@ | ||
| 96 | 122 | if (!$frame) { |
| 97 | 123 | return $block; |
| 98 | 124 | } |
| 99 | 125 | |
| 126 | + // The pattern's blocks render inline; counting them here would inflate | |
| 127 | + // every later id in the part. | |
| 128 | + if (self::isSyncedPattern($block) && ($frame['skip_depth'] ?? 0) === 0) { | |
| 129 | + self::$frames[] = self::newFrame( | |
| 130 | + $frame['label'], | |
| 131 | + $frame['slug'] ?? '', | |
| 132 | + TemplatePartBlockFinder::refPrefix('block', $block) | |
| 133 | + ); | |
| 134 | + return $block; | |
| 135 | + } | |
| 136 | + | |
| 137 | + // A ref nav's items are numbered off its post once it has rendered, so | |
| 138 | + // nothing inside its frame is counted here. | |
| 139 | + if (($frame['nav'] ?? false)) { | |
| 140 | + return $block; | |
| 141 | + } | |
| 142 | + | |
| 100 | 143 | // render_block_data runs top-down (before a block's own render), so an |
| 101 | 144 | // ignored block is counted here as a leaf and the skip is raised *after* |
| 102 | 145 | // — its descendants then render with skip_depth > 0 and are not counted. |
| 103 | 146 | if (($frame['skip_depth'] ?? 0) === 0) { |
| @@ -103,9 +146,9 @@ | ||
| 103 | 146 | if (($frame['skip_depth'] ?? 0) === 0) { |
| 104 | 147 | $frame['seq']++; |
| 105 | 148 | self::$blockStack[] = [ |
| 106 | 149 | 'name' => $name, |
| 107 | - 'id' => $frame['seq'], | |
| 150 | + 'id' => ($frame['prefix'] ?? '') . $frame['seq'], | |
| 108 | 151 | 'label' => $frame['label'], |
| 109 | 152 | 'slug' => $frame['slug'] ?? '', |
| 110 | 153 | ]; |
| 111 | 154 | } |
| @@ -113,11 +156,61 @@ | ||
| 113 | 156 | $frame['skip_depth'] = ($frame['skip_depth'] ?? 0) + 1; |
| 114 | 157 | } |
| 115 | 158 | self::setCurrentFrame($frame); |
| 116 | 159 | |
| 160 | + if (TemplatePartBlockFinder::isRefNav($block)) { | |
| 161 | + self::$frames[] = self::newFrame( | |
| 162 | + $frame['label'], | |
| 163 | + $frame['slug'] ?? '', | |
| 164 | + TemplatePartBlockFinder::refPrefix('navigation', $block), | |
| 165 | + true | |
| 166 | + ); | |
| 167 | + } | |
| 168 | + | |
| 117 | 169 | return $block; |
| 118 | 170 | } |
| 119 | 171 | |
| 172 | + // A page-list renders pages that aren't blocks, so a menu holding one is | |
| 173 | + // left unstamped rather than wrongly numbered. | |
| 174 | + // phpcs:ignore PSR12.Properties.ConstantVisibility.NotFound | |
| 175 | + const STAMPABLE_NAV_ITEMS = ['core/navigation-link', 'core/navigation-submenu']; | |
| 176 | + | |
| 177 | + public static function stampNavItems( | |
| 178 | + string $html, | |
| 179 | + string $prefix, | |
| 180 | + int $ref, | |
| 181 | + string $idAttr, | |
| 182 | + array $extra = [] | |
| 183 | + ): string { | |
| 184 | + $navPost = \get_post($ref); | |
| 185 | + if (!$navPost || $navPost->post_type !== 'wp_navigation') { | |
| 186 | + return $html; | |
| 187 | + } | |
| 188 | + | |
| 189 | + $outline = TemplatePartBlockFinder::outline(parse_blocks($navPost->post_content)); | |
| 190 | + foreach ($outline as $entry) { | |
| 191 | + if (!in_array($entry['n'], self::STAMPABLE_NAV_ITEMS, true)) { | |
| 192 | + return $html; | |
| 193 | + } | |
| 194 | + } | |
| 195 | + | |
| 196 | + $tp = new \WP_HTML_Tag_Processor($html); | |
| 197 | + $index = 0; | |
| 198 | + while ($tp->next_tag('LI') && isset($outline[$index])) { | |
| 199 | + $class = (string) $tp->get_attribute('class'); | |
| 200 | + if (strpos($class, 'wp-block-navigation-item') === false) { | |
| 201 | + continue; | |
| 202 | + } | |
| 203 | + $tp->set_attribute($idAttr, $prefix . $outline[$index]['c']); | |
| 204 | + foreach ($extra as $attr => $value) { | |
| 205 | + $tp->set_attribute($attr, $value); | |
| 206 | + } | |
| 207 | + $index++; | |
| 208 | + } | |
| 209 | + | |
| 210 | + return $tp->get_updated_html(); | |
| 211 | + } | |
| 212 | + | |
| 120 | 213 | public static function onRenderBlock(string $html, array $block): string |
| 121 | 214 | { |
| 122 | 215 | $name = $block['blockName'] ?? ''; |
| 123 | 216 | if ($name === '') { |
| @@ -132,15 +225,31 @@ | ||
| 132 | 225 | if (empty(self::$frames)) { |
| 133 | 226 | return $html; |
| 134 | 227 | } |
| 135 | 228 | |
| 229 | + // Popping on the second pass would take the part's own frame. | |
| 230 | + if (self::isSyncedPattern($block)) { | |
| 231 | + if (self::inFrame(TemplatePartBlockFinder::refPrefix('block', $block))) { | |
| 232 | + array_pop(self::$frames); | |
| 233 | + } | |
| 234 | + return $html; | |
| 235 | + } | |
| 236 | + | |
| 237 | + // A ref nav's items get stamped from the finished html, not counted here. | |
| 238 | + $navPrefix = TemplatePartBlockFinder::isRefNav($block) | |
| 239 | + ? TemplatePartBlockFinder::refPrefix('navigation', $block) | |
| 240 | + : ''; | |
| 241 | + if ($navPrefix && self::inFrame($navPrefix)) { | |
| 242 | + array_pop(self::$frames); | |
| 243 | + } elseif (self::currentFrame()['nav'] ?? false) { | |
| 244 | + return $html; | |
| 245 | + } | |
| 246 | + | |
| 136 | 247 | $frame = self::currentFrame(); |
| 137 | 248 | $skip = $frame['skip_depth'] ?? 0; |
| 138 | 249 | |
| 139 | - // render_block runs bottom-up, so the ignored block's own filter fires | |
| 140 | - // after its descendants — drop one skip level here. Only the outermost | |
| 141 | - // ignored block (skip === 1) was counted in render_block_data, so only it | |
| 142 | - // falls through to be stamped; nested ones and descendants bail. | |
| 250 | + // render_block runs bottom-up, so only the outermost ignored block was | |
| 251 | + // counted and only it may be stamped. | |
| 143 | 252 | if (in_array($name, self::$ignored, true)) { |
| 144 | 253 | $frame['skip_depth'] = max(0, $skip - 1); |
| 145 | 254 | self::setCurrentFrame($frame); |
| 146 | 255 | if ($skip > 1) { |
| @@ -149,12 +258,10 @@ | ||
| 149 | 258 | } elseif ($skip > 0) { |
| 150 | 259 | return $html; |
| 151 | 260 | } |
| 152 | 261 | |
| 153 | - // Name-keyed lookup, not array_pop: core/navigation fires render_block | |
| 154 | - // for inner items without firing render_block_data first, so a | |
| 155 | - // straight pop would consume entries belonging to unrelated outer | |
| 156 | - // blocks. If nothing matches, mint a fresh id (nav-link/-submenu path). | |
| 262 | + // core/navigation fires render_block with no render_block_data, so a | |
| 263 | + // straight pop eats an outer block's entry. | |
| 157 | 264 | $info = null; |
| 158 | 265 | $infoIndex = -1; |
| 159 | 266 | for ($i = count(self::$blockStack) - 1; $i >= 0; $i--) { |
| 160 | 267 | if (self::$blockStack[$i]['name'] === $name) { |
| @@ -171,9 +278,9 @@ | ||
| 171 | 278 | $frame['seq']++; |
| 172 | 279 | self::setCurrentFrame($frame); |
| 173 | 280 | $info = [ |
| 174 | 281 | 'name' => $name, |
| 175 | - 'id' => $frame['seq'], | |
| 282 | + 'id' => ($frame['prefix'] ?? '') . $frame['seq'], | |
| 176 | 283 | 'label' => $frame['label'], |
| 177 | 284 | 'slug' => $frame['slug'] ?? '', |
| 178 | 285 | ]; |
| 179 | 286 | } |
| @@ -181,9 +288,9 @@ | ||
| 181 | 288 | |
| 182 | 289 | if ($info && $html) { |
| 183 | 290 | $tp = new \WP_HTML_Tag_Processor($html); |
| 184 | 291 | if ($tp->next_tag()) { |
| 185 | - $tp->set_attribute('data-extendify-part-block-id', (string) (int) $info['id']); | |
| 292 | + $tp->set_attribute('data-extendify-part-block-id', (string) $info['id']); | |
| 186 | 293 | $tp->set_attribute('data-extendify-part', $info['label']); |
| 187 | 294 | if (!empty($info['slug'])) { |
| 188 | 295 | $tp->set_attribute('data-extendify-part-slug', $info['slug']); |
| 189 | 296 | } |
| @@ -188,8 +295,21 @@ | ||
| 188 | 295 | $tp->set_attribute('data-extendify-part-slug', $info['slug']); |
| 189 | 296 | } |
| 190 | 297 | $html = $tp->get_updated_html(); |
| 191 | 298 | } |
| 299 | + } | |
| 300 | + if ($navPrefix && $info) { | |
| 301 | + $extra = ['data-extendify-part' => $info['label']]; | |
| 302 | + if (!empty($info['slug'])) { | |
| 303 | + $extra['data-extendify-part-slug'] = $info['slug']; | |
| 304 | + } | |
| 305 | + $html = self::stampNavItems( | |
| 306 | + $html, | |
| 307 | + $navPrefix, | |
| 308 | + (int) $block['attrs']['ref'], | |
| 309 | + 'data-extendify-part-block-id', | |
| 310 | + $extra | |
| 311 | + ); | |
| 192 | 312 | } |
| 193 | 313 | return $html; |
| 194 | 314 | } |
| 195 | 315 | } |