PluginProbe ʕ •ᴥ•ʔ
Atarim – AI Agency for WordPress: Edit Pages, Fix Code, Update Plugins, SEO & Client Feedback / 5.1.3
Atarim – AI Agency for WordPress: Edit Pages, Fix Code, Update Plugins, SEO & Client Feedback v5.1.3
5.1.3 5.1.2 5.1.1 5.1 5.0 trunk 3.10 3.11 3.12 3.13 3.14 3.15 3.16 3.17 3.18 3.19 3.2.0 3.2.1 3.22 3.22.1 3.22.2 3.22.3 3.22.4 3.22.5 3.22.6 3.3.0 3.3.1 3.3.2 3.3.2.1 3.3.2.2 3.3.3 3.30 3.31 3.32 3.4 3.4.1 3.4.3 3.4.4 3.5 3.5.1 3.6 3.6.1 3.7 3.8 3.9 3.9.1 3.9.2 3.9.3 3.9.4 3.9.6 3.9.6.1 4.0 4.0.1 4.0.2 4.0.3 4.0.4 4.0.5 4.0.6 4.0.7 4.0.8 4.0.9 4.1.0 4.1.1 4.1.2 4.1.3 4.2 4.2.1 4.2.2 4.3 4.3.1 4.3.2 4.3.3 4.3.4 4.3.5 4.4
atarim-visual-collaboration / third-party / page-builder / divi / class-avcf-divi-helpers.php
atarim-visual-collaboration / third-party / page-builder / divi Last commit date
class-avcf-abilities-divi-pro.php 4 days ago class-avcf-abilities-divi.php 4 days ago class-avcf-divi-detector.php 4 days ago class-avcf-divi-helpers.php 4 days ago
class-avcf-divi-helpers.php
533 lines
1 <?php
2 /**
3 * Shared helpers for the Divi 5 ability cluster.
4 *
5 * Divi 5 stores page content as `divi/*` WordPress blocks in post_content. We
6 * parse with core parse_blocks() into a nested tree of { name, attrs, children }
7 * nodes (non-Divi blocks preserved verbatim as opaque passthrough so writes
8 * never drop them), address nodes by slash-path of child indices ("0/1/0"), and
9 * serialize back with core serialize_blocks(). Built on stable WP core block
10 * functions, so the content round-trip is reliable; the module *registry*
11 * (list/schema) depends on Divi 5 internals and degrades gracefully.
12 *
13 * Divi 4 shortcodes are NOT handled. Not runtime-tested here.
14 *
15 * @package atarim-visual-collaboration
16 */
17
18 if ( ! defined('ABSPATH') ) {
19 exit;
20 }
21
22 class AVCF_Divi_Helpers {
23
24 /** Structural skeleton: section > row > column > (modules / group). */
25 public static function structure_children() {
26 return [
27 'divi/section' => [ 'divi/row' ],
28 'divi/row' => [ 'divi/column' ],
29 'divi/row-inner' => [ 'divi/column-inner' ],
30 'divi/column' => [ 'divi/row-inner', 'divi/group' ],
31 'divi/column-inner' => [ 'divi/group' ],
32 'divi/group' => [],
33 ];
34 }
35 public static function raw_html_modules() {
36 return [ 'divi/code', 'divi/fullwidth-code' ];
37 }
38
39 /* ---------------------------- round-trip --------------------------- */
40
41 public static function parse_tree( $content ) {
42 return self::blocks_to_tree( parse_blocks( (string) $content ) );
43 }
44
45 private static function blocks_to_tree( $blocks ) {
46 $tree = [];
47 foreach ( (array) $blocks as $i => $block ) {
48 $name = isset( $block['blockName'] ) && is_string( $block['blockName'] ) ? $block['blockName'] : '';
49 if ( strpos( $name, 'divi/' ) === 0 ) {
50 $inner = isset( $block['innerBlocks'] ) && is_array( $block['innerBlocks'] ) ? $block['innerBlocks'] : [];
51 $tree[] = [
52 'index' => (int) $i,
53 'name' => $name,
54 'attrs' => isset( $block['attrs'] ) && is_array( $block['attrs'] ) ? $block['attrs'] : [],
55 'children' => self::blocks_to_tree( $inner ),
56 ];
57 continue;
58 }
59 $html = isset( $block['innerHTML'] ) && is_string( $block['innerHTML'] ) ? $block['innerHTML'] : '';
60 if ( $name === '' && trim( $html ) === '' ) {
61 continue; // insignificant whitespace
62 }
63 $tree[] = [ 'index' => (int) $i, 'name' => $name, '_raw' => $block, 'children' => [] ];
64 }
65 return $tree;
66 }
67
68 /**
69 * WHOLE-DOCUMENT OVERWRITE ONLY (set-content), where the caller supplies the
70 * entire tree and there is no prior markup to preserve. Never use this to
71 * write back a tree that was read from an existing post: the read projection
72 * discards each divi/* block's own inner HTML (which is where divi/code and
73 * divi/fullwidth-code keep their content) and all inter-block whitespace, so
74 * re-serialising it destroys them. Granular edits go through the raw_* engine.
75 */
76 public static function serialize_tree( $tree ) {
77 return serialize_blocks( self::tree_to_blocks( $tree ) );
78 }
79
80 private static function tree_to_blocks( $tree ) {
81 $blocks = [];
82 foreach ( (array) $tree as $node ) {
83 if ( isset( $node['_raw'] ) && is_array( $node['_raw'] ) ) {
84 $blocks[] = self::passthrough_block( $node['_raw'] );
85 continue;
86 }
87 $children = isset( $node['children'] ) && is_array( $node['children'] ) ? $node['children'] : [];
88 $inner = self::tree_to_blocks( $children );
89 $blocks[] = [
90 'blockName' => isset( $node['name'] ) && is_string( $node['name'] ) ? $node['name'] : '',
91 'attrs' => isset( $node['attrs'] ) && is_array( $node['attrs'] ) ? $node['attrs'] : [],
92 'innerBlocks' => $inner,
93 'innerHTML' => '',
94 'innerContent' => $inner ? array_fill( 0, count( $inner ), null ) : [],
95 ];
96 }
97 return $blocks;
98 }
99
100 private static function passthrough_block( $raw ) {
101 $inner = [];
102 foreach ( ( isset( $raw['innerBlocks'] ) && is_array( $raw['innerBlocks'] ) ? $raw['innerBlocks'] : [] ) as $child ) {
103 if ( is_array( $child ) ) { $inner[] = $child; }
104 }
105 return [
106 'blockName' => isset( $raw['blockName'] ) && is_string( $raw['blockName'] ) ? $raw['blockName'] : null,
107 'attrs' => isset( $raw['attrs'] ) && is_array( $raw['attrs'] ) ? $raw['attrs'] : [],
108 'innerBlocks' => $inner,
109 'innerHTML' => isset( $raw['innerHTML'] ) && is_string( $raw['innerHTML'] ) ? $raw['innerHTML'] : '',
110 'innerContent' => isset( $raw['innerContent'] ) && is_array( $raw['innerContent'] ) ? $raw['innerContent'] : [],
111 ];
112 }
113
114 public static function read_tree( $post_id ) {
115 $post = get_post( $post_id );
116 return $post ? self::parse_tree( $post->post_content ) : [];
117 }
118 public static function write_tree( $post_id, $tree ) {
119 $res = wp_update_post( [ 'ID' => (int) $post_id, 'post_content' => self::serialize_tree( $tree ) ], true );
120 return ! is_wp_error( $res );
121 }
122
123 /* ----------------------------- address ----------------------------- */
124
125 public static function split_address( $address ) {
126 $segments = explode( '/', (string) $address );
127 $index = (int) array_pop( $segments );
128 return [ implode( '/', $segments ), $index ];
129 }
130
131 /** Strict: numeric, non-empty, no leading-zero aliasing. */
132 private static function valid_segment( $segment ) {
133 if ( $segment === '' || ! ctype_digit( $segment ) ) { return false; }
134 if ( $segment !== '0' && $segment[0] === '0' ) { return false; }
135 return true;
136 }
137
138 public static function tree_get( $tree, $address ) {
139 $nodes = $tree; $found = null;
140 foreach ( explode( '/', (string) $address ) as $segment ) {
141 if ( ! self::valid_segment( $segment ) ) { return null; }
142 $index = (int) $segment;
143 if ( ! array_key_exists( $index, $nodes ) ) { return null; }
144 $found = $nodes[ $index ];
145 $nodes = isset( $found['children'] ) && is_array( $found['children'] ) ? $found['children'] : [];
146 }
147 return $found;
148 }
149
150 public static function tree_update_children( $tree, $parent_address, $op ) {
151 if ( $parent_address === '' ) { return call_user_func( $op, $tree ); }
152 return self::tree_descend( $tree, explode( '/', $parent_address ), $op );
153 }
154 private static function tree_descend( $nodes, $path, $op ) {
155 $index = (int) array_shift( $path );
156 if ( ! array_key_exists( $index, $nodes ) ) { return null; }
157 $children = isset( $nodes[ $index ]['children'] ) && is_array( $nodes[ $index ]['children'] ) ? $nodes[ $index ]['children'] : [];
158 $updated = empty( $path ) ? call_user_func( $op, $children ) : self::tree_descend( $children, $path, $op );
159 if ( $updated === null ) { return null; }
160 $nodes[ $index ]['children'] = $updated;
161 return $nodes;
162 }
163
164 public static function tree_replace( $tree, $address, $node ) {
165 list( $parent, $index ) = self::split_address( $address );
166 return self::tree_update_children( $tree, $parent, function( $children ) use ( $index, $node ) {
167 if ( ! array_key_exists( $index, $children ) ) { return null; }
168 $children[ $index ] = $node;
169 return $children;
170 } );
171 }
172 public static function tree_remove( $tree, $address ) {
173 list( $parent, $index ) = self::split_address( $address );
174 return self::tree_update_children( $tree, $parent, function( $children ) use ( $index ) {
175 if ( ! array_key_exists( $index, $children ) ) { return null; }
176 array_splice( $children, $index, 1 );
177 return $children;
178 } );
179 }
180 public static function tree_insert( $tree, $parent_address, $position, $node ) {
181 return self::tree_update_children( $tree, $parent_address, function( $children ) use ( $position, $node ) {
182 $pos = max( 0, min( (int) $position, count( $children ) ) );
183 array_splice( $children, $pos, 0, [ $node ] );
184 return $children;
185 } );
186 }
187
188 /* --------------------------- flatten / view ------------------------ */
189
190 /** Flatten to addressed rows for get-content. */
191 public static function flatten( $tree, $include_attrs = false, $cap = 400 ) {
192 $out = []; $count = [ 0 ];
193 self::flatten_level( $tree, '', '', $include_attrs, $cap, $out, $count );
194 return $out;
195 }
196 private static function flatten_level( $nodes, $prefix, $parent, $include_attrs, $cap, &$out, &$count ) {
197 foreach ( $nodes as $node ) {
198 $i = isset( $node['index'] ) ? (int) $node['index'] : 0;
199 if ( $count[0] >= $cap ) { return; }
200 $addr = $prefix === '' ? (string) $i : $prefix . '/' . $i;
201 $children = isset( $node['children'] ) && is_array( $node['children'] ) ? $node['children'] : [];
202 $is_raw = isset( $node['_raw'] );
203 $row = [
204 'address' => $addr,
205 'name' => isset( $node['name'] ) ? (string) $node['name'] : '',
206 'parent' => $parent,
207 'child_count' => count( $children ),
208 'passthrough' => $is_raw,
209 ];
210 if ( $include_attrs && ! $is_raw ) {
211 $row['attrs'] = isset( $node['attrs'] ) && is_array( $node['attrs'] ) ? $node['attrs'] : [];
212 }
213 $out[] = $row;
214 $count[0]++;
215 if ( $children ) { self::flatten_level( $children, $addr, $addr, $include_attrs, $cap, $out, $count ); }
216 }
217 }
218 public static function tree_count( $tree ) {
219 $n = 0;
220 foreach ( (array) $tree as $node ) {
221 $n++;
222 if ( isset( $node['children'] ) && is_array( $node['children'] ) ) { $n += self::tree_count( $node['children'] ); }
223 }
224 return $n;
225 }
226
227 /* --------------------------- module meta --------------------------- */
228
229 public static function normalize_module_name( $name ) {
230 $name = trim( (string) $name );
231 if ( $name === '' ) { return ''; }
232 return strpos( $name, 'divi/' ) === 0 ? $name : 'divi/' . ltrim( $name, '/' );
233 }
234
235 /** Module metadata from the Divi 5 registry, or null if unavailable. */
236 public static function module_metadata( $name ) {
237 $name = self::normalize_module_name( $name );
238 $cls = '\ET\Builder\Packages\ModuleLibrary\ModuleRegistration';
239 if ( $name === '' || ! class_exists( $cls ) || ! method_exists( $cls, 'get_core_module_metadata' ) ) {
240 return null;
241 }
242 try {
243 $meta = call_user_func( [ $cls, 'get_core_module_metadata' ], $name );
244 } catch ( \Throwable $e ) {
245 return null;
246 }
247 if ( ! is_array( $meta ) || $meta === [] || ( ( $meta['name'] ?? null ) !== $name ) ) {
248 return null;
249 }
250 return $meta;
251 }
252
253 /** Curated fallback list of common Divi 5 modules (when registry can't enumerate). */
254 public static function fallback_modules() {
255 return [
256 [ 'name' => 'divi/section', 'category' => 'structure' ],
257 [ 'name' => 'divi/row', 'category' => 'structure' ],
258 [ 'name' => 'divi/column', 'category' => 'structure' ],
259 [ 'name' => 'divi/text', 'category' => 'module' ],
260 [ 'name' => 'divi/heading', 'category' => 'module' ],
261 [ 'name' => 'divi/image', 'category' => 'module' ],
262 [ 'name' => 'divi/button', 'category' => 'module' ],
263 [ 'name' => 'divi/blurb', 'category' => 'module' ],
264 [ 'name' => 'divi/cta', 'category' => 'module' ],
265 [ 'name' => 'divi/divider', 'category' => 'module' ],
266 [ 'name' => 'divi/icon', 'category' => 'module' ],
267 [ 'name' => 'divi/code', 'category' => 'module' ],
268 [ 'name' => 'divi/blog', 'category' => 'module' ],
269 [ 'name' => 'divi/gallery', 'category' => 'module' ],
270 [ 'name' => 'divi/accordion', 'category' => 'module' ],
271 [ 'name' => 'divi/tabs', 'category' => 'module' ],
272 [ 'name' => 'divi/toggle', 'category' => 'module' ],
273 [ 'name' => 'divi/slider', 'category' => 'module' ],
274 [ 'name' => 'divi/testimonial', 'category' => 'module' ],
275 [ 'name' => 'divi/video', 'category' => 'module' ],
276 ];
277 }
278
279 /** Lite structural validation. Returns ['errors'=>[], 'warnings'=>[]]. */
280 public static function validate_tree( $tree ) {
281 $errors = []; $warnings = [];
282 self::validate_level( $tree, null, $errors, $warnings );
283 return [ 'errors' => array_values( array_unique( $errors ) ), 'warnings' => array_values( array_unique( $warnings ) ) ];
284 }
285 private static function validate_level( $nodes, $parent_name, &$errors, &$warnings ) {
286 $structure = self::structure_children();
287 foreach ( (array) $nodes as $node ) {
288 $name = isset( $node['name'] ) ? (string) $node['name'] : '';
289 if ( isset( $node['_raw'] ) ) { continue; } // passthrough, not validated
290 if ( $parent_name === null ) {
291 if ( $name !== 'divi/section' ) {
292 $errors[] = sprintf( '"%s" is not allowed at the top level — only divi/section may sit at the root.', $name );
293 }
294 } elseif ( array_key_exists( $parent_name, $structure ) ) {
295 $allowed = $structure[ $parent_name ];
296 // Containers that hold leaf modules (column/column-inner/group) accept any module.
297 $is_container_for_modules = in_array( $parent_name, [ 'divi/column', 'divi/column-inner', 'divi/group' ], true );
298 if ( ! $is_container_for_modules && ! in_array( $name, $allowed, true ) ) {
299 $errors[] = sprintf( '"%s" is not allowed inside "%s".', $name, $parent_name );
300 }
301 }
302 if ( in_array( $name, self::raw_html_modules(), true ) ) {
303 $warnings[] = sprintf( '"%s" embeds raw HTML; prefer native Divi modules so the layout stays editable.', $name );
304 }
305 $children = isset( $node['children'] ) && is_array( $node['children'] ) ? $node['children'] : [];
306 self::validate_level( $children, $name, $errors, $warnings );
307 }
308 }
309
310 /** Recursive deep-merge (associative merged; list values replaced). */
311 public static function deep_merge( $base, $patch ) {
312 foreach ( (array) $patch as $k => $v ) {
313 if ( is_array( $v ) && isset( $base[ $k ] ) && is_array( $base[ $k ] ) && self::is_assoc( $v ) && self::is_assoc( $base[ $k ] ) ) {
314 $base[ $k ] = self::deep_merge( $base[ $k ], $v );
315 } else {
316 $base[ $k ] = $v;
317 }
318 }
319 return $base;
320 }
321 private static function is_assoc( $arr ) {
322 if ( ! is_array( $arr ) || $arr === [] ) { return false; }
323 return array_keys( $arr ) !== range( 0, count( $arr ) - 1 );
324 }
325
326 /** Shallow {name, attrs, children} projection of a raw block, for validate_tree() only. */
327 public static function raw_to_probe( $block ) {
328 $name = isset( $block['blockName'] ) && is_string( $block['blockName'] ) ? $block['blockName'] : '';
329 $kids = [];
330 foreach ( ( isset( $block['innerBlocks'] ) && is_array( $block['innerBlocks'] ) ? $block['innerBlocks'] : [] ) as $c ) {
331 $kids[] = self::raw_to_probe( $c );
332 }
333 return [ 'name' => $name, 'attrs' => isset( $block['attrs'] ) && is_array( $block['attrs'] ) ? $block['attrs'] : [], 'children' => $kids ];
334 }
335
336 /* ------------------------- raw block writes -------------------------
337 *
338 * The tree above is a READ projection and is LOSSY BY DESIGN: it drops the
339 * literal HTML chunks a container wraps its children in, and the whitespace
340 * between siblings. Writes must never round-trip through it -- that is the
341 * bug this section replaced. Everything below mutates the raw parse_blocks()
342 * array and serializes that, so nothing is translated and nothing is lost.
343 *
344 * innerContent is the interleave serialize_block() walks: every string is
345 * printed literally, every null consumes the next entry of innerBlocks.
346 * Adding or removing a child without keeping the null count in step silently
347 * drops or duplicates content. raw_splice_children() and raw_unsplice_child()
348 * are the only two places allowed to touch it.
349 *
350 * Addresses are chains of RAW parse_blocks indices. The read view skips
351 * whitespace nodes, so published addresses are not contiguous. Never renumber.
352 */
353
354 public static function parse_raw( $content ) { return parse_blocks( (string) $content ); }
355 public static function serialize_raw( $blocks ) { return serialize_blocks( (array) $blocks ); }
356
357 public static function read_raw( $post_id ) {
358 $post = get_post( $post_id );
359 return $post ? self::parse_raw( $post->post_content ) : [];
360 }
361 public static function write_raw( $post_id, $blocks ) {
362 $res = wp_update_post( [ 'ID' => (int) $post_id, 'post_content' => self::serialize_raw( $blocks ) ], true );
363 return ! is_wp_error( $res );
364 }
365
366 /** Strict index-path -> int list. '' / 'root' = document root. null = malformed. */
367 public static function raw_address_parts( $address ) {
368 $address = trim( (string) $address );
369 if ( $address === '' || $address === 'root' ) { return []; }
370 $parts = [];
371 foreach ( explode( '/', $address ) as $seg ) {
372 if ( $seg === '' || ! ctype_digit( $seg ) ) { return null; }
373 if ( $seg !== '0' && $seg[0] === '0' ) { return null; }
374 $parts[] = (int) $seg;
375 }
376 return $parts;
377 }
378 public static function raw_split_address( $address ) {
379 $parts = self::raw_address_parts( $address );
380 if ( $parts === null || $parts === [] ) { return [ '', null ]; }
381 $index = array_pop( $parts );
382 return [ implode( '/', $parts ), $index ];
383 }
384
385 public static function raw_node_at( $blocks, $address ) {
386 $parts = self::raw_address_parts( $address );
387 if ( $parts === null ) { return null; }
388 $nodes = (array) $blocks; $found = null;
389 foreach ( $parts as $i ) {
390 if ( ! array_key_exists( $i, $nodes ) ) { return null; }
391 $found = $nodes[ $i ];
392 $nodes = isset( $found['innerBlocks'] ) && is_array( $found['innerBlocks'] ) ? $found['innerBlocks'] : [];
393 }
394 return $found;
395 }
396
397 /*
398 * The document root is not a block and has no innerContent. Wrapping it in a
399 * synthetic parent lets every address -- root included -- share one splice
400 * path; the synthetic innerContent is discarded on unwrap.
401 */
402 private static function raw_wrap_root( $blocks ) {
403 $blocks = array_values( (array) $blocks );
404 return [ 'blockName' => null, 'attrs' => [], 'innerBlocks' => $blocks, 'innerHTML' => '', 'innerContent' => array_fill( 0, count( $blocks ), null ) ];
405 }
406 private static function raw_unwrap_root( $root ) {
407 return ( is_array( $root ) && isset( $root['innerBlocks'] ) && is_array( $root['innerBlocks'] ) ) ? array_values( $root['innerBlocks'] ) : [];
408 }
409 private static function raw_apply_at( $block, $parts, $fn ) {
410 if ( $parts === [] ) { return call_user_func( $fn, $block ); }
411 $i = array_shift( $parts );
412 $inner = isset( $block['innerBlocks'] ) && is_array( $block['innerBlocks'] ) ? $block['innerBlocks'] : [];
413 if ( ! array_key_exists( $i, $inner ) ) { return null; }
414 $updated = self::raw_apply_at( $inner[ $i ], $parts, $fn );
415 if ( $updated === null ) { return null; }
416 $inner[ $i ] = $updated;
417 $block['innerBlocks'] = $inner;
418 return $block;
419 }
420
421 private static function raw_placeholder_offsets( $inner_content ) {
422 $pos = [];
423 foreach ( (array) $inner_content as $i => $chunk ) { if ( ! is_string( $chunk ) ) { $pos[] = $i; } }
424 return $pos;
425 }
426
427 /**
428 * A container with no children has no placeholder to insert against and its
429 * wrapper is one opaque literal. Split it just before its final closing tag
430 * so the first child lands inside. null = cannot open safely; caller refuses.
431 */
432 private static function raw_open_empty_container( $inner_content ) {
433 $joined = '';
434 foreach ( (array) $inner_content as $chunk ) { if ( is_string( $chunk ) ) { $joined .= $chunk; } }
435 if ( $joined === '' ) { return [ null ]; }
436 if ( trim( $joined ) === '' ) { return [ $joined, null ]; }
437 if ( ! preg_match( '~</[A-Za-z][A-Za-z0-9:-]*>\s*$~', $joined, $m, PREG_OFFSET_CAPTURE ) ) { return null; }
438 $at = (int) $m[0][1];
439 return [ substr( $joined, 0, $at ), null, substr( $joined, $at ) ];
440 }
441
442 private static function raw_splice_children( $parent, $k, $new ) {
443 $new = array_values( (array) $new );
444 if ( $new === [] ) { return $parent; }
445 $inner = isset( $parent['innerBlocks'] ) && is_array( $parent['innerBlocks'] ) ? $parent['innerBlocks'] : [];
446 $ic = isset( $parent['innerContent'] ) && is_array( $parent['innerContent'] ) ? array_values( $parent['innerContent'] ) : [];
447 $k = max( 0, min( (int) $k, count( $inner ) ) );
448 $slots = self::raw_placeholder_offsets( $ic );
449 if ( $slots === [] ) {
450 $opened = self::raw_open_empty_container( $ic );
451 if ( $opened === null ) { return null; }
452 $ic = $opened;
453 $s = self::raw_placeholder_offsets( $ic );
454 $at = $s[0];
455 array_splice( $ic, $at, 1 );
456 } else {
457 $at = ( $k < count( $slots ) ) ? $slots[ $k ] : ( $slots[ count( $slots ) - 1 ] + 1 );
458 }
459 array_splice( $inner, $k, 0, $new );
460 array_splice( $ic, $at, 0, array_fill( 0, count( $new ), null ) );
461 $parent['innerBlocks'] = $inner;
462 $parent['innerContent'] = $ic;
463 return $parent;
464 }
465
466 private static function raw_unsplice_child( $parent, $k ) {
467 $inner = isset( $parent['innerBlocks'] ) && is_array( $parent['innerBlocks'] ) ? $parent['innerBlocks'] : [];
468 $ic = isset( $parent['innerContent'] ) && is_array( $parent['innerContent'] ) ? array_values( $parent['innerContent'] ) : [];
469 if ( ! array_key_exists( $k, $inner ) ) { return null; }
470 $slots = self::raw_placeholder_offsets( $ic );
471 array_splice( $inner, $k, 1 );
472 if ( isset( $slots[ $k ] ) ) { array_splice( $ic, $slots[ $k ], 1 ); }
473 $parent['innerBlocks'] = $inner;
474 $parent['innerContent'] = $ic;
475 return $parent;
476 }
477
478 public static function raw_replace_at( $blocks, $address, $new_block ) {
479 list( $parent, $index ) = self::raw_split_address( $address );
480 if ( $index === null ) { return null; }
481 $parts = self::raw_address_parts( $parent );
482 if ( $parts === null ) { return null; }
483 $root = self::raw_apply_at( self::raw_wrap_root( $blocks ), $parts, function( $p ) use ( $index, $new_block ) {
484 $inner = isset( $p['innerBlocks'] ) && is_array( $p['innerBlocks'] ) ? $p['innerBlocks'] : [];
485 if ( ! array_key_exists( $index, $inner ) ) { return null; }
486 $inner[ $index ] = $new_block;
487 $p['innerBlocks'] = $inner;
488 return $p;
489 } );
490 return $root === null ? null : self::raw_unwrap_root( $root );
491 }
492
493 public static function raw_remove_at( $blocks, $address ) {
494 list( $parent, $index ) = self::raw_split_address( $address );
495 if ( $index === null ) { return null; }
496 $parts = self::raw_address_parts( $parent );
497 if ( $parts === null ) { return null; }
498 $root = self::raw_apply_at( self::raw_wrap_root( $blocks ), $parts, function( $p ) use ( $index ) {
499 return self::raw_unsplice_child( $p, $index );
500 } );
501 return $root === null ? null : self::raw_unwrap_root( $root );
502 }
503
504 /** $new_blocks is a list of raw blocks. position null = append. */
505 public static function raw_insert_at( $blocks, $parent_address, $position, $new_blocks ) {
506 $parts = self::raw_address_parts( $parent_address );
507 if ( $parts === null ) { return null; }
508 $root = self::raw_apply_at( self::raw_wrap_root( $blocks ), $parts, function( $p ) use ( $position, $new_blocks ) {
509 $n = isset( $p['innerBlocks'] ) && is_array( $p['innerBlocks'] ) ? count( $p['innerBlocks'] ) : 0;
510 return self::raw_splice_children( $p, $position === null ? $n : (int) $position, $new_blocks );
511 } );
512 return $root === null ? null : self::raw_unwrap_root( $root );
513 }
514
515 /** Build a leaf raw block. Containers must come from markup, not from a name. */
516 public static function raw_make_block( $name, $attrs = [], $html = '' ) {
517 $name = (string) $name;
518 $html = (string) $html;
519 $attrs = is_array( $attrs ) ? $attrs : [];
520 return [ 'blockName' => $name !== '' ? $name : null, 'attrs' => $attrs, 'innerBlocks' => [], 'innerHTML' => $html, 'innerContent' => $html !== '' ? [ $html ] : [] ];
521 }
522
523 /** Set a raw block's own inner HTML. Refuses when it would orphan children. */
524 public static function raw_set_html( $block, $html ) {
525 $kids = isset( $block['innerBlocks'] ) && is_array( $block['innerBlocks'] ) ? count( $block['innerBlocks'] ) : 0;
526 if ( $kids > 0 ) { return null; }
527 $html = (string) $html;
528 $block['innerHTML'] = $html;
529 $block['innerContent'] = $html !== '' ? [ $html ] : [];
530 return $block;
531 }
532
533 }