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
extendify / app / Agent / TagTemplateParts.php

TagTemplateParts.php in Extendify 3.2.1, at app/Agent/TagTemplateParts.php

316 lines 10.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace Extendify\Agent;
4
5 defined('ABSPATH') || die('No direct access.');
6
7 // Preorder numbering, per template-part scope.
8 class TagTemplateParts
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
29 private static $frames = [];
30 // Entries keyed by block-name; see onRenderBlock for why a plain stack lost
31 // tags on nav items 5+.
32 private static $blockStack = [];
33
34 public static function init()
35 {
36 add_action('template_redirect', [self::class, 'reset']);
37 add_filter('render_block_data', [self::class, 'onRenderBlockData'], 10, 2);
38 add_filter('render_block', [self::class, 'onRenderBlock'], 10, 2);
39 }
40
41 public static function reset()
42 {
43 self::$frames = [];
44 self::$blockStack = [];
45 }
46
47 private static function isTemplatePart(array $b): bool
48 {
49 return (($b['blockName'] ?? '') === 'core/template-part');
50 }
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
80 private static function currentFrame()
81 {
82 return self::$frames ? self::$frames[count(self::$frames) - 1] : null;
83 }
84
85 private static function setCurrentFrame(array $frame)
86 {
87 self::$frames[count(self::$frames) - 1] = $frame;
88 }
89
90 private static function labelForPart(array $b): string
91 {
92 if (!empty($b['attrs']['area'])) {
93 return (string) $b['attrs']['area'];
94 }
95 if (!empty($b['attrs']['slug'])) {
96 return (string) $b['attrs']['slug'];
97 }
98 return 'template-part';
99 }
100
101 public static function onRenderBlockData(array $block, array $source): array
102 {
103 $name = $block['blockName'] ?? '';
104 if ($name === '') {
105 return $block;
106 }
107
108 if (self::isTemplatePart($block)) {
109 self::$frames[] = self::newFrame(
110 self::labelForPart($block),
111 $block['attrs']['slug'] ?? ''
112 );
113 $block['attrs']['__extendify_scope_open'] = 1;
114 return $block;
115 }
116
117 if (empty(self::$frames)) {
118 return $block;
119 }
120
121 $frame = self::currentFrame();
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) {
147 $frame['seq']++;
148 self::$blockStack[] = [
149 'name' => $name,
150 'id' => ($frame['prefix'] ?? '') . $frame['seq'],
151 'label' => $frame['label'],
152 'slug' => $frame['slug'] ?? '',
153 ];
154 }
155 if (in_array($name, self::$ignored, true)) {
156 $frame['skip_depth'] = ($frame['skip_depth'] ?? 0) + 1;
157 }
158 self::setCurrentFrame($frame);
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
169 return $block;
170 }
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
213 public static function onRenderBlock(string $html, array $block): string
214 {
215 $name = $block['blockName'] ?? '';
216 if ($name === '') {
217 return $html;
218 }
219
220 if (self::isTemplatePart($block) && !empty($block['attrs']['__extendify_scope_open'])) {
221 array_pop(self::$frames);
222 return $html;
223 }
224
225 if (empty(self::$frames)) {
226 return $html;
227 }
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
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
289 if ($info && $html) {
290 $tp = new \WP_HTML_Tag_Processor($html);
291 if ($tp->next_tag()) {
292 $tp->set_attribute('data-extendify-part-block-id', (string) $info['id']);
293 $tp->set_attribute('data-extendify-part', $info['label']);
294 if (!empty($info['slug'])) {
295 $tp->set_attribute('data-extendify-part-slug', $info['slug']);
296 }
297 $html = $tp->get_updated_html();
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 );
312 }
313 return $html;
314 }
315 }
316