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