Cursor   C
last analyzed

Coupling/Cohesion

Components 1
Dependencies 1

Complexity

Total Complexity 55

Size/Duplication

Total Lines 500
Duplicated Lines 0 %

Test Coverage

Coverage 100%

Importance

Changes 4
Bugs 0 Features 0
Metric Value
wmc 55
c 4
b 0
f 0
lcom 1
cbo 1
dl 0
loc 500
ccs 179
cts 179
cp 1
rs 6.8

27 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 5 1
A getFirstNonSpacePosition() 0 6 1
A getFirstNonSpaceCharacter() 0 6 1
A getNextNonSpaceCharacter() 0 4 1
A getIndent() 0 6 1
A isIndented() 0 4 1
A getCharacter() 0 13 4
A peek() 0 4 1
A isBlank() 0 4 1
A advance() 0 4 1
B getNextNonSpacePosition() 0 26 6
A advanceBySpaceOrTab() 0 12 3
B advanceWhileMatches() 0 25 5
A advanceToFirstNonSpace() 0 6 1
A advanceToNextNonSpaceOrTab() 0 8 1
A advanceToNextNonSpaceOrNewline() 0 17 2
A advanceToEnd() 0 9 1
A getRemainder() 0 16 3
A getLine() 0 4 1
A isAtEnd() 0 4 1
A match() 0 18 2
A saveState() 0 13 1
A restoreState() 0 11 1
A getPosition() 0 4 1
A getPreviousText() 0 4 1
A getColumn() 0 4 1
C advanceBy() 0 56 11

How to fix   Complexity   

Complex Class

Complex classes like Cursor often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Cursor, and based on these observations, apply Extract Interface, too.

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 8
    public function getFirstNonSpacePosition()
78
    {
79 8
        @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);
0 ignored issues
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 8
        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 8
    public function getFirstNonSpaceCharacter()
124
    {
125 8
        @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);
0 ignored issues
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 8
        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 2286
    public function advanceBy($characters, $advanceByColumns = false)
218
    {
219 2286
        if ($characters === 0) {
220 1971
            $this->previousPosition = $this->currentPosition;
221
222 1971
            return;
223
        }
224
225 2142
        $this->previousPosition = $this->currentPosition;
226 2142
        $this->nextNonSpaceCache = null;
227
228 2142
        $nextFewChars = mb_substr($this->line, $this->currentPosition, $characters, 'utf-8');
229
230
        // Optimization to avoid tab handling logic if we have no tabs
231 2142
        if (preg_match('/\t/', $nextFewChars) === 0) {
232 2130
            $length = min($characters, $this->length - $this->currentPosition);
233 2130
            $this->partiallyConsumedTab = false;
234 2130
            $this->currentPosition += $length;
235 2130
            $this->column += $length;
236
237 2130
            return;
238
        }
239
240 45
        if ($characters === 1 && !empty($nextFewChars)) {
241 18
            $asArray = [$nextFewChars];
242 12
        } else {
243 39
            $asArray = preg_split('//u', $nextFewChars, null, PREG_SPLIT_NO_EMPTY);
244
        }
245
246 45
        foreach ($asArray as $relPos => $c) {
247 45
            if ($c === "\t") {
248 45
                $charsToTab = 4 - ($this->column % 4);
249 45
                if ($advanceByColumns) {
250 33
                    $this->partiallyConsumedTab = $charsToTab > $characters;
251 33
                    $charsToAdvance = $charsToTab > $characters ? $characters : $charsToTab;
252 33
                    $this->column += $charsToAdvance;
253 33
                    $this->currentPosition += $this->partiallyConsumedTab ? 0 : 1;
254 33
                    $characters -= $charsToAdvance;
255 22
                } else {
256 18
                    $this->partiallyConsumedTab = false;
257 18
                    $this->column += $charsToTab;
258 18
                    $this->currentPosition++;
259 27
                    $characters--;
260
                }
261 30
            } else {
262 12
                $this->partiallyConsumedTab = false;
263 12
                $this->currentPosition++;
264 12
                $this->column++;
265 12
                $characters--;
266
            }
267
268 45
            if ($characters <= 0) {
269 45
                break;
270
            }
271 30
        }
272 45
    }
