PluginProbe
Gutenberg / 4.7.0
Gutenberg v4.7.0
23.9.1 23.9.0 23.8.0 23.7.2 23.7.1 23.7.0 23.6.1 23.6.2 23.6.0 23.5.3 23.5.2 23.5.1 23.5.0 23.4.0 23.3.2 23.3.1 23.3.0 23.2.0 23.2.1 23.2.2 23.1.1 23.1.0 23.0.1 12.6.0 7.4.0 All 402 releases
gutenberg / packages / block-serialization-default-parser / parser.php

parser.php in Gutenberg 4.7.0, at packages/block-serialization-default-parser/parser.php

518 lines 13.4 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 /**
4 * Class WP_Block_Parser_Block
5 *
6 * Holds the block structure in memory
7 *
8 * @since 3.8.0
9 */
10 class WP_Block_Parser_Block {
11 /**
12 * Name of block
13 *
14 * @example "core/paragraph"
15 *
16 * @since 3.8.0
17 * @var string
18 */
19 public $blockName;
20
21 /**
22 * Optional set of attributes from block comment delimiters
23 *
24 * @example null
25 * @example array( 'columns' => 3 )
26 *
27 * @since 3.8.0
28 * @var array|null
29 */
30 public $attrs;
31
32 /**
33 * List of inner blocks (of this same class)
34 *
35 * @since 3.8.0
36 * @var WP_Block_Parser_Block[]
37 */
38 public $innerBlocks;
39
40 /**
41 * Resultant HTML from inside block comment delimiters
42 * after removing inner blocks
43 *
44 * @example "...Just <!-- wp:test /--> testing..." -> "Just testing..."
45 *
46 * @since 3.8.0
47 * @var string
48 */
49 public $innerHTML;
50
51 /**
52 * List of string fragments and null markers where inner blocks were found
53 *
54 * @example array(
55 * 'innerHTML' => 'BeforeInnerAfter',
56 * 'innerBlocks' => array( block, block ),
57 * 'innerContent' => array( 'Before', null, 'Inner', null, 'After' ),
58 * )
59 *
60 * @since 4.2.0
61 * @var array
62 */
63 public $innerContent;
64
65 function __construct( $name, $attrs, $innerBlocks, $innerHTML, $innerContent ) {
66 $this->blockName = $name;
67 $this->attrs = $attrs;
68 $this->innerBlocks = $innerBlocks;
69 $this->innerHTML = $innerHTML;
70 $this->innerContent = $innerContent;
71 }
72 }
73
74 /**
75 * Class WP_Block_Parser_Frame
76 *
77 * Holds partial blocks in memory while parsing
78 *
79 * @internal
80 * @since 3.8.0
81 */
82 class WP_Block_Parser_Frame {
83 /**
84 * Full or partial block
85 *
86 * @since 3.8.0
87 * @var WP_Block_Parser_Block
88 */
89 public $block;
90
91 /**
92 * Byte offset into document for start of parse token
93 *
94 * @since 3.8.0
95 * @var int
96 */
97 public $token_start;
98
99 /**
100 * Byte length of entire parse token string
101 *
102 * @since 3.8.0
103 * @var int
104 */
105 public $token_length;
106
107 /**
108 * Byte offset into document for after parse token ends
109 * (used during reconstruction of stack into parse production)
110 *
111 * @since 3.8.0
112 * @var int
113 */
114 public $prev_offset;
115
116 /**
117 * Byte offset into document where leading HTML before token starts
118 *
119 * @since 3.8.0
120 * @var int
121 */
122 public $leading_html_start;
123
124 function __construct( $block, $token_start, $token_length, $prev_offset = null, $leading_html_start = null ) {
125 $this->block = $block;
126 $this->token_start = $token_start;
127 $this->token_length = $token_length;
128 $this->prev_offset = isset( $prev_offset ) ? $prev_offset : $token_start + $token_length;
129 $this->leading_html_start = $leading_html_start;
130 }
131 }
132
133 /**
134 * Class WP_Block_Parser
135 *
136 * Parses a document and constructs a list of parsed block objects
137 *
138 * @since 3.8.0
139 * @since 4.0.0 returns arrays not objects, all attributes are arrays
140 */
141 class WP_Block_Parser {
142 /**
143 * Input document being parsed
144 *
145 * @example "Pre-text\n<!-- wp:paragraph -->This is inside a block!<!-- /wp:paragraph -->"
146 *
147 * @since 3.8.0
148 * @var string
149 */
150 public $document;
151
152 /**
153 * Tracks parsing progress through document
154 *
155 * @since 3.8.0
156 * @var int
157 */
158 public $offset;
159
160 /**
161 * List of parsed blocks
162 *
163 * @since 3.8.0
164 * @var WP_Block_Parser_Block[]
165 */
166 public $output;
167
168 /**
169 * Stack of partially-parsed structures in memory during parse
170 *
171 * @since 3.8.0
172 * @var WP_Block_Parser_Frame[]
173 */
174 public $stack;
175
176 /**
177 * Empty associative array, here due to PHP quirks
178 *
179 * @since 4.4.0
180 * @var array empty associative array
181 */
182 public $empty_attrs;
183
184 /**
185 * Parses a document and returns a list of block structures
186 *
187 * When encountering an invalid parse will return a best-effort
188 * parse. In contrast to the specification parser this does not
189 * return an error on invalid inputs.
190 *
191 * @since 3.8.0
192 *
193 * @param string $document
194 * @return WP_Block_Parser_Block[]
195 */
196 function parse( $document ) {
197 $this->document = $document;
198 $this->offset = 0;
199 $this->output = array();
200 $this->stack = array();
201 $this->empty_attrs = json_decode( '{}', true );
202
203 do {
204 // twiddle our thumbs
205 } while ( $this->proceed() );
206
207 return $this->output;
208 }
209
210 /**
211 * Processes the next token from the input document
212 * and returns whether to proceed eating more tokens
213 *
214 * This is the "next step" function that essentially
215 * takes a token as its input and decides what to do
216 * with that token before descending deeper into a
217 * nested block tree or continuing along the document
218 * or breaking out of a level of nesting.
219 *
220 * @internal
221 * @since 3.8.0
222 * @return bool
223 */
224 function proceed() {
225 $next_token = $this->next_token();
226 list( $token_type, $block_name, $attrs, $start_offset, $token_length ) = $next_token;
227 $stack_depth = count( $this->stack );
228
229 // we may have some HTML soup before the next block
230 $leading_html_start = $start_offset > $this->offset ? $this->offset : null;
231
232 switch ( $token_type ) {
233 case 'no-more-tokens':
234 // if not in a block then flush output
235 if ( 0 === $stack_depth ) {
236 $this->add_freeform();
237 return false;
238 }
239
240 /*
241 * Otherwise we have a problem
242 * This is an error
243 *
244 * we have options
245 * - treat it all as freeform text
246 * - assume an implicit closer (easiest when not nesting)
247 */
248
249 // for the easy case we'll assume an implicit closer
250 if ( 1 === $stack_depth ) {
251 $this->add_block_from_stack();
252 return false;
253 }
254
255 /*
256 * for the nested case where it's more difficult we'll
257 * have to assume that multiple closers are missing
258 * and so we'll collapse the whole stack piecewise
259 */
260 while ( 0 < count( $this->stack ) ) {
261 $this->add_block_from_stack();
262 }
263 return false;
264
265 case 'void-block':
266 /*
267 * easy case is if we stumbled upon a void block
268 * in the top-level of the document
269 */
270 if ( 0 === $stack_depth ) {
271 if ( isset( $leading_html_start ) ) {
272 $this->output[] = (array) self::freeform( substr(
273 $this->document,
274 $leading_html_start,
275 $start_offset - $leading_html_start
276 ) );
277 }
278
279 $this->output[] = (array) new WP_Block_Parser_Block( $block_name, $attrs, array(), '', array() );
280 $this->offset = $start_offset + $token_length;
281 return true;
282 }
283
284 // otherwise we found an inner block
285 $this->add_inner_block(
286 new WP_Block_Parser_Block( $block_name, $attrs, array(), '', array() ),
287 $start_offset,
288 $token_length
289 );
290 $this->offset = $start_offset + $token_length;
291 return true;
292
293 case 'block-opener':
294 // track all newly-opened blocks on the stack
295 array_push( $this->stack, new WP_Block_Parser_Frame(
296 new WP_Block_Parser_Block( $block_name, $attrs, array(), '', array() ),
297 $start_offset,
298 $token_length,
299 $start_offset + $token_length,
300 $leading_html_start
301 ) );
302 $this->offset = $start_offset + $token_length;
303 return true;
304
305 case 'block-closer':
306 /*
307 * if we're missing an opener we're in trouble
308 * This is an error
309 */
310 if ( 0 === $stack_depth ) {
311 /*
312 * we have options
313 * - assume an implicit opener
314 * - assume _this_ is the opener
315 * - give up and close out the document
316 */
317 $this->add_freeform();
318 return false;
319 }
320
321 // if we're not nesting then this is easy - close the block
322 if ( 1 === $stack_depth ) {
323 $this->add_block_from_stack( $start_offset );
324 $this->offset = $start_offset + $token_length;
325 return true;
326 }
327
328 /*
329 * otherwise we're nested and we have to close out the current
330 * block and add it as a new innerBlock to the parent
331 */
332 $stack_top = array_pop( $this->stack );
333 $html = substr( $this->document, $stack_top->prev_offset, $start_offset - $stack_top->prev_offset );
334 $stack_top->block->innerHTML .= $html;
335 $stack_top->block->innerContent[] = $html;
336 $stack_top->prev_offset = $start_offset + $token_length;
337
338 $this->add_inner_block(
339 $stack_top->block,
340 $stack_top->token_start,
341 $stack_top->token_length,
342 $start_offset + $token_length
343 );
344 $this->offset = $start_offset + $token_length;
345 return true;
346
347 default:
348 // This is an error
349 $this->add_freeform();
350 return false;
351 }
352 }
353
354 /**
355 * Scans the document from where we last left off
356 * and finds the next valid token to parse if it exists
357 *
358 * Returns the type of the find: kind of find, block information, attributes
359 *
360 * @internal
361 * @since 3.8.0
362 * @since 4.6.1 fixed a bug in attribute parsing which caused catastrophic backtracking on invalid block comments
363 * @return array
364 */
365 function next_token() {
366 $matches = null;
367
368 /*
369 * aye the magic
370 * we're using a single RegExp to tokenize the block comment delimiters
371 * we're also using a trick here because the only difference between a
372 * block opener and a block closer is the leading `/` before `wp:` (and
373 * a closer has no attributes). we can trap them both and process the
374 * match back in PHP to see which one it was.
375 */
376 $has_match = preg_match(
377 '/<!--\s+(?<closer>\/)?wp:(?<namespace>[a-z][a-z0-9_-]*\/)?(?<name>[a-z][a-z0-9_-]*)\s+(?<attrs>{(?:(?:[^}]+|}+(?=})|(?!}\s+\/?-->).)*+)?}\s+)?(?<void>\/)?-->/s',
378 $this->document,
379 $matches,
380 PREG_OFFSET_CAPTURE,
381 $this->offset
382 );
383
384 // if we get here we probably have catastrophic backtracking or out-of-memory in the PCRE
385 if ( false === $has_match ) {
386 return array( 'no-more-tokens', null, null, null, null );
387 }
388
389 // we have no more tokens
390 if ( 0 === $has_match ) {
391 return array( 'no-more-tokens', null, null, null, null );
392 }
393
394 list( $match, $started_at ) = $matches[ 0 ];
395
396 $length = strlen( $match );
397 $is_closer = isset( $matches[ 'closer' ] ) && -1 !== $matches[ 'closer' ][ 1 ];
398 $is_void = isset( $matches[ 'void' ] ) && -1 !== $matches[ 'void' ][ 1 ];
399 $namespace = $matches[ 'namespace' ];
400 $namespace = ( isset( $namespace ) && -1 !== $namespace[ 1 ] ) ? $namespace[ 0 ] : 'core/';
401 $name = $namespace . $matches[ 'name' ][ 0 ];
402 $has_attrs = isset( $matches[ 'attrs' ] ) && -1 !== $matches[ 'attrs' ][ 1 ];
403
404 /*
405 * Fun fact! It's not trivial in PHP to create "an empty associative array" since all arrays
406 * are associative arrays. If we use `array()` we get a JSON `[]`
407 */
408 $attrs = $has_attrs
409 ? json_decode( $matches[ 'attrs' ][ 0 ], /* as-associative */ true )
410 : $this->empty_attrs;
411
412 /*
413 * This state isn't allowed
414 * This is an error
415 */
416 if ( $is_closer && ( $is_void || $has_attrs ) ) {
417 // we can ignore them since they don't hurt anything
418 }
419
420 if ( $is_void ) {
421 return array( 'void-block', $name, $attrs, $started_at, $length );
422 }
423
424 if ( $is_closer ) {
425 return array( 'block-closer', $name, null, $started_at, $length );
426 }
427
428 return array( 'block-opener', $name, $attrs, $started_at, $length );
429 }
430
431 /**
432 * Returns a new block object for freeform HTML
433 *
434 * @internal
435 * @since 3.9.0
436 *
437 * @param string $innerHTML HTML content of block
438 * @return WP_Block_Parser_Block freeform block object
439 */
440 function freeform( $innerHTML ) {
441 return new WP_Block_Parser_Block( null, $this->empty_attrs, array(), $innerHTML, array( $innerHTML ) );
442 }
443
444 /**
445 * Pushes a length of text from the input document
446 * to the output list as a freeform block
447 *
448 * @internal
449 * @since 3.8.0
450 * @param null $length how many bytes of document text to output
451 */
452 function add_freeform( $length = null ) {
453 $length = $length ? $length : strlen( $this->document ) - $this->offset;
454
455 if ( 0 === $length ) {
456 return;
457 }
458
459 $this->output[] = (array) self::freeform( substr( $this->document, $this->offset, $length ) );
460 }
461
462 /**
463 * Given a block structure from memory pushes
464 * a new block to the output list
465 *
466 * @internal
467 * @since 3.8.0
468 * @param WP_Block_Parser_Block $block the block to add to the output
469 * @param int $token_start byte offset into the document where the first token for the block starts
470 * @param int $token_length byte length of entire block from start of opening token to end of closing token
471 * @param int|null $last_offset last byte offset into document if continuing form earlier output
472 */
473 function add_inner_block( WP_Block_Parser_Block $block, $token_start, $token_length, $last_offset = null ) {
474 $parent = $this->stack[ count( $this->stack ) - 1 ];
475 $parent->block->innerBlocks[] = (array) $block;
476 $html = substr( $this->document, $parent->prev_offset, $token_start - $parent->prev_offset );
477
478 if ( ! empty( $html ) ) {
479 $parent->block->innerHTML .= $html;
480 $parent->block->innerContent[] = $html;
481 }
482
483 $parent->block->innerContent[] = null;
484 $parent->prev_offset = $last_offset ? $last_offset : $token_start + $token_length;
485 }
486
487 /**
488 * Pushes the top block from the parsing stack to the output list
489 *
490 * @internal
491 * @since 3.8.0
492 * @param int|null $end_offset byte offset into document for where we should stop sending text output as HTML
493 */
494 function add_block_from_stack( $end_offset = null ) {
495 $stack_top = array_pop( $this->stack );
496 $prev_offset = $stack_top->prev_offset;
497
498 $html = isset( $end_offset )
499 ? substr( $this->document, $prev_offset, $end_offset - $prev_offset )
500 : substr( $this->document, $prev_offset );
501
502 if ( ! empty( $html ) ) {
503 $stack_top->block->innerHTML .= $html;
504 $stack_top->block->innerContent[] = $html;
505 }
506
507 if ( isset( $stack_top->leading_html_start ) ) {
508 $this->output[] = (array) self::freeform( substr(
509 $this->document,
510 $stack_top->leading_html_start,
511 $stack_top->token_start - $stack_top->leading_html_start
512 ) );
513 }
514
515 $this->output[] = (array) $stack_top->block;
516 }
517 }
518