Completed
Push — optimizations ( f88e62 )
by Colin
04:49
created

Cursor::advanceBy()   C

Complexity

Conditions 11
Paths 28

Size

Total Lines 54
Code Lines 37

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 39
CRAP Score 11

Importance

Changes 4
Bugs 0 Features 0
Metric Value
c 4
b 0
f 0
dl 0
loc 54
ccs 39
cts 39
cp 1
rs 6.6153
cc 11
eloc 37
nc 28
nop 2
crap 11

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
/*
4
 * This file is part of the league/commonmark package.
5
 *
6
 * (c) Colin O'Dell <[email protected]>
7
 *
8
 * For the full copyright and license information, please view the LICENSE
9
 * file that was distributed with this source code.
10
 */
11
12
namespace League\CommonMark;
13
14
class Cursor
15
{
16
    const INDENT_LEVEL = 4;
17
18
    /**
19
     * @var string
20
     */
21
    private $line;
22
23
    /**
24
     * @var int
25
     */
26
    private $length;
27
28
    /**
29
     * @var int
30
     *
31
     * It's possible for this to be 1 char past the end, meaning we've parsed all chars and have
32
     * reached the end.  In this state, any character-returning method MUST return null.
33
     */
34
    private $currentPosition = 0;
35
36
    /**
37
     * @var int
38
     */
39
    private $column = 0;
40
41
    /**
42
     * @var int
43
     */
44
    private $indent = 0;
45
46
    /**
47
     * @var int
48
     */
49
    private $previousPosition = 0;
50
51
    /**
52
     * @var int|null
53
     */
54
    private $nextNonSpaceCache;
55
56
    /**
57
     * @var bool
58
     */
59
    private $partiallyConsumedTab = false;
60
61
    /**
62
     * @param string $line
63
     */
64 2430
    public function __construct($line)
65
    {
66 2430
        $this->line = $line;
67 2430
        $this->length = mb_strlen($line, 'utf-8');
68 2430
    }
69
70
    /**
71
     * Returns the position of the next character which is not a space (or tab)
72
     *
73
     * @deprecated Use getNextNonSpacePosition() instead
74
     *
75
     * @return int
76
     */
77 16
    public function getFirstNonSpacePosition()
78
    {
79 16
        @trigger_error('Cursor::getFirstNonSpacePosition() will be removed in a future 0.x release.  Use getNextNonSpacePosition() instead. See https://github.com/thephpleague/commonmark/issues/280', E_USER_DEPRECATED);
1 ignored issue
show
Security Best Practice introduced by Colin O'Dell
It seems like you do not handle an error condition here. This can introduce security issues, and is generally not recommended.

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
80
81 16
        return $this->getNextNonSpacePosition();
82
    }
83
84
    /**
85
     * Returns the position of the next character which is not a space (or tab)
86
     *
87
     * @return int
88
     */
89 2112
    public function getNextNonSpacePosition()
90
    {
91 2112
        if ($this->nextNonSpaceCache !== null) {
92 1980
            return $this->nextNonSpaceCache;
93
        }
94
95 2112
        $i = $this->currentPosition;
96 2112
        $cols = $this->column;
97
98 2112
        while (($c = $this->getCharacter($i)) !== null) {
99 2088
            if ($c === ' ') {
100 504
                $i++;
101 504
                $cols++;
102 2074
            } elseif ($c === "\t") {
103 36
                $i++;
104 36
                $cols += (4 - ($cols % 4));
105 24
            } else {
106 2046
                break;
107
            }
108 350
        }
109
110 2112
        $nextNonSpace = ($c === null) ? $this->length : $i;
111 2112
        $this->indent = $cols - $this->column;
112
113 2112
        return $this->nextNonSpaceCache = $nextNonSpace;
114
    }
115
116
    /**
117
     * Returns the next character which isn't a space (or tab)
118
     *
119
     * @deprecated Use getNextNonSpaceCharacter() instead
120
     *
121
     * @return string
122
     */
123 16
    public function getFirstNonSpaceCharacter()
124
    {
125 16
        @trigger_error('Cursor::getFirstNonSpaceCharacter() will be removed in a future 0.x release.  Use getNextNonSpaceCharacter() instead. See https://github.com/thephpleague/commonmark/issues/280', E_USER_DEPRECATED);
1 ignored issue
show
Security Best Practice introduced by Colin O'Dell
It seems like you do not handle an error condition here. This can introduce security issues, and is generally not recommended.

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
126
127 16
        return $this->getNextNonSpaceCharacter();
128
    }
129
130
    /**
131
     * Returns the next character which isn't a space (or tab)
132
     *
133
     * @return string
134
     */
135 1905
    public function getNextNonSpaceCharacter()
136
    {
137 1905
        return $this->getCharacter($this->getNextNonSpacePosition());
138
    }
139
140
    /**
141
     * Calculates the current indent (number of spaces after current position)
142
     *
143
     * @return int
144
     */
145 1992
    public function getIndent()
146
    {
147 1992
        $this->getNextNonSpacePosition();
148
149 1992
        return $this->indent;
150
    }
151
152
    /**
153
     * Whether the cursor is indented to INDENT_LEVEL
154
     *
155
     * @return bool
156
     */
157 1932
    public function isIndented()
158
    {
159 1932
        return $this->getIndent() >= self::INDENT_LEVEL;
160
    }
161
162
    /**
163
     * @param int|null $index
164
     *
165
     * @return string|null
166
     */
167 2205
    public function getCharacter($index = null)
168
    {
169 2205
        if ($index === null) {
170 1683
            $index = $this->currentPosition;
171 1122
        }
172
173
        // Index out-of-bounds, or we're at the end
174 2205
        if ($index < 0 || $index >= $this->length) {
175 1887
            return;
176
        }
177
178 2163
        return mb_substr($this->line, $index, 1, 'utf-8');
179
    }
180
181
    /**
182
     * Returns the next character (or null, if none) without advancing forwards
183
     *
184
     * @param int $offset
185
     *
186
     * @return string|null
187
     */
188 1014
    public function peek($offset = 1)
189
    {
190 1014
        return $this->getCharacter($this->currentPosition + $offset);
191
    }
192
193
    /**
194
     * Whether the remainder is blank
195
     *
196
     * @return bool
197
     */
198 1950
    public function isBlank()
199
    {
200 1950
        return $this->getNextNonSpacePosition() === $this->length;
201
    }
202
203
    /**
204
     * Move the cursor forwards
205
     */
206 789
    public function advance()
207
    {
208 789
        $this->advanceBy(1);
209 789
    }
210
211
    /**
212
     * Move the cursor forwards
213
     *
214
     * @param int  $characters       Number of characters to advance by
215
     * @param bool $advanceByColumns Whether to advance by columns instead of spaces
216
     */
217 2313
    public function advanceBy($characters, $advanceByColumns = false)
218
    {
219 2313
        if ($characters === 0) {
220 1986
            return;
221
        }
222
223 2151
        $this->previousPosition = $this->currentPosition;
224 2151
        $this->nextNonSpaceCache = null;
225
226 2151
        $nextFewChars = mb_substr($this->line, $this->currentPosition, $characters, 'utf-8');
227
228
        // Optimization to avoid tab handling logic if we have no tabs
229 2151
        if (preg_match('/\t/', $nextFewChars) === 0) {
230 2139
            $length = min($characters, $this->length - $this->currentPosition);
231 2139
            $this->partiallyConsumedTab = false;
232 2139
            $this->currentPosition += $length;
233 2139
            $this->column += $length;
234
235 2139
            return;
236
        }
237
238 45
        if ($characters === 1 && !empty($nextFewChars)) {
239 18
            $asArray = [$nextFewChars];
240 12
        } else {
241 39
            $asArray = preg_split('//u', $nextFewChars, null, PREG_SPLIT_NO_EMPTY);
242
        }
243
244 45
        foreach ($asArray as $relPos => $c) {
245 45
            if ($c === "\t") {
246 45
                $charsToTab = 4 - ($this->column % 4);
247 45
                if ($advanceByColumns) {
248 33
                    $this->partiallyConsumedTab = $charsToTab > $characters;
249 33
                    $charsToAdvance = $charsToTab > $characters ? $characters : $charsToTab;
250 33
                    $this->column += $charsToAdvance;
251 33
                    $this->currentPosition += $this->partiallyConsumedTab ? 0 : 1;
252 33
                    $characters -= $charsToAdvance;
253 22
                } else {
254 18
                    $this->partiallyConsumedTab = false;
255 18
                    $this->column += $charsToTab;
256 18
                    $this->currentPosition++;
257 27
                    $characters--;
258
                }
259 30
            } else {
260 12
                $this->partiallyConsumedTab = false;
261 12
                $this->currentPosition++;
262 12
                $this->column++;
263 12
                $characters--;
264
            }
265
266 45
            if ($characters <= 0) {
267 45
                break;
268
            }
269 30
        }
270 45
    }
271
272
    /**
273
     * Advances the cursor by a single space or tab, if present
274
     *
275
     * @return bool
276
     */
277 333
    public function advanceBySpaceOrTab()
278
    {
279 333
        $character = $this->getCharacter();
280
281 333
        if ($character === ' ' || $character === "\t") {
282 321
            $this->advanceBy(1, true);
283
284 321
            return true;
285
        }
286
287 249
        return false;
288
    }
289
290
    /**
291
     * Advances the cursor while the given character is matched
292
     *
293
     * @param string   $character                  Character to match
294
     * @param int|null $maximumCharactersToAdvance Maximum number of characters to advance before giving up
295
     *
296
     * @return int Number of positions moved (0 if unsuccessful)
297
     */
298 141
    public function advanceWhileMatches($character, $maximumCharactersToAdvance = null)
299
    {
300
        // Calculate how far to advance
301 141
        $start = $this->currentPosition;
302 141
        $newIndex = $start;
303 141
        if ($maximumCharactersToAdvance === null) {
304 18
            $maximumCharactersToAdvance = $this->length;
305 12
        }
306
307 141
        $max = min($start + $maximumCharactersToAdvance, $this->length);
308
309 141
        while ($newIndex < $max && $this->getCharacter($newIndex) === $character) {
310 45
            ++$newIndex;
311 30
        }
312
313 141
        if ($newIndex <= $start) {
314 105
            return 0;
315
        }
316
317 45
        $this->advanceBy($newIndex - $start);
318
319 45
        return $this->currentPosition - $this->previousPosition;
320
    }
321
322
    /**
323
     * Parse zero or more space characters, including at most one newline.
324
     *
325
     * @deprecated Use advanceToNextNonSpaceOrNewline() instead
326
     */
327 36
    public function advanceToFirstNonSpace()
328
    {
329 36
        @trigger_error('Cursor::advanceToFirstNonSpace() will be removed in a future 0.x release.  Use advanceToNextNonSpaceOrTab() or advanceToNextNonSpaceOrNewline() instead. See https://github.com/thephpleague/commonmark/issues/280', E_USER_DEPRECATED);
1 ignored issue
show
Security Best Practice introduced by Colin O'Dell
It seems like you do not handle an error condition here. This can introduce security issues, and is generally not recommended.

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
330
331 36
        return $this->advanceToNextNonSpaceOrNewline();
332
    }
333
334
    /**
335
     * Parse zero or more space/tab characters
336
     *
337
     * @return int Number of positions moved
338
     */
339 1944
    public function advanceToNextNonSpaceOrTab()
340
    {
341 1944
        $newPosition = $this->getNextNonSpacePosition();
342 1944
        $this->advanceBy($newPosition - $this->currentPosition);
343 1944
        $this->partiallyConsumedTab = false;
344
345 1944
        return $this->currentPosition - $this->previousPosition;
346
    }
347
348
    /**
349
     * Parse zero or more space characters, including at most one newline.
350
     *
351
     * Tab characters are not parsed with this function.
352
     *
353
     * @return int Number of positions moved
354
     */
355 441
    public function advanceToNextNonSpaceOrNewline()
356
    {
357 441
        $matches = [];
358 441
        preg_match('/^ *(?:\n *)?/', $this->getRemainder(), $matches, PREG_OFFSET_CAPTURE);
359
360
        // [0][0] contains the matched text
361
        // [0][1] contains the index of that match
362 441
        $increment = $matches[0][1] + strlen($matches[0][0]);
363
364 441
        if ($increment === 0) {
365 300
            return 0;
366
        }
367
368 300
        $this->advanceBy($increment);
369
370 300
        return $this->currentPosition - $this->previousPosition;
371
    }
372
373
    /**
374
     * Move the position to the very end of the line
375
     *
376
     * @return int The number of characters moved
377
     */
378 84
    public function advanceToEnd()
379
    {
380 84
        $this->previousPosition = $this->currentPosition;
381 84
        $this->nextNonSpaceCache = null;
382
383 84
        $this->currentPosition = $this->length;
384
385 84
        return $this->currentPosition - $this->previousPosition;
386
    }
387
388
    /**
389
     * @return string
390
     */
391 2037
    public function getRemainder()
392
    {
393 2037
        if ($this->isAtEnd()) {
394 693
            return '';
395
        }
396
397 2019
        $prefix = '';
398 2019
        $position = $this->currentPosition;
399 2019
        if ($this->partiallyConsumedTab) {
400 12
            $position++;
401 12
            $charsToTab = 4 - ($this->column % 4);
402 12
            $prefix = str_repeat(' ', $charsToTab);
403 8
        }
404
405 2019
        return $prefix . mb_substr($this->line, $position, null, 'utf-8');
406
    }
407
408
    /**
409
     * @return string
410
     */
411 1887
    public function getLine()
412
    {
413 1887
        return $this->line;
414
    }
415
416
    /**
417
     * @return bool
418
     */
419 2058
    public function isAtEnd()
420
    {
421 2058
        return $this->currentPosition >= $this->length;
422
    }
423
424
    /**
425
     * Try to match a regular expression
426
     *
427
     * Returns the matching text and advances to the end of that match
428
     *
429
     * @param string $regex
430
     *
431
     * @return string|null
432
     */
433 1902
    public function match($regex)
434
    {
435 1902
        $subject = $this->getRemainder();
436
437 1902
        $matches = [];
438 1902
        if (!preg_match($regex, $subject, $matches, PREG_OFFSET_CAPTURE)) {
439 1758
            return;
440
        }
441
442
        // PREG_OFFSET_CAPTURE always returns the byte offset, not the char offset, which is annoying
443 1776
        $offset = mb_strlen(mb_strcut($subject, 0, $matches[0][1], 'utf-8'), 'utf-8');
444
445
        // [0][0] contains the matched text
446
        // [0][1] contains the index of that match
447 1776
        $this->advanceBy($offset + mb_strlen($matches[0][0], 'utf-8'));
448
449 1776
        return $matches[0][0];
450
    }
451
452
    /**
453
     * @return CursorState
454
     */
455 1839
    public function saveState()
456
    {
457 1839
        return new CursorState(
458 1839
            $this->line,
459 1839
            $this->length,
460 1839
            $this->currentPosition,
461 1839
            $this->previousPosition,
462 1839
            $this->nextNonSpaceCache,
463 1839
            $this->indent,
464 1839
            $this->column,
465 1839
            $this->partiallyConsumedTab
466 1226
        );
467
    }
468
469
    /**
470
     * @param CursorState $state
471
     */
472 1758
    public function restoreState(CursorState $state)
473
    {
474 1758
        $this->line = $state->getLine();
475 1758
        $this->length = $state->getLength();
476 1758
        $this->currentPosition = $state->getCurrentPosition();
477 1758
        $this->previousPosition = $state->getPreviousPosition();
478 1758
        $this->nextNonSpaceCache = $state->getNextNonSpaceCache();
479 1758
        $this->column = $state->getColumn();
480 1758
        $this->indent = $state->getIndent();
481 1758
        $this->partiallyConsumedTab = $state->getPartiallyConsumedTab();
482 1758
    }
483
484
    /**
485
     * @return int
486
     */
487 630
    public function getPosition()
488
    {
489 630
        return $this->currentPosition;
490
    }
491
492
    /**
493
     * @return string
494
     */
495 870
    public function getPreviousText()
496
    {
497 870
        return mb_substr($this->line, $this->previousPosition, $this->currentPosition - $this->previousPosition, 'utf-8');
498
    }
499
500
    /**
501
     * @return int
502
     */
503 240
    public function getColumn()
504
    {
505 240
        return $this->column;
506
    }
507
}
508