Completed
Push — master ( b3198b...bfe104 )
by Colin
39:04 queued 36:31
created

Cursor::advanceBy()   C

Complexity

Conditions 8
Paths 40

Duplication

Lines 0
Ratio 0 %

Size

Total Lines 39
Code Lines 24

Code Coverage

Tests 26
CRAP Score 8

Importance

Changes 5
Bugs 3 Features 0
Metric Value
c 5
b 3
f 0
dl 0
loc 39
ccs 26
cts 26
cp 1
rs 5.3846
cc 8
eloc 24
nc 40
nop 2
crap 8
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 $firstNonSpaceCache;
55
56
    /**
57
     * @param string $line
58
     */
59 2292
    public function __construct($line)
60
    {
61 2292
        $this->line = $line;
62 2292
        $this->length = mb_strlen($line, 'utf-8');
63 2292
    }
64
65
    /**
66
     * Returns the position of the next non-space character
67
     *
68
     * @return int
69
     */
70 1980
    public function getFirstNonSpacePosition()
71
    {
72 1980
        if ($this->firstNonSpaceCache !== null) {
73 1854
            return $this->firstNonSpaceCache;
74
        }
75
76 1980
        $i = $this->currentPosition;
77 1980
        $cols = $this->column;
78
79 1980
        while (($c = $this->getCharacter($i)) !== null) {
80 1965
            if ($c === ' ') {
81 474
                $i++;
82 474
                $cols++;
83 1965
            } elseif ($c === "\t") {
84 18
                $i++;
85 18
                $cols += (4 - ($cols % 4));
86 18
            } else {
87 1932
                break;
88
            }
89 480
        }
90
91 1980
        $nextNonSpace = ($c === null) ? $this->length : $i;
92 1980
        $this->indent = $cols - $this->column;
93
94 1980
        return $this->firstNonSpaceCache = $nextNonSpace;
95
    }
96
97
    /**
98
     * Returns the next character which isn't a space
99
     *
100
     * @return string
101
     */
102 1827
    public function getFirstNonSpaceCharacter()
103
    {
104 1827
        return $this->getCharacter($this->getFirstNonSpacePosition());
105
    }
106
107
    /**
108
     * Calculates the current indent (number of spaces after current position)
109
     *
110
     * @return int
111
     */
112 1914
    public function getIndent()
113
    {
114 1914
        $this->getFirstNonSpacePosition();
115
116 1914
        return $this->indent;
117
    }
118
119
    /**
120
     * Whether the cursor is indented to INDENT_LEVEL
121
     *
122
     * @return bool
123
     */
124 1854
    public function isIndented()
125
    {
126 1854
        return $this->getIndent() >= self::INDENT_LEVEL;
127
    }
128
129
    /**
130
     * @param int|null $index
131
     *
132
     * @return string|null
133
     */
134 2073
    public function getCharacter($index = null)
135
    {
136 2073
        if ($index === null) {
137 1608
            $index = $this->currentPosition;
138 1608
        }
139
140
        // Index out-of-bounds, or we're at the end
141 2073
        if ($index < 0 || $index >= $this->length) {
142 1800
            return;
143
        }
144
145 2040
        return mb_substr($this->line, $index, 1, 'utf-8');
146
    }
147
148
    /**
149
     * Returns the next character (or null, if none) without advancing forwards
150
     *
151
     * @param int $offset
152
     *
153
     * @return string|null
154
     */
155 978
    public function peek($offset = 1)
156
    {
157 978
        return $this->getCharacter($this->currentPosition + $offset);
158
    }
159
160
    /**
161
     * Whether the remainder is blank
162
     *
163
     * @return bool
164
     */
165 1872
    public function isBlank()
166
    {
167 1872
        return $this->getFirstNonSpacePosition() === $this->length;
168
    }
169
170
    /**
171
     * Move the cursor forwards
172
     */
173 756
    public function advance()
174
    {
175 756
        $this->advanceBy(1);
176 756
    }
177
178
    /**
179
     * Move the cursor forwards
180
     *
181
     * @param int $characters Number of characters to advance by
182
     */
183 2115
    public function advanceBy($characters, $advanceByColumns = false)
184
    {
185 2115
        $this->firstNonSpaceCache = null;
186
187 2115
        $cols = 0;
188 2115
        $relPos = -1;
189
190 2115
        $nextFewChars = mb_substr($this->line, $this->currentPosition, $characters, 'utf-8');
191 2115
        if ($characters === 1) {
192 1389
            $asArray = [$nextFewChars];
193 1389
        } else {
194 1986
            $asArray = preg_split('//u', $nextFewChars, null, PREG_SPLIT_NO_EMPTY);
195
        }
196
197 2115
        foreach ($asArray as $relPos => $char) {
198 2040
            if ($char === "\t") {
199 27
                $cols += (4 - (($this->column + $cols) % 4));
200 27
            } else {
201 2037
                $cols++;
202
            }
203
204 2040
            if ($advanceByColumns && $cols >= $characters) {
205 327
                break;
206
            }
207 2115
        }
208
209 2115
        $i = $advanceByColumns ? $relPos + 1 : $characters;
210
211 2115
        $this->previousPosition = $this->currentPosition;
212 2115
        $newPosition = $this->currentPosition + $i;
213
214 2115
        $this->column += $cols;
215
216 2115
        if ($newPosition >= $this->length) {
217 1821
            $this->currentPosition = $this->length;
218 1821
        } else {
219 1839
            $this->currentPosition = $newPosition;
0 ignored issues
show
Documentation Bug introduced by Colin O'Dell
The property $currentPosition was declared of type integer, but $newPosition is of type double. Maybe add a type cast?

This check looks for assignments to scalar types that may be of the wrong type.

To ensure the code behaves as expected, it may be a good idea to add an explicit type cast.

$answer = 42;

$correct = false;

$correct = (bool) $answer;
Loading history...
220
        }
221 2115
    }
