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 / TagBlocks.php

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

248 lines 8.5 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 class TagBlocks
8 {
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 public static $ignored = [
13 'core/query',
14 'core/post-template',
15 'core/post-content',
16 'core/comments',
17 'core/comment-template',
18 'woocommerce/product-collection',
19 'woocommerce/product-template',
20 'woocommerce/mini-cart',
21 'woocommerce/cart',
22 'woocommerce/checkout',
23 ];
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
81 public static function init()
82 {
83 \add_filter('the_content', [self::class, 'enterScope'], 0);
84 \add_filter('the_content', [self::class, 'leaveScope'], PHP_INT_MAX);
85
86 \add_filter('pre_render_block', [self::class, 'pre'], 10, 2);
87 \add_filter('render_block', [self::class, 'post'], 10, 2);
88 }
89
90 public static function enterScope($content)
91 {
92 if (is_admin()) {
93 return $content;
94 }
95
96 if (empty($GLOBALS['extendify_agent_scope'])) {
97 $GLOBALS['extendify_agent_scope'] = [
98 'depth' => 0,
99 'frames' => [],
100 ];
101 }
102 $GLOBALS['extendify_agent_scope']['depth']++;
103 $GLOBALS['extendify_agent_scope']['frames'][] = self::newFrame();
104 return $content;
105 }
106
107 public static function leaveScope($content)
108 {
109 if (is_admin()) {
110 return $content;
111 }
112 $S =& $GLOBALS['extendify_agent_scope'];
113 if (!empty($S['depth'])) {
114 $S['depth']--;
115 array_pop($S['frames']);
116 }
117 return $content;
118 }
119
120 public static function pre($pre, $parsed_block)
121 {
122 if (is_admin() || !is_array($parsed_block) || empty($parsed_block['blockName'])) {
123 return $pre;
124 }
125
126 $S = $GLOBALS['extendify_agent_scope'] ?? null;
127 if (!$S || ($S['depth'] ?? 0) !== 1 || empty($S['frames'])) {
128 return $pre;
129 } // only outer the_content
130
131 $i = count($S['frames']) - 1;
132 $frame = $S['frames'][$i];
133
134 $name = $parsed_block['blockName'];
135
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.
162 if (in_array($name, self::$ignored, true)) {
163 $frame['skip_depth']++;
164 }
165
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
174 return $pre;
175 }
176
177 public static function post($content, $parsed_block)
178 {
179 $S = $GLOBALS['extendify_agent_scope'] ?? null;
180 if (!$S || empty($S['frames'])) {
181 return $content;
182 }
183
184 $i = count($S['frames']) - 1;
185 $frame = $S['frames'][$i];
186
187 $name = is_array($parsed_block) ? ($parsed_block['blockName'] ?? null) : null;
188
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'];
216 $id = ($pushed && !empty($frame['id_stack'])) ? array_pop($frame['id_stack']) : null;
217
218 // Inject only when: outer scope, we counted this block, html present, not admin
219 if (!is_admin() && ($S['depth'] ?? 0) === 1 && $pushed && $id && $content && $name) {
220 $tp = new \WP_HTML_Tag_Processor($content);
221 $value = (string) $id;
222
223 // Move cursor to the first start tag in the fragment
224 if ($tp->next_tag()) {
225 $tp->set_attribute('data-extendify-agent-block-id', $value);
226 $content = $tp->get_updated_html();
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 );
237 }
238
239 // If this block ends an ignored subtree, exit skip mode
240 if ($name && in_array($name, self::$ignored, true) && $frame['skip_depth'] > 0) {
241 $frame['skip_depth']--;
242 }
243
244 $GLOBALS['extendify_agent_scope']['frames'][$i] = $frame;
245 return $content;
246 }
247 }
248