Parser::next_token()   F
last analyzed

Complexity

Conditions 14
Paths 386

Size

Total Lines 55
Code Lines 28

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 210

Importance

Changes 0
Metric Value
eloc 28
dl 0
loc 55
rs 3.1083
c 0
b 0
f 0
cc 14
nc 386
nop 0
ccs 0
cts 36
cp 0
crap 210

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
namespace neon\cms\services\cmsEditor;
4
5
6
use neon\core\helpers\Hash;
7
8
/**
9
 * Class Cmp
10
 *
11
 * Holds the block structure in memory
12
 *
13
 * @since 3.8.0
14
 */
15
class Cmp
16
{
17
	/**
18
	 * Name of block
19
	 *
20
	 * @example "core/paragraph"
21
	 *
22
	 * @since 3.8.0
23
	 * @var string
24
	 */
25
	public $cmp;
26
	/**
27
	 * Optional set of attributes from block comment delimiters
28
	 *
29
	 * @example null
30
	 * @example array( 'columns' => 3 )
31
	 *
32
	 * @since 3.8.0
33
	 * @var array|null
34
	 */
35
	public $props;
36
	/**
37
	 * List of inner blocks (of this same class)
38
	 *
39
	 * @since 3.8.0
40
	 * @var \neon\cms\components\WP_Block_Parser_Block[]
41
	 */
42
//	public $innerBlocks;
43
	/**
44
	 * Resultant HTML from inside block comment delimiters
45
	 * after removing inner blocks
46
	 *
47
	 * @example "...Just <!-- wp:test /--> testing..." -> "Just testing..."
48
	 *
49
	 * @since 3.8.0
50
	 * @var string
51
	 */
52
//	public $innerHTML;
53
	/**
54
	 * List of string fragments and null markers where inner blocks were found
55
	 *
56
	 * @example array(
57
	 *   'innerHTML'    => 'BeforeInnerAfter',
58
	 *   'innerBlocks'  => array( block, block ),
59
	 *   'innerContent' => array( 'Before', null, 'Inner', null, 'After' ),
60
	 * )
61
	 *
62
	 * @since 4.2.0
63
	 * @var array
64
	 */
65
//	public $innerContent;
66
67
	public $uuid = '';
68
69
	public $children = [];
70
71
	/**
72
	 * Constructor.
73
	 *
74
	 * Will populate object properties from the provided arguments.
75
	 *
76
	 * @param string $name Name of block.
77
	 * @param array $attrs Optional set of attributes from block comment delimiters.
78
	 * @param array $innerBlocks List of inner blocks (of this same class).
79
	 * @param string $innerHTML Resultant HTML from inside block comment delimiters after removing inner blocks.
80
	 * @param array $innerContent List of string fragments and null markers where inner blocks were found.
81
	 * @since 3.8.0
82
	 *
83
	 */
84
	function __construct($name, $attrs, $innerBlocks, $innerHTML, $innerContent, $uuid=null)
0 ignored issues
show
Unused Code introduced by
The parameter $innerContent is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

84
	function __construct($name, $attrs, $innerBlocks, $innerHTML, /** @scrutinizer ignore-unused */ $innerContent, $uuid=null)

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
Unused Code introduced by
The parameter $innerHTML is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

84
	function __construct($name, $attrs, $innerBlocks, /** @scrutinizer ignore-unused */ $innerHTML, $innerContent, $uuid=null)

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
Unused Code introduced by
The parameter $innerBlocks is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

84
	function __construct($name, $attrs, /** @scrutinizer ignore-unused */ $innerBlocks, $innerHTML, $innerContent, $uuid=null)

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
85
	{
86
		$this->cmp = $name;
87
		$this->props = $attrs;
88
		//$this->innerBlocks = $innerBlocks;
89
		//$this->innerHTML = $innerHTML;
90
		//$this->innerContent = $innerContent;
91
		if (isset($attrs['uuid']) ) {
92
			$uuid = $attrs['uuid'];
93
		}
94
		$this->uuid = $uuid ? $uuid : Hash::uuid64();
0 ignored issues
show
Documentation Bug introduced by
It seems like $uuid ? $uuid : neon\core\helpers\Hash::uuid64() can also be of type string. However, the property $uuid is declared as type array. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
95
	}
96
}
97
98
/**
99
 * Class CmpFrame
100
 *
101
 * Holds partial blocks in memory while parsing
102
 *
103
 * @internal
104
 * @since 3.8.0
105
 */
