PluginProbe
Extendify / 3.2.1
Extendify v3.2.1
3.2.1 3.2.0 3.1.6 3.1.5 3.1.4 3.1.3 3.1.2 3.1.1 3.1.0 3.0.6 3.0.5 3.0.4 trunk 0.1.0 0.10.0 0.10.1 0.10.2 0.11.0 0.11.1 0.2.0 0.3.0 0.3.1 0.4.0 0.5.0 0.6.0 All 127 releases
← All changes | app/Agent/TagTemplateParts.php +215 -37 3.0.43.2.1 View file →
@@ -3,36 +3,81 @@
3 3 namespace Extendify\Agent;
4 4
5 5 defined('ABSPATH') || die('No direct access.');
6 6
7 -/**
8 - * Tag blocks inside template parts (header/footer/etc.) with deterministic IDs.
9 - * - Only runs inside core/template-part scopes.
10 - * - Preorder numbering per template-part.
11 - */
7 +// Preorder numbering, per template-part scope.
12 8 class TagTemplateParts
13 9 {
10 + // Dynamic self-rendering blocks: their rendered output contains blocks that
11 + // aren't in the parsed tree (a mini-cart drawer, a loop's per-item copies),
12 + // so counting that output inflates every later id beyond what the static
13 + // TemplatePartBlockFinder can resolve. Each is counted as one opaque leaf
14 + // here and its rendered subtree skipped; the finder shares this list and
15 + // treats them as leaves too (counted, not descended into).
16 + public static $ignored = [
17 + 'core/query',
18 + 'core/post-template',
19 + 'core/post-content',
20 + 'core/comments',
21 + 'core/comment-template',
22 + 'woocommerce/product-collection',
23 + 'woocommerce/product-template',
24 + 'woocommerce/mini-cart',
25 + 'woocommerce/cart',
26 + 'woocommerce/checkout',
27 + ];
28 +
14 29 private static $frames = [];
30 + // Entries keyed by block-name; see onRenderBlock for why a plain stack lost
31 + // tags on nav items 5+.
15 32 private static $blockStack = [];
16 33
17 34 public static function init()
18 35 {
19 - // Reset per-request state
20 - add_action('template_redirect', function () {
21 - self::$frames = [];
22 - self::$blockStack = [];
23 - });
24 - // Assign IDs at the block-data layer (pre-order, stable)
36 + add_action('template_redirect', [self::class, 'reset']);
25 37 add_filter('render_block_data', [self::class, 'onRenderBlockData'], 10, 2);
26 - // Turn those IDs into DOM attributes & manage scope lifecycle
27 38 add_filter('render_block', [self::class, 'onRenderBlock'], 10, 2);
28 39 }
29 40
41 + public static function reset()
42 + {
43 + self::$frames = [];
44 + self::$blockStack = [];
45 + }
46 +
30 47 private static function isTemplatePart(array $b): bool
31 48 {
32 49 return (($b['blockName'] ?? '') === 'core/template-part');
33 50 }
34 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 +
35 80 private static function currentFrame()
36 81 {
37 82 return self::$frames ? self::$frames[count(self::$frames) - 1] : null;
38 83 }
@@ -52,9 +97,8 @@
52 97 }
53 98 return 'template-part';
54 99 }
55 100
56 - /** Assign IDs/labels into attrs before render (pre-order) */
57 101 public static function onRenderBlockData(array $block, array $source): array
58 102 {
59 103 $name = $block['blockName'] ?? '';
60 104 if ($name === '') {
@@ -60,44 +104,113 @@
60 104 if ($name === '') {
61 105 return $block;
62 106 }
63 107
64 - // OPEN a template-part scope (do not number the wrapper itself)
65 108 if (self::isTemplatePart($block)) {
66 - $label = self::labelForPart($block);
67 - $slug = $block['attrs']['slug'] ?? '';
68 - self::$frames[] = [
69 - 'label' => $label,
70 - 'slug' => $slug,
71 - 'seq' => 0, // per-part counter
72 - ];
73 - // mark so we know to pop when this wrapper finishes rendering
109 + self::$frames[] = self::newFrame(
110 + self::labelForPart($block),
111 + $block['attrs']['slug'] ?? ''
112 + );
74 113 $block['attrs']['__extendify_scope_open'] = 1;
75 114 return $block;
76 115 }
77 116
78 - // Not inside a template-part? Ignore (prevents tagging post content).
79 117 if (empty(self::$frames)) {
80 118 return $block;
81 119 }
82 120
83 - // Inside a template part
84 121 $frame = self::currentFrame();
85 - // Any other block inside the part → number it
86 - if ($frame) {
122 + if (!$frame) {
123 + return $block;
124 + }
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 +
143 + // render_block_data runs top-down (before a block's own render), so an
144 + // ignored block is counted here as a leaf and the skip is raised *after*
145 + // — its descendants then render with skip_depth > 0 and are not counted.
146 + if (($frame['skip_depth'] ?? 0) === 0) {
87 147 $frame['seq']++;
88 148 self::$blockStack[] = [
89 - 'id' => $frame['seq'],
149 + 'name' => $name,
150 + 'id' => ($frame['prefix'] ?? '') . $frame['seq'],
90 151 'label' => $frame['label'],
91 152 'slug' => $frame['slug'] ?? '',
92 153 ];
93 - self::setCurrentFrame($frame);
94 154 }
155 + if (in_array($name, self::$ignored, true)) {
156 + $frame['skip_depth'] = ($frame['skip_depth'] ?? 0) + 1;
157 + }
158 + self::setCurrentFrame($frame);
95 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 +
96 169 return $block;
97 170 }
98 171
99 - /** Inject DOM attributes and close scopes after render */
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 +
100 213 public static function onRenderBlock(string $html, array $block): string
101 214 {
102 215 $name = $block['blockName'] ?? '';
103 216 if ($name === '') {
@@ -103,29 +216,81 @@
103 216 if ($name === '') {
104 217 return $html;
105 218 }
106 219
107 - // When the template-part wrapper finishes rendering, POP the frame
108 220 if (self::isTemplatePart($block) && !empty($block['attrs']['__extendify_scope_open'])) {
109 - // wrapper itself isn’t tagged; just close the scope
110 221 array_pop(self::$frames);
111 222 return $html;
112 223 }
113 224
114 - // If we’re not inside a template part, do nothing
115 225 if (empty(self::$frames)) {
116 226 return $html;
117 227 }
118 228
119 - // Inject attributes for blocks we numbered
120 - $info = !empty(self::$blockStack)
121 - ? array_pop(self::$blockStack)
122 - : null;
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 + }
123 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 +
247 + $frame = self::currentFrame();
248 + $skip = $frame['skip_depth'] ?? 0;
249 +
250 + // render_block runs bottom-up, so only the outermost ignored block was
251 + // counted and only it may be stamped.
252 + if (in_array($name, self::$ignored, true)) {
253 + $frame['skip_depth'] = max(0, $skip - 1);
254 + self::setCurrentFrame($frame);
255 + if ($skip > 1) {
256 + return $html;
257 + }
258 + } elseif ($skip > 0) {
259 + return $html;
260 + }
261 +
262 + // core/navigation fires render_block with no render_block_data, so a
263 + // straight pop eats an outer block's entry.
264 + $info = null;
265 + $infoIndex = -1;
266 + for ($i = count(self::$blockStack) - 1; $i >= 0; $i--) {
267 + if (self::$blockStack[$i]['name'] === $name) {
268 + $info = self::$blockStack[$i];
269 + $infoIndex = $i;
270 + break;
271 + }
272 + }
273 + if ($info !== null) {
274 + array_splice(self::$blockStack, $infoIndex, 1);
275 + } else {
276 + $frame = self::currentFrame();
277 + if ($frame) {
278 + $frame['seq']++;
279 + self::setCurrentFrame($frame);
280 + $info = [
281 + 'name' => $name,
282 + 'id' => ($frame['prefix'] ?? '') . $frame['seq'],
283 + 'label' => $frame['label'],
284 + 'slug' => $frame['slug'] ?? '',
285 + ];
286 + }
287 + }
288 +
124 289 if ($info && $html) {
125 290 $tp = new \WP_HTML_Tag_Processor($html);
126 291 if ($tp->next_tag()) {
127 - $tp->set_attribute('data-extendify-part-block-id', (string) (int) $info['id']);
292 + $tp->set_attribute('data-extendify-part-block-id', (string) $info['id']);
128 293 $tp->set_attribute('data-extendify-part', $info['label']);
129 294 if (!empty($info['slug'])) {
130 295 $tp->set_attribute('data-extendify-part-slug', $info['slug']);
131 296 }
@@ -130,8 +295,21 @@
130 295 $tp->set_attribute('data-extendify-part-slug', $info['slug']);
131 296 }
132 297 $html = $tp->get_updated_html();
133 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 + );
134 312 }
135 313 return $html;
136 314 }
137 315 }