273
274
    /**
275
     * Advances the cursor by a single space or tab, if present
276
     *
277
     * @return bool
278
     */
279 333
    public function advanceBySpaceOrTab()
280
    {
281 333
        $character = $this->getCharacter();
282
283 333
        if ($character === ' ' || $character === "\t") {
284 321
            $this->advanceBy(1, true);
285
286 321
            return true;
287
        }
288
289 249
        return false;
290
    }
291
292
    /**
293
     * Advances the cursor while the given character is matched
294
     *
295
     * @param string   $character                  Character to match
296
     * @param int|null $maximumCharactersToAdvance Maximum number of characters to advance before giving up
297
     *
298
     * @return int Number of positions moved (0 if unsuccessful)
299
     *
300
     * @deprecated Use match() instead
301
     */
302 15
    public function advanceWhileMatches($character, $maximumCharactersToAdvance = null)
303
    {
304 15
        @trigger_error('Cursor::advanceWhileMatches() will be removed in a future 0.x release.  Use match() instead.', E_USER_DEPRECATED);
305
306
        // Calculate how far to advance
307 15
        $start = $this->currentPosition;
308 15
        $newIndex = $start;
309 15
        if ($maximumCharactersToAdvance === null) {
310 6
            $maximumCharactersToAdvance = $this->length;
311
        }
312
313 15
        $max = min($start + $maximumCharactersToAdvance, $this->length);
314
315 15
        while ($newIndex < $max && $this->getCharacter($newIndex) === $character) {
316 11
            ++$newIndex;
317
        }
318
319 15
        if ($newIndex <= $start) {
320 4
            return 0;
321
        }
322
323 11
        $this->advanceBy($newIndex - $start);
324
325 11
        return $this->currentPosition - $this->previousPosition;
326
    }
327
328
    /**
329
     * Parse zero or more space characters, including at most one newline.
330
     *
331
     * @deprecated Use advanceToNextNonSpaceOrNewline() instead
332
     */
333 18
    public function advanceToFirstNonSpace()
334
    {
335 18
        @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);
0 ignored issues
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...
336
337 18
        return $this->advanceToNextNonSpaceOrNewline();
338
    }
339
340
    /**
341
     * Parse zero or more space/tab characters
342
     *
343
     * @return int Number of positions moved
344
     */
345 1842
    public function advanceToNextNonSpaceOrTab()
346
    {
347 1842
        $newPosition = $this->getNextNonSpacePosition();
348 1842
        $this->advanceBy($newPosition - $this->currentPosition);
349 1842
        $this->partiallyConsumedTab = false;
350
351 1842
        return $this->currentPosition - $this->previousPosition;
352
    }
353
354
    /**
355
     * Parse zero or more space characters, including at most one newline.
356
     *
357
     * Tab characters are not parsed with this function.
358
     *
359
     * @return int Number of positions moved
360
     */
361 441
    public function advanceToNextNonSpaceOrNewline()
362
    {
363 441
        $matches = [];
364 441
        preg_match('/^ *(?:\n *)?/', $this->getRemainder(), $matches, PREG_OFFSET_CAPTURE);
365
366
        // [0][0] contains the matched text
367
        // [0][1] contains the index of that match
368 441
        $increment = $matches[0][1] + strlen($matches[0][0]);
369
370 441
        if ($increment === 0) {
371 300
            return 0;
372
        }
373
374 300
        $this->advanceBy($increment);
375
376 300
        return $this->currentPosition - $this->previousPosition;
377
    }
378
379
    /**
380
     * Move the position to the very end of the line
381
     *
382
     * @return int The number of characters moved
383
     */
