PluginProbe
Extendify / 3.1.5
Extendify v3.1.5
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 0.7.0 All 126 releases
extendify / app / Agent / TagTemplateParts.php

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

196 lines 6.4 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 currentFrame()
53 {
54 return self::$frames ? self::$frames[count(self::$frames) - 1] : null;
55 }
56
57 private static function setCurrentFrame(array $frame)
58 {
59 self::$frames[count(self::$frames) - 1] = $frame;
60 }
61
62 private static function labelForPart(array $b): string
63 {
64 if (!empty($b['attrs']['area'])) {
65 return (string) $b['attrs']['area'];
66 }
67 if (!empty($b['attrs']['slug'])) {
68 return (string) $b['attrs']['slug'];
69 }
70 return 'template-part';
71 }
72
73 public static function onRenderBlockData(array $block, array $source): array
74 {
75 $name = $block['blockName'] ?? '';
76 if ($name === '') {
77 return $block;
78 }
79
80 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 ];
87 $block['attrs']['__extendify_scope_open'] = 1;
88 return $block;
89 }
90
91 if (empty(self::$frames)) {
92 return $block;
93 }
94
95 $frame = self::currentFrame();
96 if (!$frame) {
97 return $block;
98 }
99
100 // render_block_data runs top-down (before a block's own render), so an
101 // ignored block is counted here as a leaf and the skip is raised *after*
102 // — its descendants then render with skip_depth > 0 and are not counted.
103 if (($frame['skip_depth'] ?? 0) === 0) {
104 $frame['seq']++;
105 self::$blockStack[] = [
106 'name' => $name,
107 'id' => $frame['seq'],
108 'label' => $frame['label'],
109 'slug' => $frame['slug'] ?? '',
110 ];
111 }
112 if (in_array($name, self::$ignored, true)) {
113 $frame['skip_depth'] = ($frame['skip_depth'] ?? 0) + 1;
114 }
115 self::setCurrentFrame($frame);
116
117 return $block;
118 }
119
120 public static function onRenderBlock(string $html, array $block): string
121 {
122 $name = $block['blockName'] ?? '';
123 if ($name === '') {
124 return $html;
125 }
126
127 if (self::isTemplatePart($block) && !empty($block['attrs']['__extendify_scope_open'])) {
128 array_pop(self::$frames);
129 return $html;
130 }
131
132 if (empty(self::$frames)) {
133 return $html;
134 }
135
136 $frame = self::currentFrame();
137 $skip = $frame['skip_depth'] ?? 0;
138
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.
143 if (in_array($name, self::$ignored, true)) {
144 $frame['skip_depth'] = max(0, $skip - 1);
145 self::setCurrentFrame($frame);
146 if ($skip > 1) {
147 return $html;
148 }
149 } elseif ($skip > 0) {
150 return $html;
151 }
152
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).
157 $info = null;
158 $infoIndex = -1;
159 for ($i = count(self::$blockStack) - 1; $i >= 0; $i--) {
160 if (self::$blockStack[$i]['name'] === $name) {
161 $info = self::$blockStack[$i];
162 $infoIndex = $i;
163 break;
164 }
165 }
166 if ($info !== null) {
167 array_splice(self::$blockStack, $infoIndex, 1);
168 } else {
169 $frame = self::currentFrame();
170 if ($frame) {
171 $frame['seq']++;
172 self::setCurrentFrame($frame);
173 $info = [
174 'name' => $name,
175 'id' => $frame['seq'],
176 'label' => $frame['label'],
177 'slug' => $frame['slug'] ?? '',
178 ];
179 }
180 }
181
182 if ($info && $html) {
183 $tp = new \WP_HTML_Tag_Processor($html);
184 if ($tp->next_tag()) {
185 $tp->set_attribute('data-extendify-part-block-id', (string) (int) $info['id']);
186 $tp->set_attribute('data-extendify-part', $info['label']);
187 if (!empty($info['slug'])) {
188 $tp->set_attribute('data-extendify-part-slug', $info['slug']);
189 }
190 $html = $tp->get_updated_html();
191 }
192 }
193 return $html;
194 }
195 }
196