| @@ -5,11 +5,11 @@ | ||
| 5 | 5 | defined('ABSPATH') || die('No direct access.'); |
| 6 | 6 | |
| 7 | 7 | class TagBlocks |
| 8 | 8 | { |
| 9 | - // Iterating blocks: their inner template renders once per item, so tagging | |
| 10 | - // children would assign N rendered ids to one parsed block. Skip the subtree. | |
| 11 | - // Public so WPController::getBlockCode walks the same list. | |
| 9 | + // Counting a loop's per-item copies or a cart's drawer spends ids the save | |
| 10 | + // walk can't resolve. | |
| 11 | + // Public so SaveController + WPController can share the same list. | |
| 12 | 12 | public static $ignored = [ |
| 13 | 13 | 'core/query', |
| 14 | 14 | 'core/post-template', |
| 15 | 15 | 'core/post-content', |
| @@ -16,10 +16,69 @@ | ||
| 16 | 16 | 'core/comments', |
| 17 | 17 | 'core/comment-template', |
| 18 | 18 | 'woocommerce/product-collection', |
| 19 | 19 | 'woocommerce/product-template', |
| 20 | + 'woocommerce/mini-cart', | |
| 21 | + 'woocommerce/cart', | |
| 22 | + 'woocommerce/checkout', | |
| 20 | 23 | ]; |
| 21 | 24 | |
| 25 | + // Shared with PostBlockFinder so the save walk skips what this declined to count. | |
| 26 | + public static $refContainers = [ | |
| 27 | + 'core/block' => 'wp_block', | |
| 28 | + ]; | |
| 29 | + | |
| 30 | + // TagTemplateParts numbers the interior from 1 under the part's own slug. | |
| 31 | + private static function partPrefix(array $block): string | |
| 32 | + { | |
| 33 | + return 'part:' . (string) ($block['attrs']['slug'] ?? '') . ':'; | |
| 34 | + } | |
| 35 | + | |
| 36 | + private static function isTemplatePart(array $block): bool | |
| 37 | + { | |
| 38 | + return ($block['blockName'] ?? '') === 'core/template-part'; | |
| 39 | + } | |
| 40 | + | |
| 41 | + // Blocks whose interior belongs to another post's id space. | |
| 42 | + private static function scopePrefix(array $block): string | |
| 43 | + { | |
| 44 | + if (TemplatePartBlockFinder::isRefNav($block)) { | |
| 45 | + return TemplatePartBlockFinder::refPrefix('navigation', $block); | |
| 46 | + } | |
| 47 | + return self::isTemplatePart($block) ? self::partPrefix($block) : ''; | |
| 48 | + } | |
| 49 | + | |
| 50 | + // `prefix` names the post an id belongs to; empty means this post. | |
| 51 | + // A `foreign` frame is numbered by another tagger, so nothing in it counts. | |
| 52 | + // A `ref` frame's ids are only ever resolved by TemplatePartBlockFinder. | |
| 53 | + private static function newFrame( | |
| 54 | + string $prefix = '', | |
| 55 | + bool $foreign = false, | |
| 56 | + bool $ref = false | |
| 57 | + ): array { | |
| 58 | + return [ | |
| 59 | + 'seq' => 0, | |
| 60 | + 'id_stack' => [], | |
| 61 | + 'pushed_stack' => [], | |
| 62 | + 'skip_depth' => 0, // >0 while inside an ignored subtree | |
| 63 | + 'prefix' => $prefix, | |
| 64 | + 'foreign' => $foreign, | |
| 65 | + 'ref' => $ref, | |
| 66 | + ]; | |
| 67 | + } | |
| 68 | + | |
| 69 | + // Diverging from TemplatePartBlockFinder here drifts every later id. | |
| 70 | + private static function counts(array $frame, string $name, array $block): bool | |
| 71 | + { | |
| 72 | + if ($frame['skip_depth'] !== 0) { | |
| 73 | + return false; | |
| 74 | + } | |
| 75 | + if (empty($frame['ref'])) { | |
| 76 | + return !in_array($name, self::$ignored, true); | |
| 77 | + } | |
| 78 | + return !self::isTemplatePart($block); | |
| 79 | + } | |
| 80 | + | |
| 22 | 81 | public static function init() |
| 23 | 82 | { |
| 24 | 83 | \add_filter('the_content', [self::class, 'enterScope'], 0); |
| 25 | 84 | \add_filter('the_content', [self::class, 'leaveScope'], PHP_INT_MAX); |
| @@ -40,15 +99,9 @@ | ||
| 40 | 99 | 'frames' => [], |
| 41 | 100 | ]; |
| 42 | 101 | } |
| 43 | 102 | $GLOBALS['extendify_agent_scope']['depth']++; |
| 44 | - // Each scope has: seq, id_stack, pushed_stack, skip_depth | |
| 45 | - $GLOBALS['extendify_agent_scope']['frames'][] = [ | |
| 46 | - 'seq' => 0, | |
| 47 | - 'id_stack' => [], | |
| 48 | - 'pushed_stack' => [], | |
| 49 | - 'skip_depth' => 0, // >0 while inside an ignored subtree | |
| 50 | - ]; | |
| 103 | + $GLOBALS['extendify_agent_scope']['frames'][] = self::newFrame(); | |
| 51 | 104 | return $content; |
| 52 | 105 | } |
| 53 | 106 | |
| 54 | 107 | public static function leaveScope($content) |
| @@ -79,24 +132,46 @@ | ||
| 79 | 132 | $frame = $S['frames'][$i]; |
| 80 | 133 | |
| 81 | 134 | $name = $parsed_block['blockName']; |
| 82 | 135 | |
| 83 | - // If this block starts an ignored subtree, enter skip mode | |
| 136 | + // Another tagger owns this interior's numbering. | |
| 137 | + if (!empty($frame['foreign'])) { | |
| 138 | + $frame['pushed_stack'][] = ['counts' => false, 'name' => $name]; | |
| 139 | + $GLOBALS['extendify_agent_scope']['frames'][$i] = $frame; | |
| 140 | + return $pre; | |
| 141 | + } | |
| 142 | + | |
| 143 | + // The pattern's blocks render inline but live in another post, so they | |
| 144 | + // are numbered off that post instead of counted here. | |
| 145 | + if (isset(self::$refContainers[$name]) && $frame['skip_depth'] === 0) { | |
| 146 | + $GLOBALS['extendify_agent_scope']['frames'][] = self::newFrame( | |
| 147 | + TemplatePartBlockFinder::refPrefix('block', $parsed_block), | |
| 148 | + false, | |
| 149 | + true | |
| 150 | + ); | |
| 151 | + return $pre; | |
| 152 | + } | |
| 153 | + | |
| 154 | + $counts = self::counts($frame, $name, $parsed_block); | |
| 155 | + if ($counts) { | |
| 156 | + $frame['seq']++; | |
| 157 | + $frame['id_stack'][] = $frame['prefix'] . $frame['seq']; | |
| 158 | + } | |
| 159 | + $frame['pushed_stack'][] = ['counts' => $counts, 'name' => $name]; | |
| 160 | + | |
| 161 | + // Raised after counting so a counted leaf still hides its rendered subtree. | |
| 84 | 162 | if (in_array($name, self::$ignored, true)) { |
| 85 | 163 | $frame['skip_depth']++; |
| 86 | - $frame['pushed_stack'][] = false; // we didn't assign an id to this block | |
| 87 | - } elseif ($frame['skip_depth'] > 0) { | |
| 88 | - // Already skipping? (we're inside an ignored subtree) | |
| 89 | - $frame['pushed_stack'][] = false; // no id for anything under ignored | |
| 90 | - } else { | |
| 91 | - // Normal counting | |
| 92 | - $frame['seq']++; | |
| 93 | - $id = $frame['seq']; | |
| 94 | - $frame['id_stack'][] = $id; | |
| 95 | - $frame['pushed_stack'][] = true; | |
| 96 | 164 | } |
| 97 | 165 | |
| 98 | 166 | $GLOBALS['extendify_agent_scope']['frames'][$i] = $frame; |
| 167 | + | |
| 168 | + // Pops from the interior must not reach the page's stack. | |
| 169 | + $scope = self::scopePrefix($parsed_block); | |
| 170 | + if ($scope !== '' && $frame['skip_depth'] === 0) { | |
| 171 | + $GLOBALS['extendify_agent_scope']['frames'][] = self::newFrame($scope, true); | |
| 172 | + } | |
| 173 | + | |
| 99 | 174 | return $pre; |
| 100 | 175 | } |
| 101 | 176 | |
| 102 | 177 | public static function post($content, $parsed_block) |
| @@ -110,16 +185,41 @@ | ||
| 110 | 185 | $frame = $S['frames'][$i]; |
| 111 | 186 | |
| 112 | 187 | $name = is_array($parsed_block) ? ($parsed_block['blockName'] ?? null) : null; |
| 113 | 188 | |
| 114 | - // Pop pushed flag & optional id (ALWAYS pop to stay balanced) | |
| 115 | - $pushed = !empty($frame['pushed_stack']) ? array_pop($frame['pushed_stack']) : false; | |
| 189 | + // core/block renders through a nested WP_Block::render, so this fires twice. | |
| 190 | + if ($name !== null && isset(self::$refContainers[$name])) { | |
| 191 | + if ($frame['prefix'] === TemplatePartBlockFinder::refPrefix('block', $parsed_block)) { | |
| 192 | + array_pop($GLOBALS['extendify_agent_scope']['frames']); | |
| 193 | + return $content; | |
| 194 | + } | |
| 195 | + } | |
| 196 | + | |
| 197 | + $scope = is_array($parsed_block) ? self::scopePrefix($parsed_block) : ''; | |
| 198 | + if ($scope !== '' && $frame['prefix'] === $scope) { | |
| 199 | + array_pop($GLOBALS['extendify_agent_scope']['frames']); | |
| 200 | + $i = count($GLOBALS['extendify_agent_scope']['frames']) - 1; | |
| 201 | + $frame = $GLOBALS['extendify_agent_scope']['frames'][$i]; | |
| 202 | + } | |
| 203 | + $navPrefix = (is_array($parsed_block) && TemplatePartBlockFinder::isRefNav($parsed_block)) | |
| 204 | + ? TemplatePartBlockFinder::refPrefix('navigation', $parsed_block) | |
| 205 | + : ''; | |
| 206 | + | |
| 207 | + // A loop item's wrapper hits render_block alone, so popping spends the | |
| 208 | + // container's id on the item. | |
| 209 | + $top = $frame['pushed_stack'] ? $frame['pushed_stack'][count($frame['pushed_stack']) - 1] : null; | |
| 210 | + if ($top === null || $top['name'] !== $name) { | |
| 211 | + return $content; | |
| 212 | + } | |
| 213 | + | |
| 214 | + array_pop($frame['pushed_stack']); | |
| 215 | + $pushed = $top['counts']; | |
| 116 | 216 | $id = ($pushed && !empty($frame['id_stack'])) ? array_pop($frame['id_stack']) : null; |
| 117 | 217 | |
| 118 | 218 | // Inject only when: outer scope, we counted this block, html present, not admin |
| 119 | 219 | if (!is_admin() && ($S['depth'] ?? 0) === 1 && $pushed && $id && $content && $name) { |
| 120 | 220 | $tp = new \WP_HTML_Tag_Processor($content); |
| 121 | - $value = (string) (int) $id; | |
| 221 | + $value = (string) $id; | |
| 122 | 222 | |
| 123 | 223 | // Move cursor to the first start tag in the fragment |
| 124 | 224 | if ($tp->next_tag()) { |
| 125 | 225 | $tp->set_attribute('data-extendify-agent-block-id', $value); |
| @@ -124,8 +224,17 @@ | ||
| 124 | 224 | if ($tp->next_tag()) { |
| 125 | 225 | $tp->set_attribute('data-extendify-agent-block-id', $value); |
| 126 | 226 | $content = $tp->get_updated_html(); |
| 127 | 227 | } |
| 228 | + } | |
| 229 | + | |
| 230 | + if ($navPrefix && $content) { | |
| 231 | + $content = TagTemplateParts::stampNavItems( | |
| 232 | + $content, | |
| 233 | + $navPrefix, | |
| 234 | + (int) $parsed_block['attrs']['ref'], | |
| 235 | + 'data-extendify-agent-block-id' | |
| 236 | + ); | |
| 128 | 237 | } |
| 129 | 238 | |
| 130 | 239 | // If this block ends an ignored subtree, exit skip mode |
| 131 | 240 | if ($name && in_array($name, self::$ignored, true) && $frame['skip_depth'] > 0) { |