384 84
    public function advanceToEnd()
385
    {
386 84
        $this->previousPosition = $this->currentPosition;
387 84
        $this->nextNonSpaceCache = null;
388
389 84
        $this->currentPosition = $this->length;
390
391 84
        return $this->currentPosition - $this->previousPosition;
392
    }
393
394
    /**
395
     * @return string
396
     */
397 2037
    public function getRemainder()
398
    {
399 2037
        if ($this->currentPosition >= $this->length) {
400 693
            return '';
401
        }
402
403 2022
        $prefix = '';
404 2022
        $position = $this->currentPosition;
405 2022
        if ($this->partiallyConsumedTab) {
406 15
            $position++;
407 15
            $charsToTab = 4 - ($this->column % 4);
408 15
            $prefix = str_repeat(' ', $charsToTab);
409 10
        }
410
411 2022
        return $prefix . mb_substr($this->line, $position, null, 'utf-8');
412
    }
413
414
    /**
415
     * @return string
416
     */
417 1887
    public function getLine()
418
    {
419 1887
        return $this->line;
420
    }
421
422
    /**
423
     * @return bool
424
     */
425 411
    public function isAtEnd()
426
    {
427 411
        return $this->currentPosition >= $this->length;
428
    }
429
430
    /**
431
     * Try to match a regular expression
432
     *
433
     * Returns the matching text and advances to the end of that match
434
     *
435
     * @param string $regex
436
     *
437
     * @return string|null
438
     */
439 1902
    public function match($regex)
440
    {
441 1902
        $subject = $this->getRemainder();
442
443 1902
        $matches = [];
444 1902
        if (!preg_match($regex, $subject, $matches, PREG_OFFSET_CAPTURE)) {
445 1758
            return;
446
        }
447
448
        // PREG_OFFSET_CAPTURE always returns the byte offset, not the char offset, which is annoying
449 1776
        $offset = mb_strlen(mb_strcut($subject, 0, $matches[0][1], 'utf-8'), 'utf-8');
450
451
        // [0][0] contains the matched text
452
        // [0][1] contains the index of that match
453 1776
        $this->advanceBy($offset + mb_strlen($matches[0][0], 'utf-8'));
454
455 1776
        return $matches[0][0];
456
    }
457
458
    /**
459
     * @return CursorState
460
     */
461 1011
    public function saveState()
462
    {
463 1011
        return new CursorState(
464 1011
            $this->line,
465 1011
            $this->length,
466 1011
            $this->currentPosition,
467 1011
            $this->previousPosition,
468 1011
            $this->nextNonSpaceCache,
469 1011
            $this->indent,
470 1011
            $this->column,
471 1011
            $this->partiallyConsumedTab
472 674
        );
473
    }
474
475
    /**
476
     * @param CursorState $state
477
     */
478 795
    public function restoreState(CursorState $state)
479
    {
480 795
        $this->line = $state->getLine();
481 795
        $this->length = $state->getLength();
482 795
        $this->currentPosition = $state->getCurrentPosition();
483 795
        $this->previousPosition = $state->getPreviousPosition();
484 795
        $this->nextNonSpaceCache = $state->getNextNonSpaceCache();
485 795
        $this->column = $state->getColumn();
486 795
        $this->indent = $state->getIndent();
487 795
        $this->partiallyConsumedTab = $state->getPartiallyConsumedTab();
488 795
    }
489
490
    /**
491
     * @return int
492
     */
493 630
    public function getPosition()
494
    {
495 630
        return $this->currentPosition;
496
    }
497
498
    /**
499
     * @return string
500
     */
501 870
    public function getPreviousText()
502
    {
503 870
        return mb_substr($this->line, $this->previousPosition, $this->currentPosition - $this->previousPosition, 'utf-8');
504
    }
505
506
    /**
507
     * @return int
508
     */
509 240
    public function getColumn()
510
    {
511 240
        return $this->column;
512
    }
513
}
514