106
class CmpFrame
107
{
108
	/**
109
	 * Full or partial block
110
	 *
111
	 * @since 3.8.0
112
	 * @var CmpFrame
113
	 */
114
	public $block;
115
	/**
116
	 * Byte offset into document for start of parse token
117
	 *
118
	 * @since 3.8.0
119
	 * @var int
120
	 */
121
	public $token_start;
122
	/**
123
	 * Byte length of entire parse token string
124
	 *
125
	 * @since 3.8.0
126
	 * @var int
127
	 */
128
	public $token_length;
129
	/**
130
	 * Byte offset into document for after parse token ends
131
	 * (used during reconstruction of stack into parse production)
132
	 *
133
	 * @since 3.8.0
134
	 * @var int
135
	 */
136
	public $prev_offset;
137
	/**
138
	 * Byte offset into document where leading HTML before token starts
139
	 *
140
	 * @since 3.8.0
141
	 * @var int
142
	 */
143
	public $leading_html_start;
144
145
	/**
146
	 * Constructor
147
	 *
148
	 * Will populate object properties from the provided arguments.
149
	 *
150
	 * @param Cmp $block Full or partial block.
151
	 * @param int $token_start Byte offset into document for start of parse token.
152
	 * @param int $token_length Byte length of entire parse token string.
153
	 * @param int $prev_offset Byte offset into document for after parse token ends.
154
	 * @param int $leading_html_start Byte offset into document where leading HTML before token starts.
155
	 * @since 3.8.0
156
	 *
157
	 */
158
	function __construct($block, $token_start, $token_length, $prev_offset = null, $leading_html_start = null)
159
	{
160
		$this->block = $block;
0 ignored issues
show
Documentation Bug introduced by
It seems like $block of type neon\cms\services\cmsEditor\Cmp is incompatible with the declared type neon\cms\services\cmsEditor\CmpFrame of property $block.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
161
		$this->token_start = $token_start;
162
		$this->token_length = $token_length;
163
		$this->prev_offset = isset($prev_offset) ? $prev_offset : $token_start + $token_length;
164
		$this->leading_html_start = $leading_html_start;
165
	}
166
}
167
168
169
170
/**
171
 * Class Parser
172
 *
173
 * Parses a document and constructs a list of parsed block objects
174
 *
175
 * @since 3.8.0
176
 * @since 4.0.0 returns arrays not objects, all attributes are arrays
177
 */