222
223
    /**
224
     * Advances the cursor while the given character is matched
225
     *
226
     * @param string   $character                  Character to match
227
     * @param int|null $maximumCharactersToAdvance Maximum number of characters to advance before giving up
228
     *
229
     * @return int Number of positions moved (0 if unsuccessful)
230
     */
231 144
    public function advanceWhileMatches($character, $maximumCharactersToAdvance = null)
232
    {
233
        // Calculate how far to advance
234 144
        $start = $this->currentPosition;
235 144
        $newIndex = $start;
236 144
        if ($maximumCharactersToAdvance === null) {
237 18
            $maximumCharactersToAdvance = $this->length;
238 18
        }
239
240 144
        $max = min($start + $maximumCharactersToAdvance, $this->length);
241
242 144
        while ($newIndex < $max && $this->getCharacter($newIndex) === $character) {
243 45
            ++$newIndex;
244 45
        }
245
246 144
        if ($newIndex <= $start) {
247 108
            return 0;
248
        }
249
250 45
        $this->advanceBy($newIndex - $start);
251
252 45
        return $this->currentPosition - $this->previousPosition;
253
    }
254
255
    /**
256
     * Parse zero or more space characters, including at most one newline
257
     *
258
     * @return int Number of positions moved
259
     */
260 1863
    public function advanceToFirstNonSpace()
261
    {
262 1863
        $matches = [];
263 1863
        preg_match('/^ *(?:\n *)?/', $this->getRemainder(), $matches, PREG_OFFSET_CAPTURE);
264
265
        // [0][0] contains the matched text
266
        // [0][1] contains the index of that match
267 1863
        $increment = $matches[0][1] + strlen($matches[0][0]);
268
269 1863
        if ($increment === 0) {
270 1800
            return 0;
271
        }
272
273 495
        $this->advanceBy($increment);
274
275 495
        return $this->currentPosition - $this->previousPosition;
276
    }
277
278
    /**
279
     * @return string
280
     */
281 1956
    public function getRemainder()
282
    {
283 1956
        if ($this->isAtEnd()) {
284 657
            return '';
285
        } else {
286 1944
            return mb_substr($this->line, $this->currentPosition, null, 'utf-8');
287
        }
288
    }
289
290
    /**
291
     * @return string
292
     */
293 1812
    public function getLine()
294
    {
295 1812
        return $this->line;
296
    }
297
298
    /**
299
     * @return bool
300
     */
301 1977
    public function isAtEnd()
302
    {
303 1977
        return $this->currentPosition >= $this->length;
304
    }
305
306
    /**
307
     * Try to match a regular expression
308
     *
309
     * Returns the matching text and advances to the end of that match
310
     *
311
     * @param string $regex
312
     *
313
     * @return string|null
314
     */
315 1827
    public function match($regex)
316
    {
317 1827
        $subject = $this->getRemainder();
318
319 1827
        $matches = [];
320 1827
        if (!preg_match($regex, $subject, $matches, PREG_OFFSET_CAPTURE)) {
321 1689
            return;
322
        }
323
324
        // PREG_OFFSET_CAPTURE always returns the byte offset, not the char offset, which is annoying
325 1713
        $offset = mb_strlen(mb_strcut($subject, 0, $matches[0][1], 'utf-8'), 'utf-8');
326
327
        // [0][0] contains the matched text
328
        // [0][1] contains the index of that match
329 1713
        $this->advanceBy($offset + mb_strlen($matches[0][0], 'utf-8'));
330
331 1713
        return $matches[0][0];
332
    }
333
334
    /**
335
     * @return CursorState
336
     */
337 1770
    public function saveState()
338
    {
339 1770
        return new CursorState(
340 1770
            $this->line,
341 1770
            $this->length,
342 1770
            $this->currentPosition,
343 1770
            $this->previousPosition,
344 1770
            $this->firstNonSpaceCache,
345 1770
            $this->indent,
346 1770
            $this->column
347 1770
        );
348
    }
349
350
    /**
351
     * @param CursorState $state
352
     */
353 1689
    public function restoreState(CursorState $state)
354
    {
355 1689
        $this->line = $state->getLine();
356 1689
        $this->length = $state->getLength();
357 1689
        $this->currentPosition = $state->getCurrentPosition();
358 1689
        $this->previousPosition = $state->getPreviousPosition();
359 1689
        $this->firstNonSpaceCache = $state->getFirstNonSpaceCache();
360 1689
        $this->column = $state->getColumn();
361 1689
        $this->indent = $state->getIndent();
362 1689
    }
363
364
    /**
365
     * @return int
366
     */
367 588
    public function getPosition()
368
    {
369 588
        return $this->currentPosition;
370
    }
371
372
    /**
373
     * @return string
374
     */
375 810
    public function getPreviousText()
376
    {
377 810
        return mb_substr($this->line, $this->previousPosition, $this->currentPosition - $this->previousPosition, 'utf-8');
378
    }
379
380
    /**
381
     * @return int
382
     */
383 234
    public function getColumn()
384
    {
385 234
        return $this->column;
386
    }
387
}
388