class-avcf-etch-helpers.php
| 1 | <?php |
| 2 | /** |
| 3 | * Shared helpers for the Etch ability cluster. |
| 4 | * |
| 5 | * Etch stores its document as Gutenberg blocks in post_content. We parse with |
| 6 | * core parse_blocks() into a tree of { block, attrs?, children?, html? } nodes |
| 7 | * (block = Gutenberg block name like "etch/element"/"etch/text"; html = inner |
| 8 | * HTML for leaf/freeform; block "" + html = freeform HTML), address nodes by |
| 9 | * index-path ("0/1/2"), and serialize back with core serialize_blocks(). |
| 10 | * Built on stable WP core block functions; not runtime-tested here. |
| 11 | * |
| 12 | * @package atarim-visual-collaboration |
| 13 | */ |
| 14 | |
| 15 | if ( ! defined('ABSPATH') ) { |
| 16 | exit; |
| 17 | } |
| 18 | |
| 19 | class AVCF_Etch_Helpers { |
| 20 | |
| 21 | /* ---------------------------- round-trip --------------------------- */ |
| 22 | |
| 23 | public static function parse_tree( $content ) { |
| 24 | return self::blocks_to_tree( parse_blocks( (string) $content ) ); |
| 25 | } |
| 26 | |
| 27 | private static function blocks_to_tree( $blocks ) { |
| 28 | $tree = []; |
| 29 | foreach ( (array) $blocks as $i => $block ) { |
| 30 | $name = isset( $block['blockName'] ) && is_string( $block['blockName'] ) ? $block['blockName'] : ''; |
| 31 | $inner_blocks = isset( $block['innerBlocks'] ) && is_array( $block['innerBlocks'] ) ? $block['innerBlocks'] : []; |
| 32 | $inner_html = isset( $block['innerHTML'] ) && is_string( $block['innerHTML'] ) ? $block['innerHTML'] : ''; |
| 33 | |
| 34 | if ( $name === '' && trim( $inner_html ) === '' ) { continue; } // whitespace |
| 35 | |
| 36 | $node = [ 'index' => (int) $i, 'block' => $name ]; |
| 37 | if ( isset( $block['attrs'] ) && is_array( $block['attrs'] ) && $block['attrs'] !== [] ) { $node['attrs'] = $block['attrs']; } |
| 38 | if ( $inner_blocks ) { $node['children'] = self::blocks_to_tree( $inner_blocks ); } |
| 39 | elseif ( trim( $inner_html ) !== '' ) { $node['html'] = $inner_html; } |
| 40 | $tree[] = $node; |
| 41 | } |
| 42 | return $tree; |
| 43 | } |
| 44 | |
| 45 | /** |
| 46 | * WHOLE-DOCUMENT OVERWRITE ONLY (set-content), where the caller supplies the |
| 47 | * entire tree and there is no prior markup to preserve. Never use this to |
| 48 | * write back a tree that was read from an existing post: the read projection |
| 49 | * has already discarded that post's wrapper markup, so re-serialising it |
| 50 | * destroys every container in the document. Granular edits go through the |
| 51 | * raw_* engine below. |
| 52 | */ |
| 53 | public static function serialize_tree( $tree ) { |
| 54 | return serialize_blocks( self::tree_to_blocks( $tree ) ); |
| 55 | } |
| 56 | |
| 57 | private static function tree_to_blocks( $tree ) { |
| 58 | $blocks = []; |
| 59 | foreach ( (array) $tree as $node ) { |
| 60 | if ( ! is_array( $node ) ) { continue; } |
| 61 | $name = isset( $node['block'] ) ? (string) $node['block'] : ''; |
| 62 | $attrs = isset( $node['attrs'] ) && is_array( $node['attrs'] ) ? $node['attrs'] : []; |
| 63 | $children = isset( $node['children'] ) && is_array( $node['children'] ) ? $node['children'] : []; |
| 64 | $html = isset( $node['html'] ) ? (string) $node['html'] : ''; |
| 65 | |
| 66 | if ( $name === '' ) { |
| 67 | // Freeform HTML node. |
| 68 | $blocks[] = [ 'blockName' => null, 'attrs' => [], 'innerBlocks' => [], 'innerHTML' => $html, 'innerContent' => [ $html ] ]; |
| 69 | continue; |
| 70 | } |
| 71 | if ( $children ) { |
| 72 | $inner = self::tree_to_blocks( $children ); |
| 73 | $blocks[] = [ 'blockName' => $name, 'attrs' => $attrs, 'innerBlocks' => $inner, 'innerHTML' => '', 'innerContent' => $inner ? array_fill( 0, count( $inner ), null ) : [] ]; |
| 74 | } else { |
| 75 | $blocks[] = [ 'blockName' => $name, 'attrs' => $attrs, 'innerBlocks' => [], 'innerHTML' => $html, 'innerContent' => $html !== '' ? [ $html ] : [] ]; |
| 76 | } |
| 77 | } |
| 78 | return $blocks; |
| 79 | } |
| 80 | |
| 81 | public static function read_tree( $post_id ) { |
| 82 | $post = get_post( $post_id ); |
| 83 | return $post ? self::parse_tree( $post->post_content ) : []; |
| 84 | } |
| 85 | public static function write_tree( $post_id, $tree ) { |
| 86 | $res = wp_update_post( [ 'ID' => (int) $post_id, 'post_content' => self::serialize_tree( $tree ) ], true ); |
| 87 | return ! is_wp_error( $res ); |
| 88 | } |
| 89 | |
| 90 | /** |
| 91 | * Lossy = a whole-tree overwrite would destroy something the tree can't carry: |
| 92 | * freeform HTML nodes with real content, or any container holding literal |
| 93 | * wrapper markup around its children (<ul class="wp-block-list">, <div |
| 94 | * class="wp-block-group">, ...). Granular raw_* edits are never lossy; this |
| 95 | * guard is for set-content. |
| 96 | */ |
| 97 | public static function is_lossy( $content ) { |
| 98 | return self::scan_lossy( parse_blocks( (string) $content ) ); |
| 99 | } |
| 100 | private static function scan_lossy( $blocks ) { |
| 101 | foreach ( (array) $blocks as $b ) { |
| 102 | $name = isset( $b['blockName'] ) ? $b['blockName'] : null; |
| 103 | $html = isset( $b['innerHTML'] ) ? (string) $b['innerHTML'] : ''; |
| 104 | if ( ( $name === null || $name === '' ) && trim( $html ) !== '' ) { return true; } |
| 105 | $kids = isset( $b['innerBlocks'] ) && is_array( $b['innerBlocks'] ) ? $b['innerBlocks'] : []; |
| 106 | if ( $kids ) { |
| 107 | foreach ( ( isset( $b['innerContent'] ) && is_array( $b['innerContent'] ) ? $b['innerContent'] : [] ) as $chunk ) { |
| 108 | if ( is_string( $chunk ) && trim( $chunk ) !== '' ) { return true; } // wrapper markup |
| 109 | } |
| 110 | if ( self::scan_lossy( $kids ) ) { return true; } |
| 111 | } |
| 112 | } |
| 113 | return false; |
| 114 | } |
| 115 | |
| 116 | /* ----------------------------- address ----------------------------- */ |
| 117 | |
| 118 | public static function address_parts( $address ) { |
| 119 | $address = trim( (string) $address ); |
| 120 | if ( $address === '' || $address === 'root' ) { return []; } |
| 121 | $parts = []; |
| 122 | foreach ( explode( '/', $address ) as $seg ) { |
| 123 | if ( $seg === '' || ! ctype_digit( $seg ) ) { return null; } |
| 124 | if ( $seg !== '0' && $seg[0] === '0' ) { return null; } |
| 125 | $parts[] = (int) $seg; |
| 126 | } |
| 127 | return $parts; |
| 128 | } |
| 129 | |
| 130 | public static function node_at( $tree, $address ) { |
| 131 | $parts = self::address_parts( $address ); |
| 132 | if ( $parts === null ) { return null; } |
| 133 | $nodes = $tree; $found = null; |
| 134 | foreach ( $parts as $i ) { |
| 135 | if ( ! array_key_exists( $i, $nodes ) ) { return null; } |
| 136 | $found = $nodes[ $i ]; |
| 137 | $nodes = isset( $found['children'] ) && is_array( $found['children'] ) ? $found['children'] : []; |
| 138 | } |
| 139 | return $found; |
| 140 | } |
| 141 | |
| 142 | /** Apply $op to the children list of the node at $parent_address (""=root). */ |
| 143 | public static function update_children( $tree, $parent_address, $op ) { |
| 144 | $parts = self::address_parts( $parent_address ); |
| 145 | if ( $parts === null ) { return null; } |
| 146 | if ( $parts === [] ) { return call_user_func( $op, $tree ); } |
| 147 | return self::descend( $tree, $parts, $op ); |
| 148 | } |
| 149 | private static function descend( $nodes, $parts, $op ) { |
| 150 | $i = array_shift( $parts ); |
| 151 | if ( ! array_key_exists( $i, $nodes ) ) { return null; } |
| 152 | $children = isset( $nodes[ $i ]['children'] ) && is_array( $nodes[ $i ]['children'] ) ? $nodes[ $i ]['children'] : []; |
| 153 | $updated = empty( $parts ) ? call_user_func( $op, $children ) : self::descend( $children, $parts, $op ); |
| 154 | if ( $updated === null ) { return null; } |
| 155 | $nodes[ $i ]['children'] = $updated; |
| 156 | return $nodes; |
| 157 | } |
| 158 | |
| 159 | public static function split_address( $address ) { |
| 160 | $parts = self::address_parts( $address ); |
| 161 | if ( $parts === null || $parts === [] ) { return [ '', null ]; } |
| 162 | $index = array_pop( $parts ); |
| 163 | return [ implode( '/', $parts ), $index ]; |
| 164 | } |
| 165 | |
| 166 | public static function replace_at( $tree, $address, $node ) { |
| 167 | list( $parent, $index ) = self::split_address( $address ); |
| 168 | if ( $index === null ) { return null; } |
| 169 | return self::update_children( $tree, $parent, function( $children ) use ( $index, $node ) { |
| 170 | if ( ! array_key_exists( $index, $children ) ) { return null; } |
| 171 | $children[ $index ] = $node; |
| 172 | return $children; |
| 173 | } ); |
| 174 | } |
| 175 | public static function remove_at( $tree, $address ) { |
| 176 | list( $parent, $index ) = self::split_address( $address ); |
| 177 | if ( $index === null ) { return null; } |
| 178 | return self::update_children( $tree, $parent, function( $children ) use ( $index ) { |
| 179 | if ( ! array_key_exists( $index, $children ) ) { return null; } |
| 180 | array_splice( $children, $index, 1 ); |
| 181 | return $children; |
| 182 | } ); |
| 183 | } |
| 184 | public static function insert_at( $tree, $parent_address, $position, $node ) { |
| 185 | return self::update_children( $tree, $parent_address, function( $children ) use ( $position, $node ) { |
| 186 | $pos = $position === null ? count( $children ) : max( 0, min( (int) $position, count( $children ) ) ); |
| 187 | array_splice( $children, $pos, 0, [ $node ] ); |
| 188 | return $children; |
| 189 | } ); |
| 190 | } |
| 191 | |
| 192 | /* ---------------------------- flatten/view ------------------------- */ |
| 193 | |
| 194 | public static function flatten( $tree, $include_attrs = false, $cap = 400 ) { |
| 195 | $out = []; $count = [ 0 ]; |
| 196 | self::flatten_level( $tree, '', '', $include_attrs, $cap, $out, $count ); |
| 197 | return $out; |
| 198 | } |
| 199 | private static function flatten_level( $nodes, $prefix, $parent, $include_attrs, $cap, &$out, &$count ) { |
| 200 | foreach ( $nodes as $node ) { |
| 201 | if ( $count[0] >= $cap ) { return; } |
| 202 | $i = isset( $node['index'] ) ? (int) $node['index'] : 0; |
| 203 | $addr = $prefix === '' ? (string) $i : $prefix . '/' . $i; |
| 204 | $children = isset( $node['children'] ) && is_array( $node['children'] ) ? $node['children'] : []; |
| 205 | $row = [ 'address' => $addr, 'block' => isset( $node['block'] ) ? (string) $node['block'] : '', 'parent' => $parent, 'child_count' => count( $children ), 'has_html' => isset( $node['html'] ) ]; |
| 206 | if ( $include_attrs ) { |
| 207 | if ( isset( $node['attrs'] ) ) { $row['attrs'] = $node['attrs']; } |
| 208 | if ( isset( $node['html'] ) ) { $row['html'] = (string) $node['html']; } |
| 209 | } |
| 210 | $out[] = $row; |
| 211 | $count[0]++; |
| 212 | if ( $children ) { self::flatten_level( $children, $addr, $addr, $include_attrs, $cap, $out, $count ); } |
| 213 | } |
| 214 | } |
| 215 | public static function tree_count( $tree ) { |
| 216 | $n = 0; |
| 217 | foreach ( (array) $tree as $node ) { |
| 218 | $n++; |
| 219 | if ( isset( $node['children'] ) && is_array( $node['children'] ) ) { $n += self::tree_count( $node['children'] ); } |
| 220 | } |
| 221 | return $n; |
| 222 | } |
| 223 | |
| 224 | /** Sanitize an incoming tree to clean {block, attrs?, children?, html?} nodes. */ |
| 225 | public static function sanitize_tree( $nodes ) { |
| 226 | $out = []; |
| 227 | foreach ( (array) $nodes as $n ) { |
| 228 | if ( ! is_array( $n ) || ! isset( $n['block'] ) ) { continue; } |
| 229 | $node = [ 'block' => (string) $n['block'] ]; |
| 230 | if ( isset( $n['attrs'] ) && is_array( $n['attrs'] ) ) { $node['attrs'] = $n['attrs']; } |
| 231 | if ( isset( $n['children'] ) && is_array( $n['children'] ) ) { $node['children'] = self::sanitize_tree( $n['children'] ); } |
| 232 | elseif ( isset( $n['html'] ) ) { $node['html'] = (string) $n['html']; } |
| 233 | $out[] = $node; |
| 234 | } |
| 235 | return $out; |
| 236 | } |
| 237 | |
| 238 | /* --------------------------- vocabulary ---------------------------- */ |
| 239 | |
| 240 | public static function list_elements() { |
| 241 | $out = []; |
| 242 | if ( class_exists( '\WP_Block_Type_Registry' ) ) { |
| 243 | $reg = \WP_Block_Type_Registry::get_instance(); |
| 244 | if ( $reg && method_exists( $reg, 'get_all_registered' ) ) { |
| 245 | foreach ( $reg->get_all_registered() as $name => $type ) { |
| 246 | if ( strpos( (string) $name, 'etch/' ) === 0 ) { |
| 247 | $out[] = [ 'block' => (string) $name, 'title' => isset( $type->title ) ? (string) $type->title : '' ]; |
| 248 | } |
| 249 | } |
| 250 | } |
| 251 | } |
| 252 | if ( empty( $out ) ) { |
| 253 | foreach ( [ 'etch/element', 'etch/text', 'etch/component', 'etch/loop', 'etch/template-part', 'etch/fragment' ] as $b ) { |
| 254 | $out[] = [ 'block' => $b, 'title' => '' ]; |
| 255 | } |
| 256 | } |
| 257 | return $out; |
| 258 | } |
| 259 | |
| 260 | /* ------------------------- raw block writes ------------------------- |
| 261 | * |
| 262 | * The tree above is a READ projection and is LOSSY BY DESIGN: it drops the |
| 263 | * literal HTML chunks a container wraps its children in, and the whitespace |
| 264 | * between siblings. Writes must never round-trip through it -- that is the |
| 265 | * bug this section replaced. Everything below mutates the raw parse_blocks() |
| 266 | * array and serializes that, so nothing is translated and nothing is lost. |
| 267 | * |
| 268 | * innerContent is the interleave serialize_block() walks: every string is |
| 269 | * printed literally, every null consumes the next entry of innerBlocks. |
| 270 | * Adding or removing a child without keeping the null count in step silently |
| 271 | * drops or duplicates content. raw_splice_children() and raw_unsplice_child() |
| 272 | * are the only two places allowed to touch it. |
| 273 | * |
| 274 | * Addresses are chains of RAW parse_blocks indices. The read view skips |
| 275 | * whitespace nodes, so published addresses are not contiguous. Never renumber. |
| 276 | */ |
| 277 | |
| 278 | public static function parse_raw( $content ) { return parse_blocks( (string) $content ); } |
| 279 | public static function serialize_raw( $blocks ) { return serialize_blocks( (array) $blocks ); } |
| 280 | |
| 281 | public static function read_raw( $post_id ) { |
| 282 | $post = get_post( $post_id ); |
| 283 | return $post ? self::parse_raw( $post->post_content ) : []; |
| 284 | } |
| 285 | public static function write_raw( $post_id, $blocks ) { |
| 286 | $res = wp_update_post( [ 'ID' => (int) $post_id, 'post_content' => self::serialize_raw( $blocks ) ], true ); |
| 287 | return ! is_wp_error( $res ); |
| 288 | } |
| 289 | |
| 290 | /** Strict index-path -> int list. '' / 'root' = document root. null = malformed. */ |
| 291 | public static function raw_address_parts( $address ) { |
| 292 | $address = trim( (string) $address ); |
| 293 | if ( $address === '' || $address === 'root' ) { return []; } |
| 294 | $parts = []; |
| 295 | foreach ( explode( '/', $address ) as $seg ) { |
| 296 | if ( $seg === '' || ! ctype_digit( $seg ) ) { return null; } |
| 297 | if ( $seg !== '0' && $seg[0] === '0' ) { return null; } |
| 298 | $parts[] = (int) $seg; |
| 299 | } |
| 300 | return $parts; |
| 301 | } |
| 302 | public static function raw_split_address( $address ) { |
| 303 | $parts = self::raw_address_parts( $address ); |
| 304 | if ( $parts === null || $parts === [] ) { return [ '', null ]; } |
| 305 | $index = array_pop( $parts ); |
| 306 | return [ implode( '/', $parts ), $index ]; |
| 307 | } |
| 308 | |
| 309 | public static function raw_node_at( $blocks, $address ) { |
| 310 | $parts = self::raw_address_parts( $address ); |
| 311 | if ( $parts === null ) { return null; } |
| 312 | $nodes = (array) $blocks; $found = null; |
| 313 | foreach ( $parts as $i ) { |
| 314 | if ( ! array_key_exists( $i, $nodes ) ) { return null; } |
| 315 | $found = $nodes[ $i ]; |
| 316 | $nodes = isset( $found['innerBlocks'] ) && is_array( $found['innerBlocks'] ) ? $found['innerBlocks'] : []; |
| 317 | } |
| 318 | return $found; |
| 319 | } |
| 320 | |
| 321 | /* |
| 322 | * The document root is not a block and has no innerContent. Wrapping it in a |
| 323 | * synthetic parent lets every address -- root included -- share one splice |
| 324 | * path; the synthetic innerContent is discarded on unwrap. |
| 325 | */ |
| 326 | private static function raw_wrap_root( $blocks ) { |
| 327 | $blocks = array_values( (array) $blocks ); |
| 328 | return [ 'blockName' => null, 'attrs' => [], 'innerBlocks' => $blocks, 'innerHTML' => '', 'innerContent' => array_fill( 0, count( $blocks ), null ) ]; |
| 329 | } |
| 330 | private static function raw_unwrap_root( $root ) { |
| 331 | return ( is_array( $root ) && isset( $root['innerBlocks'] ) && is_array( $root['innerBlocks'] ) ) ? array_values( $root['innerBlocks'] ) : []; |
| 332 | } |
| 333 | private static function raw_apply_at( $block, $parts, $fn ) { |
| 334 | if ( $parts === [] ) { return call_user_func( $fn, $block ); } |
| 335 | $i = array_shift( $parts ); |
| 336 | $inner = isset( $block['innerBlocks'] ) && is_array( $block['innerBlocks'] ) ? $block['innerBlocks'] : []; |
| 337 | if ( ! array_key_exists( $i, $inner ) ) { return null; } |
| 338 | $updated = self::raw_apply_at( $inner[ $i ], $parts, $fn ); |
| 339 | if ( $updated === null ) { return null; } |
| 340 | $inner[ $i ] = $updated; |
| 341 | $block['innerBlocks'] = $inner; |
| 342 | return $block; |
| 343 | } |
| 344 | |
| 345 | private static function raw_placeholder_offsets( $inner_content ) { |
| 346 | $pos = []; |
| 347 | foreach ( (array) $inner_content as $i => $chunk ) { if ( ! is_string( $chunk ) ) { $pos[] = $i; } } |
| 348 | return $pos; |
| 349 | } |
| 350 | |
| 351 | /** |
| 352 | * A container with no children has no placeholder to insert against and its |
| 353 | * wrapper is one opaque literal. Split it just before its final closing tag |
| 354 | * so the first child lands inside. null = cannot open safely; caller refuses. |
| 355 | */ |
| 356 | private static function raw_open_empty_container( $inner_content ) { |
| 357 | $joined = ''; |
| 358 | foreach ( (array) $inner_content as $chunk ) { if ( is_string( $chunk ) ) { $joined .= $chunk; } } |
| 359 | if ( $joined === '' ) { return [ null ]; } |
| 360 | if ( trim( $joined ) === '' ) { return [ $joined, null ]; } |
| 361 | if ( ! preg_match( '~</[A-Za-z][A-Za-z0-9:-]*>\s*$~', $joined, $m, PREG_OFFSET_CAPTURE ) ) { return null; } |
| 362 | $at = (int) $m[0][1]; |
| 363 | return [ substr( $joined, 0, $at ), null, substr( $joined, $at ) ]; |
| 364 | } |
| 365 | |
| 366 | private static function raw_splice_children( $parent, $k, $new ) { |
| 367 | $new = array_values( (array) $new ); |
| 368 | if ( $new === [] ) { return $parent; } |
| 369 | $inner = isset( $parent['innerBlocks'] ) && is_array( $parent['innerBlocks'] ) ? $parent['innerBlocks'] : []; |
| 370 | $ic = isset( $parent['innerContent'] ) && is_array( $parent['innerContent'] ) ? array_values( $parent['innerContent'] ) : []; |
| 371 | $k = max( 0, min( (int) $k, count( $inner ) ) ); |
| 372 | $slots = self::raw_placeholder_offsets( $ic ); |
| 373 | if ( $slots === [] ) { |
| 374 | $opened = self::raw_open_empty_container( $ic ); |
| 375 | if ( $opened === null ) { return null; } |
| 376 | $ic = $opened; |
| 377 | $s = self::raw_placeholder_offsets( $ic ); |
| 378 | $at = $s[0]; |
| 379 | array_splice( $ic, $at, 1 ); |
| 380 | } else { |
| 381 | $at = ( $k < count( $slots ) ) ? $slots[ $k ] : ( $slots[ count( $slots ) - 1 ] + 1 ); |
| 382 | } |
| 383 | array_splice( $inner, $k, 0, $new ); |
| 384 | array_splice( $ic, $at, 0, array_fill( 0, count( $new ), null ) ); |
| 385 | $parent['innerBlocks'] = $inner; |
| 386 | $parent['innerContent'] = $ic; |
| 387 | return $parent; |
| 388 | } |
| 389 | |
| 390 | private static function raw_unsplice_child( $parent, $k ) { |
| 391 | $inner = isset( $parent['innerBlocks'] ) && is_array( $parent['innerBlocks'] ) ? $parent['innerBlocks'] : []; |
| 392 | $ic = isset( $parent['innerContent'] ) && is_array( $parent['innerContent'] ) ? array_values( $parent['innerContent'] ) : []; |
| 393 | if ( ! array_key_exists( $k, $inner ) ) { return null; } |
| 394 | $slots = self::raw_placeholder_offsets( $ic ); |
| 395 | array_splice( $inner, $k, 1 ); |
| 396 | if ( isset( $slots[ $k ] ) ) { array_splice( $ic, $slots[ $k ], 1 ); } |
| 397 | $parent['innerBlocks'] = $inner; |
| 398 | $parent['innerContent'] = $ic; |
| 399 | return $parent; |
| 400 | } |
| 401 | |
| 402 | public static function raw_replace_at( $blocks, $address, $new_block ) { |
| 403 | list( $parent, $index ) = self::raw_split_address( $address ); |
| 404 | if ( $index === null ) { return null; } |
| 405 | $parts = self::raw_address_parts( $parent ); |
| 406 | if ( $parts === null ) { return null; } |
| 407 | $root = self::raw_apply_at( self::raw_wrap_root( $blocks ), $parts, function( $p ) use ( $index, $new_block ) { |
| 408 | $inner = isset( $p['innerBlocks'] ) && is_array( $p['innerBlocks'] ) ? $p['innerBlocks'] : []; |
| 409 | if ( ! array_key_exists( $index, $inner ) ) { return null; } |
| 410 | $inner[ $index ] = $new_block; |
| 411 | $p['innerBlocks'] = $inner; |
| 412 | return $p; |
| 413 | } ); |
| 414 | return $root === null ? null : self::raw_unwrap_root( $root ); |
| 415 | } |
| 416 | |
| 417 | public static function raw_remove_at( $blocks, $address ) { |
| 418 | list( $parent, $index ) = self::raw_split_address( $address ); |
| 419 | if ( $index === null ) { return null; } |
| 420 | $parts = self::raw_address_parts( $parent ); |
| 421 | if ( $parts === null ) { return null; } |
| 422 | $root = self::raw_apply_at( self::raw_wrap_root( $blocks ), $parts, function( $p ) use ( $index ) { |
| 423 | return self::raw_unsplice_child( $p, $index ); |
| 424 | } ); |
| 425 | return $root === null ? null : self::raw_unwrap_root( $root ); |
| 426 | } |
| 427 | |
| 428 | /** $new_blocks is a list of raw blocks. position null = append. */ |
| 429 | public static function raw_insert_at( $blocks, $parent_address, $position, $new_blocks ) { |
| 430 | $parts = self::raw_address_parts( $parent_address ); |
| 431 | if ( $parts === null ) { return null; } |
| 432 | $root = self::raw_apply_at( self::raw_wrap_root( $blocks ), $parts, function( $p ) use ( $position, $new_blocks ) { |
| 433 | $n = isset( $p['innerBlocks'] ) && is_array( $p['innerBlocks'] ) ? count( $p['innerBlocks'] ) : 0; |
| 434 | return self::raw_splice_children( $p, $position === null ? $n : (int) $position, $new_blocks ); |
| 435 | } ); |
| 436 | return $root === null ? null : self::raw_unwrap_root( $root ); |
| 437 | } |
| 438 | |
| 439 | /** Build a leaf raw block. Containers must come from markup, not from a name. */ |
| 440 | public static function raw_make_block( $name, $attrs = [], $html = '' ) { |
| 441 | $name = (string) $name; |
| 442 | $html = (string) $html; |
| 443 | $attrs = is_array( $attrs ) ? $attrs : []; |
| 444 | return [ 'blockName' => $name !== '' ? $name : null, 'attrs' => $attrs, 'innerBlocks' => [], 'innerHTML' => $html, 'innerContent' => $html !== '' ? [ $html ] : [] ]; |
| 445 | } |
| 446 | |
| 447 | /** Set a raw block's own inner HTML. Refuses when it would orphan children. */ |
| 448 | public static function raw_set_html( $block, $html ) { |
| 449 | $kids = isset( $block['innerBlocks'] ) && is_array( $block['innerBlocks'] ) ? count( $block['innerBlocks'] ) : 0; |
| 450 | if ( $kids > 0 ) { return null; } |
| 451 | $html = (string) $html; |
| 452 | $block['innerHTML'] = $html; |
| 453 | $block['innerContent'] = $html !== '' ? [ $html ] : []; |
| 454 | return $block; |
| 455 | } |
| 456 | |
| 457 | } |