PluginProbe
Gutenberg / 4.7.0
Gutenberg v4.7.0
24.0.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 All 403 releases
← All changes | packages/block-serialization-default-parser/parser.php +512 -6 23.2.14.7.0 View file →
@@ -1,11 +1,517 @@
1 1 <?php
2 +
2 3 /**
3 - * Block Serialization Parser
4 + * Class WP_Block_Parser_Block
4 5 *
5 - * @package WordPress
6 + * Holds the block structure in memory
7 + *
8 + * @since 3.8.0
6 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;
7 20
8 -// Require files.
9 -require_once __DIR__ . '/class-wp-block-parser-block.php';
10 -require_once __DIR__ . '/class-wp-block-parser-frame.php';
11 -require_once __DIR__ . '/class-wp-block-parser.php';
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 +}