PluginProbe
Extendify / trunk
Extendify vtrunk
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.5 → trunk 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 }