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

src/Cursor.php (1 issue)

Checks property assignments for possibly missing type casts

Bug Documentation Minor

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

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
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