178
class Parser
179
{
180
	/**
181
	 * Input document being parsed
182
	 *
183
	 * @example "Pre-text\n<!-- wp:paragraph -->This is inside a block!<!-- /wp:paragraph -->"
184
	 *
185
	 * @since 3.8.0
186
	 * @var string
187
	 */
188
	public $document;
189
	/**
190
	 * Tracks parsing progress through document
191
	 *
192
	 * @since 3.8.0
193
	 * @var int
194
	 */
195
	public $offset;
196
	/**
197
	 * List of parsed blocks
198
	 *
199
	 * @since 3.8.0
200
	 * @var Cmp[]
201
	 */
202
	public $output;
203
	/**
204
	 * Stack of partially-parsed structures in memory during parse
205
	 *
206
	 * @since 3.8.0
207
	 * @var CmpFrame[]
208
	 */
209
	public $stack;
210
	/**
211
	 * Empty associative array, here due to PHP quirks
212
	 *
213
	 * @since 4.4.0
214
	 * @var array empty associative array
215
	 */
216
	public $empty_attrs;
217
218
	public $componentNamespace = 'ni:';
219
220
	/**
221
	 * Parses a document and returns a list of block structures
222
	 *
223
	 * When encountering an invalid parse will return a best-effort
224
	 * parse. In contrast to the specification parser this does not
225
	 * return an error on invalid inputs.
226
	 *
227
	 * @param string $document Input document being parsed.
228
	 * @return Cmp[]
229
	 * @since 3.8.0
230
	 *
231
	 */
232
	function parse($document)
233
	{
234
		$this->document = $document;
235
		$this->offset = 0;
236
		$this->output = array();
237
		$this->stack = array();
238
		$this->empty_attrs = json_decode('{}', true);
239
		do {
240
			// twiddle our thumbs.
241
		} while ($this->proceed());
242
		return $this->output;
243
	}
244
245
	/**
246
	 * Processes the next token from the input document
247
	 * and returns whether to proceed eating more tokens
248
	 *
249
	 * This is the "next step" function that essentially
250
	 * takes a token as its input and decides what to do
251
	 * with that token before descending deeper into a
252
	 * nested block tree or continuing along the document
253
	 * or breaking out of a level of nesting.
254
	 *
255
	 * @return bool
256
	 * @since 3.8.0
257
	 * @internal
258
	 */
259
	function proceed()
0 ignored issues
show
Best Practice introduced by
It is generally recommended to explicitly declare the visibility for methods.

Adding explicit visibility (private, protected, or public) is generally recommend to communicate to other developers how, and from where this method is intended to be used.

Loading history...
260
	{
261
		$next_token = $this->next_token();
262
		list($token_type, $block_name, $attrs, $start_offset, $token_length) = $next_token;
263
		$stack_depth = count($this->stack);
264
		// we may have some HTML soup before the next block.
265
		$leading_html_start = $start_offset > $this->offset ? $this->offset : null;
266
		switch ($token_type) {
267
			case 'no-more-tokens':
268
				// if not in a block then flush output.
269
				if (0 === $stack_depth) {
270
					$this->add_freeform();
271
					return false;
272
				}
273
				/*
274
				 * Otherwise we have a problem
275
				 * This is an error
276
				 *
277
				 * we have options
278
				 * - treat it all as freeform text
279
				 * - assume an implicit closer (easiest when not nesting)
280
				 */
281
				// for the easy case we'll assume an implicit closer.
282
				if (1 === $stack_depth) {
283
					$this->add_block_from_stack();
284
					return false;
285
				}
286
				/*
287
				 * for the nested case where it's more difficult we'll
288
				 * have to assume that multiple closers are missing
289
				 * and so we'll collapse the whole stack piecewise
290
				 */
291
				while (0 < count($this->stack)) {
292
					$this->add_block_from_stack();
293
				}
294
				return false;
295
			case 'void-block':
296
				/*
297
				 * easy case is if we stumbled upon a void block
298
				 * in the top-level of the document
299
				 */
300
				if (0 === $stack_depth) {
301
					if (isset($leading_html_start)) {
302
						$uuid = Hash::uuid64();
303
						$this->output[$uuid] = (array)self::freeform(
0 ignored issues
show
Bug Best Practice introduced by
The method neon\cms\services\cmsEditor\Parser::freeform() is not static, but was called statically. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

303
						$this->output[$uuid] = (array)self::/** @scrutinizer ignore-call */ freeform(
Loading history...
304
							substr(
305
								$this->document,
306
								$leading_html_start,
307
								$start_offset - $leading_html_start
308
							), $uuid
309
						);
310
					}
311
					$cmp = (array)new Cmp($block_name, $attrs, array(), '', array(), $uuid);
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $uuid does not seem to be defined for all execution paths leading up to this point.
Loading history...
312
					$this->output[$cmp->uuid] = $cmp;
313
					$this->offset = $start_offset + $token_length;
314
					return true;
315
				}
316
				// otherwise we found an inner block.
317
				$this->add_inner_block(
318
					new Cmp($block_name, $attrs, array(), '', array()),
319
					$start_offset,
320
					$token_length
321
				);
322
				$this->offset = $start_offset + $token_length;
323
				return true;
324
			case 'block-opener':
325
				// track all newly-opened blocks on the stack.
326
				array_push(
327
					$this->stack,
328
					new CmpFrame(
329
						new Cmp($block_name, $attrs, array(), '', array()),
330
						$start_offset,
331
						$token_length,
332
						$start_offset + $token_length,
333
						$leading_html_start
334
					)
335
				);
336
				$this->offset = $start_offset + $token_length;
337
				return true;
338
			case 'block-closer':
339
				/*
340
				 * if we're missing an opener we're in trouble
341
				 * This is an error
342
				 */
343
				if (0 === $stack_depth) {
344
					/*
345
					 * we have options
346
					 * - assume an implicit opener
347
					 * - assume _this_ is the opener
348
					 * - give up and close out the document
349
					 */
350
					$this->add_freeform();
351
					return false;
352
				}
353
				// if we're not nesting then this is easy - close the block.
354
				if (1 === $stack_depth) {
355
					$this->add_block_from_stack($start_offset);
356
					$this->offset = $start_offset + $token_length;
357
					return true;
358
				}
359
				/*
360
				 * otherwise we're nested and we have to close out the current
361
				 * block and add it as a new innerBlock to the parent
362
				 */
363
				$stack_top = array_pop($this->stack);
364
				$html = substr($this->document, $stack_top->prev_offset, $start_offset - $stack_top->prev_offset);
0 ignored issues
show
Unused Code introduced by
The assignment to $html is dead and can be removed.
Loading history...
365
//				$stack_top->block->innerHTML .= $html;
366
//				$stack_top->block->innerContent[] = $html;
367
				$stack_top->prev_offset = $start_offset + $token_length;
368
				$this->add_inner_block(
369
					$stack_top->block,
370
					$stack_top->token_start,
371
					$stack_top->token_length,
372
					$start_offset + $token_length
373
				);
374
				$this->offset = $start_offset + $token_length;
375
				return true;
376
			default:
377
				// This is an error.
378
				$this->add_freeform();
379
				return false;
380
		}
381
	}
382
383
	/**
384
	 * Scans the document from where we last left off
385
	 * and finds the next valid token to parse if it exists
386
	 *
387
	 * Returns the type of the find: kind of find, block information, attributes
388
	 *
389
	 * @return array
390
	 * @since 3.8.0
391
	 * @since 4.6.1 fixed a bug in attribute parsing which caused catastrophic backtracking on invalid block comments
392
	 * @internal
393
	 */
394
	function next_token()
0 ignored issues
show
Best Practice introduced by
It is generally recommended to explicitly declare the visibility for methods.

Adding explicit visibility (private, protected, or public) is generally recommend to communicate to other developers how, and from where this method is intended to be used.

Loading history...
395
	{
396
		$matches = null;
397
		/*
398
		 * aye the magic
399
		 * we're using a single RegExp to tokenize the block comment delimiters
400
		 * we're also using a trick here because the only difference between a
401
		 * block opener and a block closer is the leading `/` before `wp:` (and
402
		 * a closer has no attributes). we can trap them both and process the
403
		 * match back in PHP to see which one it was.
404
		 */
405
		$has_match = preg_match(
406
			'/<\s+(?P<closer>\/)?'.$this->componentNamespace.'(?P<namespace>[a-z][a-z0-9_-]*\/)?(?P<name>[a-z][a-z0-9_-]*)\s+(?P<attrs>{(?:(?:[^}]+|}+(?=})|(?!}\s+\/?>).)*+)?}\s+)?(?P<void>\/)?>/s',
407
			$this->document,
408
			$matches,
409
			PREG_OFFSET_CAPTURE,
410
			$this->offset
411
		);
412
		// if we get here we probably have catastrophic backtracking or out-of-memory in the PCRE.
413
		if (false === $has_match) {
414
			return array('no-more-tokens', null, null, null, null);
415
		}
416
		// we have no more tokens.
417
		if (0 === $has_match) {
418
			return array('no-more-tokens', null, null, null, null);
419
		}
420
		list($match, $started_at) = $matches[0];
421
		$length = strlen($match);
422
		$is_closer = isset($matches['closer']) && -1 !== $matches['closer'][1];
423
		$is_void = isset($matches['void']) && -1 !== $matches['void'][1];
424
		$namespace = $matches['namespace'];
425
		$namespace = (isset($namespace) && -1 !== $namespace[1]) ? $namespace[0] : ''; // 'core/';
426
		$name = $namespace . $matches['name'][0];
427
		$has_attrs = isset($matches['attrs']) && -1 !== $matches['attrs'][1];
428
		/*
429
		 * Fun fact! It's not trivial in PHP to create "an empty associative array" since all arrays
430
		 * are associative arrays. If we use `array()` we get a JSON `[]`
431
		 */
432
		$attrs = $has_attrs
433
			? json_decode($matches['attrs'][0], /* as-associative */ true)
434
			: $this->empty_attrs;
435
		/*
436
		 * This state isn't allowed
437
		 * This is an error
438
		 */
439
		if ($is_closer && ($is_void || $has_attrs)) {
440
			// we can ignore them since they don't hurt anything.
441
		}
442
		if ($is_void) {
443
			return array('void-block', $name, $attrs, $started_at, $length);
444
		}
445
		if ($is_closer) {
446
			return array('block-closer', $name, null, $started_at, $length);
447
		}
448
		return array('block-opener', $name, $attrs, $started_at, $length);
449
	}
450
451
	/**
452
	 * Returns a new block object for freeform HTML
453
	 *
454
	 * @param string $innerHTML HTML content of block.
455
	 * @return Cmp freeform block object.
456
	 * @internal
457
	 * @since 3.9.0
458
	 *
459
	 */
460
	function freeform($innerHTML, $uuid)
0 ignored issues
show
Best Practice introduced by
It is generally recommended to explicitly declare the visibility for methods.

Adding explicit visibility (private, protected, or public) is generally recommend to communicate to other developers how, and from where this method is intended to be used.

Loading history...
461
	{
462
		return new Cmp(null, $this->empty_attrs, array(), $innerHTML, array($innerHTML), $uuid);
463
	}
464
465
	/**
466
	 * Pushes a length of text from the input document
467
	 * to the output list as a freeform block.
468
	 *
469
	 * @param null $length how many bytes of document text to output.
0 ignored issues
show
Documentation Bug introduced by
Are you sure the doc-type for parameter $length is correct as it would always require null to be passed?
Loading history...
470
	 * @since 3.8.0
471
	 * @internal
472
	 */
473
	function add_freeform($length = null)
0 ignored issues
show
Best Practice introduced by
It is generally recommended to explicitly declare the visibility for methods.

Adding explicit visibility (private, protected, or public) is generally recommend to communicate to other developers how, and from where this method is intended to be used.

Loading history...
474
	{
475
		$length = $length ? $length : strlen($this->document) - $this->offset;
0 ignored issues
show
introduced by
$length is of type null, thus it always evaluated to false.
Loading history...
476
		if (0 === $length) {
477
			return;
478
		}
479
		$uuid = Hash::uuid64();
480
		$this->output[$uuid] = (array)self::freeform(substr($this->document, $this->offset, $length), $uuid);
0 ignored issues
show
Bug Best Practice introduced by
The method neon\cms\services\cmsEditor\Parser::freeform() is not static, but was called statically. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

480
		$this->output[$uuid] = (array)self::/** @scrutinizer ignore-call */ freeform(substr($this->document, $this->offset, $length), $uuid);
Loading history...
481
	}
482
483
	/**
484
	 * Given a block structure from memory pushes
485
	 * a new block to the output list.
486
	 *
487
	 * @param Cmp $block The block to add to the output.
488
	 * @param int $token_start Byte offset into the document where the first token for the block starts.
489
	 * @param int $token_length Byte length of entire block from start of opening token to end of closing token.
490
	 * @param int|null $last_offset Last byte offset into document if continuing form earlier output.
491
	 * @internal
492
	 * @since 3.8.0
493
	 */
494
	function add_inner_block(Cmp $block, $token_start, $token_length, $last_offset = null)
0 ignored issues
show
Best Practice introduced by
It is generally recommended to explicitly declare the visibility for methods.

Adding explicit visibility (private, protected, or public) is generally recommend to communicate to other developers how, and from where this method is intended to be used.

Loading history...
495
	{
496
		$parent = $this->stack[count($this->stack) - 1];
497
//		$parent->block->innerBlocks[] = (array)$block;
498
		$html = substr($this->document, $parent->prev_offset, $token_start - $parent->prev_offset);
499
		if (!empty($html)) {
500
//			$parent->block->innerHTML .= $html;
501
//			$parent->block->innerContent[] = $html;
502
		}
503
		$parent->block->innerContent[] = null;
0 ignored issues
show
Bug introduced by
The property innerContent does not seem to exist on neon\cms\services\cmsEditor\CmpFrame.
Loading history...
504
		$parent->prev_offset = $last_offset ? $last_offset : $token_start + $token_length;
505
		$parent->block->children[] = $block->uuid;
0 ignored issues
show
Bug introduced by
The property children does not seem to exist on neon\cms\services\cmsEditor\CmpFrame.
Loading history...
506
		$this->output[$block->uuid] = $block;
507
	}
508
509
	/**
510
	 * Pushes the top block from the parsing stack to the output list.
511
	 *
512
	 * @param int|null $end_offset byte offset into document for where we should stop sending text output as HTML.
513
	 * @since 3.8.0
514
	 * @internal
515
	 */
516
	function add_block_from_stack($end_offset = null)
0 ignored issues
show
Best Practice introduced by
It is generally recommended to explicitly declare the visibility for methods.

Adding explicit visibility (private, protected, or public) is generally recommend to communicate to other developers how, and from where this method is intended to be used.

Loading history...
517
	{
518
		$stack_top = array_pop($this->stack);
519
		$prev_offset = $stack_top->prev_offset;
520
		$html = isset($end_offset)
521
			? substr($this->document, $prev_offset, $end_offset - $prev_offset)
522
			: substr($this->document, $prev_offset);
523
		if (!empty($html)) {
524
//			$stack_top->block->innerHTML .= $html;
525
//			$stack_top->block->innerContent[] = $html;
526
		}
527
		if (isset($stack_top->leading_html_start)) {
528
			$uuid = Hash::uuid64();
529
			$this->output[$uuid] = (array)self::freeform(
0 ignored issues
show
Bug Best Practice introduced by
The method neon\cms\services\cmsEditor\Parser::freeform() is not static, but was called statically. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

529
			$this->output[$uuid] = (array)self::/** @scrutinizer ignore-call */ freeform(
Loading history...
530
				substr(
531
					$this->document,
532
					$stack_top->leading_html_start,
533
					$stack_top->token_start - $stack_top->leading_html_start
534
				), $uuid
535
			);
536
		}
537
		$this->output[$stack_top->block->uuid] = (array)$stack_top->block;
538
	}
539
}