Cursor   B
last analyzed

Coupling/Cohesion

Components 1
Dependencies 1

Complexity

Total Complexity 53

Size/Duplication

Total Lines 479
Duplicated Lines 0 %

Test Coverage

Coverage 100%

Importance

Changes 4
Bugs 0 Features 0
Metric Value
wmc 53
c 4
b 0
f 0
lcom 1
cbo 1
dl 0
loc 479
ccs 171
cts 171
cp 1
rs 7.4757

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
D advanceBy() 0 39 9
A advanceBySpaceOrTab() 0 12 3
B advanceWhileMatches() 0 23 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
B getNextNonSpacePosition() 0 26 6

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