Total Complexity | 539 |
Total Lines | 2796 |
Duplicated Lines | 0 % |
Changes | 0 |
Complex classes like Parser 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.
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 Parser, and based on these observations, apply Extract Interface, too.
1 | <?php |
||
26 | { |
||
27 | const SOURCE_INDEX = -1; |
||
28 | const SOURCE_LINE = -2; |
||
29 | const SOURCE_COLUMN = -3; |
||
30 | |||
31 | /** |
||
32 | * @var array |
||
33 | */ |
||
34 | protected static $precedence = [ |
||
35 | '=' => 0, |
||
36 | 'or' => 1, |
||
37 | 'and' => 2, |
||
38 | '==' => 3, |
||
39 | '!=' => 3, |
||
40 | '<=>' => 3, |
||
41 | '<=' => 4, |
||
42 | '>=' => 4, |
||
43 | '<' => 4, |
||
44 | '>' => 4, |
||
45 | '+' => 5, |
||
46 | '-' => 5, |
||
47 | '*' => 6, |
||
48 | '/' => 6, |
||
49 | '%' => 6, |
||
50 | ]; |
||
51 | |||
52 | protected static $commentPattern; |
||
53 | protected static $operatorPattern; |
||
54 | protected static $whitePattern; |
||
55 | |||
56 | private $sourceName; |
||
57 | private $sourceIndex; |
||
58 | private $sourcePositions; |
||
59 | private $charset; |
||
60 | private $count; |
||
61 | private $env; |
||
62 | private $inParens; |
||
63 | private $eatWhiteDefault; |
||
64 | private $buffer; |
||
65 | private $utf8; |
||
66 | private $encoding; |
||
67 | private $patternModifiers; |
||
68 | |||
69 | /** |
||
70 | * Constructor |
||
71 | * |
||
72 | * @api |
||
73 | * |
||
74 | * @param string $sourceName |
||
75 | * @param integer $sourceIndex |
||
76 | * @param string $encoding |
||
77 | */ |
||
78 | public function __construct($sourceName, $sourceIndex = 0, $encoding = 'utf-8') |
||
79 | { |
||
80 | $this->sourceName = $sourceName ?: '(stdin)'; |
||
81 | $this->sourceIndex = $sourceIndex; |
||
82 | $this->charset = null; |
||
83 | $this->utf8 = ! $encoding || strtolower($encoding) === 'utf-8'; |
||
84 | $this->patternModifiers = $this->utf8 ? 'Aisu' : 'Ais'; |
||
85 | |||
86 | if (empty(static::$operatorPattern)) { |
||
87 | static::$operatorPattern = '([*\/%+-]|[!=]\=|\>\=?|\<\=\>|\<\=?|and|or)'; |
||
88 | |||
89 | $commentSingle = '\/\/'; |
||
90 | $commentMultiLeft = '\/\*'; |
||
91 | $commentMultiRight = '\*\/'; |
||
92 | |||
93 | static::$commentPattern = $commentMultiLeft . '.*?' . $commentMultiRight; |
||
94 | static::$whitePattern = $this->utf8 |
||
95 | ? '/' . $commentSingle . '[^\n]*\s*|(' . static::$commentPattern . ')\s*|\s+/AisuS' |
||
96 | : '/' . $commentSingle . '[^\n]*\s*|(' . static::$commentPattern . ')\s*|\s+/AisS'; |
||
97 | } |
||
98 | } |
||
99 | |||
100 | /** |
||
101 | * Get source file name |
||
102 | * |
||
103 | * @api |
||
104 | * |
||
105 | * @return string |
||
106 | */ |
||
107 | public function getSourceName() |
||
108 | { |
||
109 | return $this->sourceName; |
||
110 | } |
||
111 | |||
112 | /** |
||
113 | * Throw parser error |
||
114 | * |
||
115 | * @api |
||
116 | * |
||
117 | * @param string $msg |
||
118 | * |
||
119 | * @throws \Leafo\ScssPhp\Exception\ParserException |
||
120 | */ |
||
121 | public function throwParseError($msg = 'parse error') |
||
122 | { |
||
123 | list($line, /* $column */) = $this->getSourcePosition($this->count); |
||
124 | |||
125 | $loc = empty($this->sourceName) ? "line: $line" : "$this->sourceName on line $line"; |
||
126 | |||
127 | if ($this->peek("(.*?)(\n|$)", $m, $this->count)) { |
||
128 | throw new ParserException("$msg: failed at `$m[1]` $loc"); |
||
129 | } |
||
130 | |||
131 | throw new ParserException("$msg: $loc"); |
||
132 | } |
||
133 | |||
134 | /** |
||
135 | * Parser buffer |
||
136 | * |
||
137 | * @api |
||
138 | * |
||
139 | * @param string $buffer |
||
140 | * |
||
141 | * @return \Leafo\ScssPhp\Block |
||
142 | */ |
||
143 | public function parse($buffer) |
||
144 | { |
||
145 | // strip BOM (byte order marker) |
||
146 | if (substr($buffer, 0, 3) === "\xef\xbb\xbf") { |
||
147 | $buffer = substr($buffer, 3); |
||
148 | } |
||
149 | |||
150 | $this->buffer = rtrim($buffer, "\x00..\x1f"); |
||
151 | $this->count = 0; |
||
152 | $this->env = null; |
||
153 | $this->inParens = false; |
||
154 | $this->eatWhiteDefault = true; |
||
155 | |||
156 | $this->saveEncoding(); |
||
157 | $this->extractLineNumbers($buffer); |
||
158 | |||
159 | $this->pushBlock(null); // root block |
||
160 | $this->whitespace(); |
||
161 | $this->pushBlock(null); |
||
162 | $this->popBlock(); |
||
163 | |||
164 | while ($this->parseChunk()) { |
||
165 | ; |
||
166 | } |
||
167 | |||
168 | if ($this->count !== strlen($this->buffer)) { |
||
169 | $this->throwParseError(); |
||
170 | } |
||
171 | |||
172 | if (! empty($this->env->parent)) { |
||
173 | $this->throwParseError('unclosed block'); |
||
174 | } |
||
175 | |||
176 | if ($this->charset) { |
||
177 | array_unshift($this->env->children, $this->charset); |
||
178 | } |
||
179 | |||
180 | $this->env->isRoot = true; |
||
181 | |||
182 | $this->restoreEncoding(); |
||
183 | |||
184 | return $this->env; |
||
185 | } |
||
186 | |||
187 | /** |
||
188 | * Parse a value or value list |
||
189 | * |
||
190 | * @api |
||
191 | * |
||
192 | * @param string $buffer |
||
193 | * @param string $out |
||
194 | * |
||
195 | * @return boolean |
||
196 | */ |
||
197 | public function parseValue($buffer, &$out) |
||
198 | { |
||
199 | $this->count = 0; |
||
200 | $this->env = null; |
||
201 | $this->inParens = false; |
||
202 | $this->eatWhiteDefault = true; |
||
203 | $this->buffer = (string) $buffer; |
||
204 | |||
205 | $this->saveEncoding(); |
||
206 | |||
207 | $list = $this->valueList($out); |
||
208 | |||
209 | $this->restoreEncoding(); |
||
210 | |||
211 | return $list; |
||
212 | } |
||
213 | |||
214 | /** |
||
215 | * Parse a selector or selector list |
||
216 | * |
||
217 | * @api |
||
218 | * |
||
219 | * @param string $buffer |
||
220 | * @param string $out |
||
221 | * |
||
222 | * @return boolean |
||
223 | */ |
||
224 | public function parseSelector($buffer, &$out) |
||
225 | { |
||
226 | $this->count = 0; |
||
227 | $this->env = null; |
||
228 | $this->inParens = false; |
||
229 | $this->eatWhiteDefault = true; |
||
230 | $this->buffer = (string) $buffer; |
||
231 | |||
232 | $this->saveEncoding(); |
||
233 | |||
234 | $selector = $this->selectors($out); |
||
|
|||
235 | |||
236 | $this->restoreEncoding(); |
||
237 | |||
238 | return $selector; |
||
239 | } |
||
240 | |||
241 | /** |
||
242 | * Parse a single chunk off the head of the buffer and append it to the |
||
243 | * current parse environment. |
||
244 | * |
||
245 | * Returns false when the buffer is empty, or when there is an error. |
||
246 | * |
||
247 | * This function is called repeatedly until the entire document is |
||
248 | * parsed. |
||
249 | * |
||
250 | * This parser is most similar to a recursive descent parser. Single |
||
251 | * functions represent discrete grammatical rules for the language, and |
||
252 | * they are able to capture the text that represents those rules. |
||
253 | * |
||
254 | * Consider the function Compiler::keyword(). (All parse functions are |
||
255 | * structured the same.) |
||
256 | * |
||
257 | * The function takes a single reference argument. When calling the |
||
258 | * function it will attempt to match a keyword on the head of the buffer. |
||
259 | * If it is successful, it will place the keyword in the referenced |
||
260 | * argument, advance the position in the buffer, and return true. If it |
||
261 | * fails then it won't advance the buffer and it will return false. |
||
262 | * |
||
263 | * All of these parse functions are powered by Compiler::match(), which behaves |
||
264 | * the same way, but takes a literal regular expression. Sometimes it is |
||
265 | * more convenient to use match instead of creating a new function. |
||
266 | * |
||
267 | * Because of the format of the functions, to parse an entire string of |
||
268 | * grammatical rules, you can chain them together using &&. |
||
269 | * |
||
270 | * But, if some of the rules in the chain succeed before one fails, then |
||
271 | * the buffer position will be left at an invalid state. In order to |
||
272 | * avoid this, Compiler::seek() is used to remember and set buffer positions. |
||
273 | * |
||
274 | * Before parsing a chain, use $s = $this->seek() to remember the current |
||
275 | * position into $s. Then if a chain fails, use $this->seek($s) to |
||
276 | * go back where we started. |
||
277 | * |
||
278 | * @return boolean |
||
279 | */ |
||
280 | protected function parseChunk() |
||
281 | { |
||
282 | $s = $this->seek(); |
||
283 | |||
284 | // the directives |
||
285 | if (isset($this->buffer[$this->count]) && $this->buffer[$this->count] === '@') { |
||
286 | if ($this->literal('@at-root') && |
||
287 | ($this->selectors($selector) || true) && |
||
288 | ($this->map($with) || true) && |
||
289 | $this->literal('{') |
||
290 | ) { |
||
291 | $atRoot = $this->pushSpecialBlock(Type::T_AT_ROOT, $s); |
||
292 | $atRoot->selector = $selector; |
||
293 | $atRoot->with = $with; |
||
294 | |||
295 | return true; |
||
296 | } |
||
297 | |||
298 | $this->seek($s); |
||
299 | |||
300 | if ($this->literal('@media') && $this->mediaQueryList($mediaQueryList) && $this->literal('{')) { |
||
301 | $media = $this->pushSpecialBlock(Type::T_MEDIA, $s); |
||
302 | $media->queryList = $mediaQueryList[2]; |
||
303 | |||
304 | return true; |
||
305 | } |
||
306 | |||
307 | $this->seek($s); |
||
308 | |||
309 | if ($this->literal('@mixin') && |
||
310 | $this->keyword($mixinName) && |
||
311 | ($this->argumentDef($args) || true) && |
||
312 | $this->literal('{') |
||
313 | ) { |
||
314 | $mixin = $this->pushSpecialBlock(Type::T_MIXIN, $s); |
||
315 | $mixin->name = $mixinName; |
||
316 | $mixin->args = $args; |
||
317 | |||
318 | return true; |
||
319 | } |
||
320 | |||
321 | $this->seek($s); |
||
322 | |||
323 | if ($this->literal('@include') && |
||
324 | $this->keyword($mixinName) && |
||
325 | ($this->literal('(') && |
||
326 | ($this->argValues($argValues) || true) && |
||
327 | $this->literal(')') || true) && |
||
328 | ($this->end() || |
||
329 | $this->literal('{') && $hasBlock = true) |
||
330 | ) { |
||
331 | $child = [Type::T_INCLUDE, $mixinName, isset($argValues) ? $argValues : null, null]; |
||
332 | |||
333 | if (! empty($hasBlock)) { |
||
334 | $include = $this->pushSpecialBlock(Type::T_INCLUDE, $s); |
||
335 | $include->child = $child; |
||
336 | } else { |
||
337 | $this->append($child, $s); |
||
338 | } |
||
339 | |||
340 | return true; |
||
341 | } |
||
342 | |||
343 | $this->seek($s); |
||
344 | |||
345 | if ($this->literal('@scssphp-import-once') && |
||
346 | $this->valueList($importPath) && |
||
347 | $this->end() |
||
348 | ) { |
||
349 | $this->append([Type::T_SCSSPHP_IMPORT_ONCE, $importPath], $s); |
||
350 | |||
351 | return true; |
||
352 | } |
||
353 | |||
354 | $this->seek($s); |
||
355 | |||
356 | if ($this->literal('@import') && |
||
357 | $this->valueList($importPath) && |
||
358 | $this->end() |
||
359 | ) { |
||
360 | $this->append([Type::T_IMPORT, $importPath], $s); |
||
361 | |||
362 | return true; |
||
363 | } |
||
364 | |||
365 | $this->seek($s); |
||
366 | |||
367 | if ($this->literal('@import') && |
||
368 | $this->url($importPath) && |
||
369 | $this->end() |
||
370 | ) { |
||
371 | $this->append([Type::T_IMPORT, $importPath], $s); |
||
372 | |||
373 | return true; |
||
374 | } |
||
375 | |||
376 | $this->seek($s); |
||
377 | |||
378 | if ($this->literal('@extend') && |
||
379 | $this->selectors($selectors) && |
||
380 | $this->end() |
||
381 | ) { |
||
382 | // check for '!flag' |
||
383 | $optional = $this->stripOptionalFlag($selectors); |
||
384 | $this->append([Type::T_EXTEND, $selectors, $optional], $s); |
||
385 | |||
386 | return true; |
||
387 | } |
||
388 | |||
389 | $this->seek($s); |
||
390 | |||
391 | if ($this->literal('@function') && |
||
392 | $this->keyword($fnName) && |
||
393 | $this->argumentDef($args) && |
||
394 | $this->literal('{') |
||
395 | ) { |
||
396 | $func = $this->pushSpecialBlock(Type::T_FUNCTION, $s); |
||
397 | $func->name = $fnName; |
||
398 | $func->args = $args; |
||
399 | |||
400 | return true; |
||
401 | } |
||
402 | |||
403 | $this->seek($s); |
||
404 | |||
405 | if ($this->literal('@break') && $this->end()) { |
||
406 | $this->append([Type::T_BREAK], $s); |
||
407 | |||
408 | return true; |
||
409 | } |
||
410 | |||
411 | $this->seek($s); |
||
412 | |||
413 | if ($this->literal('@continue') && $this->end()) { |
||
414 | $this->append([Type::T_CONTINUE], $s); |
||
415 | |||
416 | return true; |
||
417 | } |
||
418 | |||
419 | $this->seek($s); |
||
420 | |||
421 | |||
422 | if ($this->literal('@return') && ($this->valueList($retVal) || true) && $this->end()) { |
||
423 | $this->append([Type::T_RETURN, isset($retVal) ? $retVal : [Type::T_NULL]], $s); |
||
424 | |||
425 | return true; |
||
426 | } |
||
427 | |||
428 | $this->seek($s); |
||
429 | |||
430 | if ($this->literal('@each') && |
||
431 | $this->genericList($varNames, 'variable', ',', false) && |
||
432 | $this->literal('in') && |
||
433 | $this->valueList($list) && |
||
434 | $this->literal('{') |
||
435 | ) { |
||
436 | $each = $this->pushSpecialBlock(Type::T_EACH, $s); |
||
437 | |||
438 | foreach ($varNames[2] as $varName) { |
||
439 | $each->vars[] = $varName[1]; |
||
440 | } |
||
441 | |||
442 | $each->list = $list; |
||
443 | |||
444 | return true; |
||
445 | } |
||
446 | |||
447 | $this->seek($s); |
||
448 | |||
449 | if ($this->literal('@while') && |
||
450 | $this->expression($cond) && |
||
451 | $this->literal('{') |
||
452 | ) { |
||
453 | $while = $this->pushSpecialBlock(Type::T_WHILE, $s); |
||
454 | $while->cond = $cond; |
||
455 | |||
456 | return true; |
||
457 | } |
||
458 | |||
459 | $this->seek($s); |
||
460 | |||
461 | if ($this->literal('@for') && |
||
462 | $this->variable($varName) && |
||
463 | $this->literal('from') && |
||
464 | $this->expression($start) && |
||
465 | ($this->literal('through') || |
||
466 | ($forUntil = true && $this->literal('to'))) && |
||
467 | $this->expression($end) && |
||
468 | $this->literal('{') |
||
469 | ) { |
||
470 | $for = $this->pushSpecialBlock(Type::T_FOR, $s); |
||
471 | $for->var = $varName[1]; |
||
472 | $for->start = $start; |
||
473 | $for->end = $end; |
||
474 | $for->until = isset($forUntil); |
||
475 | |||
476 | return true; |
||
477 | } |
||
478 | |||
479 | $this->seek($s); |
||
480 | |||
481 | if ($this->literal('@if') && $this->valueList($cond) && $this->literal('{')) { |
||
482 | $if = $this->pushSpecialBlock(Type::T_IF, $s); |
||
483 | $if->cond = $cond; |
||
484 | $if->cases = []; |
||
485 | |||
486 | return true; |
||
487 | } |
||
488 | |||
489 | $this->seek($s); |
||
490 | |||
491 | if ($this->literal('@debug') && |
||
492 | $this->valueList($value) && |
||
493 | $this->end() |
||
494 | ) { |
||
495 | $this->append([Type::T_DEBUG, $value], $s); |
||
496 | |||
497 | return true; |
||
498 | } |
||
499 | |||
500 | $this->seek($s); |
||
501 | |||
502 | if ($this->literal('@warn') && |
||
503 | $this->valueList($value) && |
||
504 | $this->end() |
||
505 | ) { |
||
506 | $this->append([Type::T_WARN, $value], $s); |
||
507 | |||
508 | return true; |
||
509 | } |
||
510 | |||
511 | $this->seek($s); |
||
512 | |||
513 | if ($this->literal('@error') && |
||
514 | $this->valueList($value) && |
||
515 | $this->end() |
||
516 | ) { |
||
517 | $this->append([Type::T_ERROR, $value], $s); |
||
518 | |||
519 | return true; |
||
520 | } |
||
521 | |||
522 | $this->seek($s); |
||
523 | |||
524 | if ($this->literal('@content') && $this->end()) { |
||
525 | $this->append([Type::T_MIXIN_CONTENT], $s); |
||
526 | |||
527 | return true; |
||
528 | } |
||
529 | |||
530 | $this->seek($s); |
||
531 | |||
532 | $last = $this->last(); |
||
533 | |||
534 | if (isset($last) && $last[0] === Type::T_IF) { |
||
535 | list(, $if) = $last; |
||
536 | |||
537 | if ($this->literal('@else')) { |
||
538 | if ($this->literal('{')) { |
||
539 | $else = $this->pushSpecialBlock(Type::T_ELSE, $s); |
||
540 | } elseif ($this->literal('if') && $this->valueList($cond) && $this->literal('{')) { |
||
541 | $else = $this->pushSpecialBlock(Type::T_ELSEIF, $s); |
||
542 | $else->cond = $cond; |
||
543 | } |
||
544 | |||
545 | if (isset($else)) { |
||
546 | $else->dontAppend = true; |
||
547 | $if->cases[] = $else; |
||
548 | |||
549 | return true; |
||
550 | } |
||
551 | } |
||
552 | |||
553 | $this->seek($s); |
||
554 | } |
||
555 | |||
556 | // only retain the first @charset directive encountered |
||
557 | if ($this->literal('@charset') && |
||
558 | $this->valueList($charset) && |
||
559 | $this->end() |
||
560 | ) { |
||
561 | if (! isset($this->charset)) { |
||
562 | $statement = [Type::T_CHARSET, $charset]; |
||
563 | |||
564 | list($line, $column) = $this->getSourcePosition($s); |
||
565 | |||
566 | $statement[static::SOURCE_LINE] = $line; |
||
567 | $statement[static::SOURCE_COLUMN] = $column; |
||
568 | $statement[static::SOURCE_INDEX] = $this->sourceIndex; |
||
569 | |||
570 | $this->charset = $statement; |
||
571 | } |
||
572 | |||
573 | return true; |
||
574 | } |
||
575 | |||
576 | $this->seek($s); |
||
577 | |||
578 | // doesn't match built in directive, do generic one |
||
579 | if ($this->literal('@', false) && |
||
580 | $this->keyword($dirName) && |
||
581 | ($this->variable($dirValue) || $this->openString('{', $dirValue) || true) && |
||
582 | $this->literal('{') |
||
583 | ) { |
||
584 | if ($dirName === 'media') { |
||
585 | $directive = $this->pushSpecialBlock(Type::T_MEDIA, $s); |
||
586 | } else { |
||
587 | $directive = $this->pushSpecialBlock(Type::T_DIRECTIVE, $s); |
||
588 | $directive->name = $dirName; |
||
589 | } |
||
590 | |||
591 | if (isset($dirValue)) { |
||
592 | $directive->value = $dirValue; |
||
593 | } |
||
594 | |||
595 | return true; |
||
596 | } |
||
597 | |||
598 | $this->seek($s); |
||
599 | |||
600 | return false; |
||
601 | } |
||
602 | |||
603 | // property shortcut |
||
604 | // captures most properties before having to parse a selector |
||
605 | if ($this->keyword($name, false) && |
||
606 | $this->literal(': ') && |
||
607 | $this->valueList($value) && |
||
608 | $this->end() |
||
609 | ) { |
||
610 | $name = [Type::T_STRING, '', [$name]]; |
||
611 | $this->append([Type::T_ASSIGN, $name, $value], $s); |
||
612 | |||
613 | return true; |
||
614 | } |
||
615 | |||
616 | $this->seek($s); |
||
617 | |||
618 | // variable assigns |
||
619 | if ($this->variable($name) && |
||
620 | $this->literal(':') && |
||
621 | $this->valueList($value) && |
||
622 | $this->end() |
||
623 | ) { |
||
624 | // check for '!flag' |
||
625 | $assignmentFlags = $this->stripAssignmentFlags($value); |
||
626 | $this->append([Type::T_ASSIGN, $name, $value, $assignmentFlags], $s); |
||
627 | |||
628 | return true; |
||
629 | } |
||
630 | |||
631 | $this->seek($s); |
||
632 | |||
633 | // misc |
||
634 | if ($this->literal('-->')) { |
||
635 | return true; |
||
636 | } |
||
637 | |||
638 | // opening css block |
||
639 | if ($this->selectors($selectors) && $this->literal('{')) { |
||
640 | $this->pushBlock($selectors, $s); |
||
641 | |||
642 | return true; |
||
643 | } |
||
644 | |||
645 | $this->seek($s); |
||
646 | |||
647 | // property assign, or nested assign |
||
648 | if ($this->propertyName($name) && $this->literal(':')) { |
||
649 | $foundSomething = false; |
||
650 | |||
651 | if ($this->valueList($value)) { |
||
652 | $this->append([Type::T_ASSIGN, $name, $value], $s); |
||
653 | $foundSomething = true; |
||
654 | } |
||
655 | |||
656 | if ($this->literal('{')) { |
||
657 | $propBlock = $this->pushSpecialBlock(Type::T_NESTED_PROPERTY, $s); |
||
658 | $propBlock->prefix = $name; |
||
659 | $foundSomething = true; |
||
660 | } elseif ($foundSomething) { |
||
661 | $foundSomething = $this->end(); |
||
662 | } |
||
663 | |||
664 | if ($foundSomething) { |
||
665 | return true; |
||
666 | } |
||
667 | } |
||
668 | |||
669 | $this->seek($s); |
||
670 | |||
671 | // closing a block |
||
672 | if ($this->literal('}')) { |
||
673 | $block = $this->popBlock(); |
||
674 | |||
675 | if (isset($block->type) && $block->type === Type::T_INCLUDE) { |
||
676 | $include = $block->child; |
||
677 | unset($block->child); |
||
678 | $include[3] = $block; |
||
679 | $this->append($include, $s); |
||
680 | } elseif (empty($block->dontAppend)) { |
||
681 | $type = isset($block->type) ? $block->type : Type::T_BLOCK; |
||
682 | $this->append([$type, $block], $s); |
||
683 | } |
||
684 | |||
685 | return true; |
||
686 | } |
||
687 | |||
688 | // extra stuff |
||
689 | if ($this->literal(';') || |
||
690 | $this->literal('<!--') |
||
691 | ) { |
||
692 | return true; |
||
693 | } |
||
694 | |||
695 | return false; |
||
696 | } |
||
697 | |||
698 | /** |
||
699 | * Push block onto parse tree |
||
700 | * |
||
701 | * @param array $selectors |
||
702 | * @param integer $pos |
||
703 | * |
||
704 | * @return \Leafo\ScssPhp\Block |
||
705 | */ |
||
706 | protected function pushBlock($selectors, $pos = 0) |
||
707 | { |
||
708 | list($line, $column) = $this->getSourcePosition($pos); |
||
709 | |||
710 | $b = new Block; |
||
711 | $b->sourceName = $this->sourceName; |
||
712 | $b->sourceLine = $line; |
||
713 | $b->sourceColumn = $column; |
||
714 | $b->sourceIndex = $this->sourceIndex; |
||
715 | $b->selectors = $selectors; |
||
716 | $b->comments = []; |
||
717 | $b->parent = $this->env; |
||
718 | |||
719 | if (! $this->env) { |
||
720 | $b->children = []; |
||
721 | } elseif (empty($this->env->children)) { |
||
722 | $this->env->children = $this->env->comments; |
||
723 | $b->children = []; |
||
724 | $this->env->comments = []; |
||
725 | } else { |
||
726 | $b->children = $this->env->comments; |
||
727 | $this->env->comments = []; |
||
728 | } |
||
729 | |||
730 | $this->env = $b; |
||
731 | |||
732 | return $b; |
||
733 | } |
||
734 | |||
735 | /** |
||
736 | * Push special (named) block onto parse tree |
||
737 | * |
||
738 | * @param string $type |
||
739 | * @param integer $pos |
||
740 | * |
||
741 | * @return \Leafo\ScssPhp\Block |
||
742 | */ |
||
743 | protected function pushSpecialBlock($type, $pos) |
||
744 | { |
||
745 | $block = $this->pushBlock(null, $pos); |
||
746 | $block->type = $type; |
||
747 | |||
748 | return $block; |
||
749 | } |
||
750 | |||
751 | /** |
||
752 | * Pop scope and return last block |
||
753 | * |
||
754 | * @return \Leafo\ScssPhp\Block |
||
755 | * |
||
756 | * @throws \Exception |
||
757 | */ |
||
758 | protected function popBlock() |
||
759 | { |
||
760 | $block = $this->env; |
||
761 | |||
762 | if (empty($block->parent)) { |
||
763 | $this->throwParseError('unexpected }'); |
||
764 | } |
||
765 | |||
766 | $this->env = $block->parent; |
||
767 | unset($block->parent); |
||
768 | |||
769 | $comments = $block->comments; |
||
770 | if (count($comments)) { |
||
771 | $this->env->comments = $comments; |
||
772 | unset($block->comments); |
||
773 | } |
||
774 | |||
775 | return $block; |
||
776 | } |
||
777 | |||
778 | /** |
||
779 | * Peek input stream |
||
780 | * |
||
781 | * @param string $regex |
||
782 | * @param array $out |
||
783 | * @param integer $from |
||
784 | * |
||
785 | * @return integer |
||
786 | */ |
||
787 | protected function peek($regex, &$out, $from = null) |
||
788 | { |
||
789 | if (! isset($from)) { |
||
790 | $from = $this->count; |
||
791 | } |
||
792 | |||
793 | $r = '/' . $regex . '/' . $this->patternModifiers; |
||
794 | $result = preg_match($r, $this->buffer, $out, null, $from); |
||
795 | |||
796 | return $result; |
||
797 | } |
||
798 | |||
799 | /** |
||
800 | * Seek to position in input stream (or return current position in input stream) |
||
801 | * |
||
802 | * @param integer $where |
||
803 | * |
||
804 | * @return integer |
||
805 | */ |
||
806 | protected function seek($where = null) |
||
807 | { |
||
808 | if ($where === null) { |
||
809 | return $this->count; |
||
810 | } |
||
811 | |||
812 | $this->count = $where; |
||
813 | |||
814 | return true; |
||
815 | } |
||
816 | |||
817 | /** |
||
818 | * Match string looking for either ending delim, escape, or string interpolation |
||
819 | * |
||
820 | * {@internal This is a workaround for preg_match's 250K string match limit. }} |
||
821 | * |
||
822 | * @param array $m Matches (passed by reference) |
||
823 | * @param string $delim Delimeter |
||
824 | * |
||
825 | * @return boolean True if match; false otherwise |
||
826 | */ |
||
827 | protected function matchString(&$m, $delim) |
||
828 | { |
||
829 | $token = null; |
||
830 | |||
831 | $end = strlen($this->buffer); |
||
832 | |||
833 | // look for either ending delim, escape, or string interpolation |
||
834 | foreach (['#{', '\\', $delim] as $lookahead) { |
||
835 | $pos = strpos($this->buffer, $lookahead, $this->count); |
||
836 | |||
837 | if ($pos !== false && $pos < $end) { |
||
838 | $end = $pos; |
||
839 | $token = $lookahead; |
||
840 | } |
||
841 | } |
||
842 | |||
843 | if (! isset($token)) { |
||
844 | return false; |
||
845 | } |
||
846 | |||
847 | $match = substr($this->buffer, $this->count, $end - $this->count); |
||
848 | $m = [ |
||
849 | $match . $token, |
||
850 | $match, |
||
851 | $token |
||
852 | ]; |
||
853 | $this->count = $end + strlen($token); |
||
854 | |||
855 | return true; |
||
856 | } |
||
857 | |||
858 | /** |
||
859 | * Try to match something on head of buffer |
||
860 | * |
||
861 | * @param string $regex |
||
862 | * @param array $out |
||
863 | * @param boolean $eatWhitespace |
||
864 | * |
||
865 | * @return boolean |
||
866 | */ |
||
867 | protected function match($regex, &$out, $eatWhitespace = null) |
||
868 | { |
||
869 | if (! isset($eatWhitespace)) { |
||
870 | $eatWhitespace = $this->eatWhiteDefault; |
||
871 | } |
||
872 | |||
873 | $r = '/' . $regex . '/' . $this->patternModifiers; |
||
874 | |||
875 | if (preg_match($r, $this->buffer, $out, null, $this->count)) { |
||
876 | $this->count += strlen($out[0]); |
||
877 | |||
878 | if ($eatWhitespace) { |
||
879 | $this->whitespace(); |
||
880 | } |
||
881 | |||
882 | return true; |
||
883 | } |
||
884 | |||
885 | return false; |
||
886 | } |
||
887 | |||
888 | /** |
||
889 | * Match literal string |
||
890 | * |
||
891 | * @param string $what |
||
892 | * @param boolean $eatWhitespace |
||
893 | * |
||
894 | * @return boolean |
||
895 | */ |
||
896 | protected function literal($what, $eatWhitespace = null) |
||
897 | { |
||
898 | if (! isset($eatWhitespace)) { |
||
899 | $eatWhitespace = $this->eatWhiteDefault; |
||
900 | } |
||
901 | |||
902 | $len = strlen($what); |
||
903 | |||
904 | if (strcasecmp(substr($this->buffer, $this->count, $len), $what) === 0) { |
||
905 | $this->count += $len; |
||
906 | |||
907 | if ($eatWhitespace) { |
||
908 | $this->whitespace(); |
||
909 | } |
||
910 | |||
911 | return true; |
||
912 | } |
||
913 | |||
914 | return false; |
||
915 | } |
||
916 | |||
917 | /** |
||
918 | * Match some whitespace |
||
919 | * |
||
920 | * @return boolean |
||
921 | */ |
||
922 | protected function whitespace() |
||
923 | { |
||
924 | $gotWhite = false; |
||
925 | |||
926 | while (preg_match(static::$whitePattern, $this->buffer, $m, null, $this->count)) { |
||
927 | if (isset($m[1]) && empty($this->commentsSeen[$this->count])) { |
||
928 | $this->appendComment([Type::T_COMMENT, $m[1]]); |
||
929 | |||
930 | $this->commentsSeen[$this->count] = true; |
||
931 | } |
||
932 | |||
933 | $this->count += strlen($m[0]); |
||
934 | $gotWhite = true; |
||
935 | } |
||
936 | |||
937 | return $gotWhite; |
||
938 | } |
||
939 | |||
940 | /** |
||
941 | * Append comment to current block |
||
942 | * |
||
943 | * @param array $comment |
||
944 | */ |
||
945 | protected function appendComment($comment) |
||
946 | { |
||
947 | $comment[1] = substr(preg_replace(['/^\s+/m', '/^(.)/m'], ['', ' \1'], $comment[1]), 1); |
||
948 | |||
949 | $this->env->comments[] = $comment; |
||
950 | } |
||
951 | |||
952 | /** |
||
953 | * Append statement to current block |
||
954 | * |
||
955 | * @param array $statement |
||
956 | * @param integer $pos |
||
957 | */ |
||
958 | protected function append($statement, $pos = null) |
||
959 | { |
||
960 | if ($pos !== null) { |
||
961 | list($line, $column) = $this->getSourcePosition($pos); |
||
962 | |||
963 | $statement[static::SOURCE_LINE] = $line; |
||
964 | $statement[static::SOURCE_COLUMN] = $column; |
||
965 | $statement[static::SOURCE_INDEX] = $this->sourceIndex; |
||
966 | } |
||
967 | |||
968 | $this->env->children[] = $statement; |
||
969 | |||
970 | $comments = $this->env->comments; |
||
971 | |||
972 | if (count($comments)) { |
||
973 | $this->env->children = array_merge($this->env->children, $comments); |
||
974 | $this->env->comments = []; |
||
975 | } |
||
976 | } |
||
977 | |||
978 | /** |
||
979 | * Returns last child was appended |
||
980 | * |
||
981 | * @return array|null |
||
982 | */ |
||
983 | protected function last() |
||
984 | { |
||
985 | $i = count($this->env->children) - 1; |
||
986 | |||
987 | if (isset($this->env->children[$i])) { |
||
988 | return $this->env->children[$i]; |
||
989 | } |
||
990 | } |
||
991 | |||
992 | /** |
||
993 | * Parse media query list |
||
994 | * |
||
995 | * @param array $out |
||
996 | * |
||
997 | * @return boolean |
||
998 | */ |
||
999 | protected function mediaQueryList(&$out) |
||
1000 | { |
||
1001 | return $this->genericList($out, 'mediaQuery', ',', false); |
||
1002 | } |
||
1003 | |||
1004 | /** |
||
1005 | * Parse media query |
||
1006 | * |
||
1007 | * @param array $out |
||
1008 | * |
||
1009 | * @return boolean |
||
1010 | */ |
||
1011 | protected function mediaQuery(&$out) |
||
1012 | { |
||
1013 | $expressions = null; |
||
1014 | $parts = []; |
||
1015 | |||
1016 | if (($this->literal('only') && ($only = true) || $this->literal('not') && ($not = true) || true) && |
||
1017 | $this->mixedKeyword($mediaType) |
||
1018 | ) { |
||
1019 | $prop = [Type::T_MEDIA_TYPE]; |
||
1020 | |||
1021 | if (isset($only)) { |
||
1022 | $prop[] = [Type::T_KEYWORD, 'only']; |
||
1023 | } |
||
1024 | |||
1025 | if (isset($not)) { |
||
1026 | $prop[] = [Type::T_KEYWORD, 'not']; |
||
1027 | } |
||
1028 | |||
1029 | $media = [Type::T_LIST, '', []]; |
||
1030 | |||
1031 | foreach ((array) $mediaType as $type) { |
||
1032 | if (is_array($type)) { |
||
1033 | $media[2][] = $type; |
||
1034 | } else { |
||
1035 | $media[2][] = [Type::T_KEYWORD, $type]; |
||
1036 | } |
||
1037 | } |
||
1038 | |||
1039 | $prop[] = $media; |
||
1040 | $parts[] = $prop; |
||
1041 | } |
||
1042 | |||
1043 | if (empty($parts) || $this->literal('and')) { |
||
1044 | $this->genericList($expressions, 'mediaExpression', 'and', false); |
||
1045 | |||
1046 | if (is_array($expressions)) { |
||
1047 | $parts = array_merge($parts, $expressions[2]); |
||
1048 | } |
||
1049 | } |
||
1050 | |||
1051 | $out = $parts; |
||
1052 | |||
1053 | return true; |
||
1054 | } |
||
1055 | |||
1056 | /** |
||
1057 | * Parse media expression |
||
1058 | * |
||
1059 | * @param array $out |
||
1060 | * |
||
1061 | * @return boolean |
||
1062 | */ |
||
1063 | protected function mediaExpression(&$out) |
||
1064 | { |
||
1065 | $s = $this->seek(); |
||
1066 | $value = null; |
||
1067 | |||
1068 | if ($this->literal('(') && |
||
1069 | $this->expression($feature) && |
||
1070 | ($this->literal(':') && $this->expression($value) || true) && |
||
1071 | $this->literal(')') |
||
1072 | ) { |
||
1073 | $out = [Type::T_MEDIA_EXPRESSION, $feature]; |
||
1074 | |||
1075 | if ($value) { |
||
1076 | $out[] = $value; |
||
1077 | } |
||
1078 | |||
1079 | return true; |
||
1080 | } |
||
1081 | |||
1082 | $this->seek($s); |
||
1083 | |||
1084 | return false; |
||
1085 | } |
||
1086 | |||
1087 | /** |
||
1088 | * Parse argument values |
||
1089 | * |
||
1090 | * @param array $out |
||
1091 | * |
||
1092 | * @return boolean |
||
1093 | */ |
||
1094 | protected function argValues(&$out) |
||
1095 | { |
||
1096 | if ($this->genericList($list, 'argValue', ',', false)) { |
||
1097 | $out = $list[2]; |
||
1098 | |||
1099 | return true; |
||
1100 | } |
||
1101 | |||
1102 | return false; |
||
1103 | } |
||
1104 | |||
1105 | /** |
||
1106 | * Parse argument value |
||
1107 | * |
||
1108 | * @param array $out |
||
1109 | * |
||
1110 | * @return boolean |
||
1111 | */ |
||
1112 | protected function argValue(&$out) |
||
1113 | { |
||
1114 | $s = $this->seek(); |
||
1115 | |||
1116 | $keyword = null; |
||
1117 | |||
1118 | if (! $this->variable($keyword) || ! $this->literal(':')) { |
||
1119 | $this->seek($s); |
||
1120 | $keyword = null; |
||
1121 | } |
||
1122 | |||
1123 | if ($this->genericList($value, 'expression')) { |
||
1124 | $out = [$keyword, $value, false]; |
||
1125 | $s = $this->seek(); |
||
1126 | |||
1127 | if ($this->literal('...')) { |
||
1128 | $out[2] = true; |
||
1129 | } else { |
||
1130 | $this->seek($s); |
||
1131 | } |
||
1132 | |||
1133 | return true; |
||
1134 | } |
||
1135 | |||
1136 | return false; |
||
1137 | } |
||
1138 | |||
1139 | /** |
||
1140 | * Parse comma separated value list |
||
1141 | * |
||
1142 | * @param string $out |
||
1143 | * |
||
1144 | * @return boolean |
||
1145 | */ |
||
1146 | protected function valueList(&$out) |
||
1147 | { |
||
1148 | return $this->genericList($out, 'spaceList', ','); |
||
1149 | } |
||
1150 | |||
1151 | /** |
||
1152 | * Parse space separated value list |
||
1153 | * |
||
1154 | * @param array $out |
||
1155 | * |
||
1156 | * @return boolean |
||
1157 | */ |
||
1158 | protected function spaceList(&$out) |
||
1159 | { |
||
1160 | return $this->genericList($out, 'expression'); |
||
1161 | } |
||
1162 | |||
1163 | /** |
||
1164 | * Parse generic list |
||
1165 | * |
||
1166 | * @param array $out |
||
1167 | * @param callable $parseItem |
||
1168 | * @param string $delim |
||
1169 | * @param boolean $flatten |
||
1170 | * |
||
1171 | * @return boolean |
||
1172 | */ |
||
1173 | protected function genericList(&$out, $parseItem, $delim = '', $flatten = true) |
||
1174 | { |
||
1175 | $s = $this->seek(); |
||
1176 | $items = []; |
||
1177 | |||
1178 | while ($this->$parseItem($value)) { |
||
1179 | $items[] = $value; |
||
1180 | |||
1181 | if ($delim) { |
||
1182 | if (! $this->literal($delim)) { |
||
1183 | break; |
||
1184 | } |
||
1185 | } |
||
1186 | } |
||
1187 | |||
1188 | if (count($items) === 0) { |
||
1189 | $this->seek($s); |
||
1190 | |||
1191 | return false; |
||
1192 | } |
||
1193 | |||
1194 | if ($flatten && count($items) === 1) { |
||
1195 | $out = $items[0]; |
||
1196 | } else { |
||
1197 | $out = [Type::T_LIST, $delim, $items]; |
||
1198 | } |
||
1199 | |||
1200 | return true; |
||
1201 | } |
||
1202 | |||
1203 | /** |
||
1204 | * Parse expression |
||
1205 | * |
||
1206 | * @param array $out |
||
1207 | * |
||
1208 | * @return boolean |
||
1209 | */ |
||
1210 | protected function expression(&$out) |
||
1211 | { |
||
1212 | $s = $this->seek(); |
||
1213 | |||
1214 | if ($this->literal('(')) { |
||
1215 | if ($this->literal(')')) { |
||
1216 | $out = [Type::T_LIST, '', []]; |
||
1217 | |||
1218 | return true; |
||
1219 | } |
||
1220 | |||
1221 | if ($this->valueList($out) && $this->literal(')') && $out[0] === Type::T_LIST) { |
||
1222 | return true; |
||
1223 | } |
||
1224 | |||
1225 | $this->seek($s); |
||
1226 | |||
1227 | if ($this->map($out)) { |
||
1228 | return true; |
||
1229 | } |
||
1230 | |||
1231 | $this->seek($s); |
||
1232 | } |
||
1233 | |||
1234 | if ($this->value($lhs)) { |
||
1235 | $out = $this->expHelper($lhs, 0); |
||
1236 | |||
1237 | return true; |
||
1238 | } |
||
1239 | |||
1240 | return false; |
||
1241 | } |
||
1242 | |||
1243 | /** |
||
1244 | * Parse left-hand side of subexpression |
||
1245 | * |
||
1246 | * @param array $lhs |
||
1247 | * @param integer $minP |
||
1248 | * |
||
1249 | * @return array |
||
1250 | */ |
||
1251 | protected function expHelper($lhs, $minP) |
||
1252 | { |
||
1253 | $operators = static::$operatorPattern; |
||
1254 | |||
1255 | $ss = $this->seek(); |
||
1256 | $whiteBefore = isset($this->buffer[$this->count - 1]) && |
||
1257 | ctype_space($this->buffer[$this->count - 1]); |
||
1258 | |||
1259 | while ($this->match($operators, $m, false) && static::$precedence[$m[1]] >= $minP) { |
||
1260 | $whiteAfter = isset($this->buffer[$this->count]) && |
||
1261 | ctype_space($this->buffer[$this->count]); |
||
1262 | $varAfter = isset($this->buffer[$this->count]) && |
||
1263 | $this->buffer[$this->count] === '$'; |
||
1264 | |||
1265 | $this->whitespace(); |
||
1266 | |||
1267 | $op = $m[1]; |
||
1268 | |||
1269 | // don't turn negative numbers into expressions |
||
1270 | if ($op === '-' && $whiteBefore && ! $whiteAfter && ! $varAfter) { |
||
1271 | break; |
||
1272 | } |
||
1273 | |||
1274 | if (! $this->value($rhs)) { |
||
1275 | break; |
||
1276 | } |
||
1277 | |||
1278 | // peek and see if rhs belongs to next operator |
||
1279 | if ($this->peek($operators, $next) && static::$precedence[$next[1]] > static::$precedence[$op]) { |
||
1280 | $rhs = $this->expHelper($rhs, static::$precedence[$next[1]]); |
||
1281 | } |
||
1282 | |||
1283 | $lhs = [Type::T_EXPRESSION, $op, $lhs, $rhs, $this->inParens, $whiteBefore, $whiteAfter]; |
||
1284 | $ss = $this->seek(); |
||
1285 | $whiteBefore = isset($this->buffer[$this->count - 1]) && |
||
1286 | ctype_space($this->buffer[$this->count - 1]); |
||
1287 | } |
||
1288 | |||
1289 | $this->seek($ss); |
||
1290 | |||
1291 | return $lhs; |
||
1292 | } |
||
1293 | |||
1294 | /** |
||
1295 | * Parse value |
||
1296 | * |
||
1297 | * @param array $out |
||
1298 | * |
||
1299 | * @return boolean |
||
1300 | */ |
||
1301 | protected function value(&$out) |
||
1302 | { |
||
1303 | $s = $this->seek(); |
||
1304 | |||
1305 | if ($this->literal('not', false) && $this->whitespace() && $this->value($inner)) { |
||
1306 | $out = [Type::T_UNARY, 'not', $inner, $this->inParens]; |
||
1307 | |||
1308 | return true; |
||
1309 | } |
||
1310 | |||
1311 | $this->seek($s); |
||
1312 | |||
1313 | if ($this->literal('not', false) && $this->parenValue($inner)) { |
||
1314 | $out = [Type::T_UNARY, 'not', $inner, $this->inParens]; |
||
1315 | |||
1316 | return true; |
||
1317 | } |
||
1318 | |||
1319 | $this->seek($s); |
||
1320 | |||
1321 | if ($this->literal('+') && $this->value($inner)) { |
||
1322 | $out = [Type::T_UNARY, '+', $inner, $this->inParens]; |
||
1323 | |||
1324 | return true; |
||
1325 | } |
||
1326 | |||
1327 | $this->seek($s); |
||
1328 | |||
1329 | // negation |
||
1330 | if ($this->literal('-', false) && |
||
1331 | ($this->variable($inner) || |
||
1332 | $this->unit($inner) || |
||
1333 | $this->parenValue($inner)) |
||
1334 | ) { |
||
1335 | $out = [Type::T_UNARY, '-', $inner, $this->inParens]; |
||
1336 | |||
1337 | return true; |
||
1338 | } |
||
1339 | |||
1340 | $this->seek($s); |
||
1341 | |||
1342 | if ($this->parenValue($out) || |
||
1343 | $this->interpolation($out) || |
||
1344 | $this->variable($out) || |
||
1345 | $this->color($out) || |
||
1346 | $this->unit($out) || |
||
1347 | $this->string($out) || |
||
1348 | $this->func($out) || |
||
1349 | $this->progid($out) |
||
1350 | ) { |
||
1351 | return true; |
||
1352 | } |
||
1353 | |||
1354 | if ($this->keyword($keyword)) { |
||
1355 | if ($keyword === 'null') { |
||
1356 | $out = [Type::T_NULL]; |
||
1357 | } else { |
||
1358 | $out = [Type::T_KEYWORD, $keyword]; |
||
1359 | } |
||
1360 | |||
1361 | return true; |
||
1362 | } |
||
1363 | |||
1364 | return false; |
||
1365 | } |
||
1366 | |||
1367 | /** |
||
1368 | * Parse parenthesized value |
||
1369 | * |
||
1370 | * @param array $out |
||
1371 | * |
||
1372 | * @return boolean |
||
1373 | */ |
||
1374 | protected function parenValue(&$out) |
||
1375 | { |
||
1376 | $s = $this->seek(); |
||
1377 | |||
1378 | $inParens = $this->inParens; |
||
1379 | |||
1380 | if ($this->literal('(')) { |
||
1381 | if ($this->literal(')')) { |
||
1382 | $out = [Type::T_LIST, '', []]; |
||
1383 | |||
1384 | return true; |
||
1385 | } |
||
1386 | |||
1387 | $this->inParens = true; |
||
1388 | |||
1389 | if ($this->expression($exp) && $this->literal(')')) { |
||
1390 | $out = $exp; |
||
1391 | $this->inParens = $inParens; |
||
1392 | |||
1393 | return true; |
||
1394 | } |
||
1395 | } |
||
1396 | |||
1397 | $this->inParens = $inParens; |
||
1398 | $this->seek($s); |
||
1399 | |||
1400 | return false; |
||
1401 | } |
||
1402 | |||
1403 | /** |
||
1404 | * Parse "progid:" |
||
1405 | * |
||
1406 | * @param array $out |
||
1407 | * |
||
1408 | * @return boolean |
||
1409 | */ |
||
1410 | protected function progid(&$out) |
||
1411 | { |
||
1412 | $s = $this->seek(); |
||
1413 | |||
1414 | if ($this->literal('progid:', false) && |
||
1415 | $this->openString('(', $fn) && |
||
1416 | $this->literal('(') |
||
1417 | ) { |
||
1418 | $this->openString(')', $args, '('); |
||
1419 | |||
1420 | if ($this->literal(')')) { |
||
1421 | $out = [Type::T_STRING, '', [ |
||
1422 | 'progid:', $fn, '(', $args, ')' |
||
1423 | ]]; |
||
1424 | |||
1425 | return true; |
||
1426 | } |
||
1427 | } |
||
1428 | |||
1429 | $this->seek($s); |
||
1430 | |||
1431 | return false; |
||
1432 | } |
||
1433 | |||
1434 | /** |
||
1435 | * Parse function call |
||
1436 | * |
||
1437 | * @param array $out |
||
1438 | * |
||
1439 | * @return boolean |
||
1440 | */ |
||
1441 | protected function func(&$func) |
||
1442 | { |
||
1443 | $s = $this->seek(); |
||
1444 | |||
1445 | if ($this->keyword($name, false) && |
||
1446 | $this->literal('(') |
||
1447 | ) { |
||
1448 | if ($name === 'alpha' && $this->argumentList($args)) { |
||
1449 | $func = [Type::T_FUNCTION, $name, [Type::T_STRING, '', $args]]; |
||
1450 | |||
1451 | return true; |
||
1452 | } |
||
1453 | |||
1454 | if ($name !== 'expression' && ! preg_match('/^(-[a-z]+-)?calc$/', $name)) { |
||
1455 | $ss = $this->seek(); |
||
1456 | |||
1457 | if ($this->argValues($args) && $this->literal(')')) { |
||
1458 | $func = [Type::T_FUNCTION_CALL, $name, $args]; |
||
1459 | |||
1460 | return true; |
||
1461 | } |
||
1462 | |||
1463 | $this->seek($ss); |
||
1464 | } |
||
1465 | |||
1466 | if (($this->openString(')', $str, '(') || true) && |
||
1467 | $this->literal(')') |
||
1468 | ) { |
||
1469 | $args = []; |
||
1470 | |||
1471 | if (! empty($str)) { |
||
1472 | $args[] = [null, [Type::T_STRING, '', [$str]]]; |
||
1473 | } |
||
1474 | |||
1475 | $func = [Type::T_FUNCTION_CALL, $name, $args]; |
||
1476 | |||
1477 | return true; |
||
1478 | } |
||
1479 | } |
||
1480 | |||
1481 | $this->seek($s); |
||
1482 | |||
1483 | return false; |
||
1484 | } |
||
1485 | |||
1486 | /** |
||
1487 | * Parse function call argument list |
||
1488 | * |
||
1489 | * @param array $out |
||
1490 | * |
||
1491 | * @return boolean |
||
1492 | */ |
||
1493 | protected function argumentList(&$out) |
||
1494 | { |
||
1495 | $s = $this->seek(); |
||
1496 | $this->literal('('); |
||
1497 | |||
1498 | $args = []; |
||
1499 | |||
1500 | while ($this->keyword($var)) { |
||
1501 | if ($this->literal('=') && $this->expression($exp)) { |
||
1502 | $args[] = [Type::T_STRING, '', [$var . '=']]; |
||
1503 | $arg = $exp; |
||
1504 | } else { |
||
1505 | break; |
||
1506 | } |
||
1507 | |||
1508 | $args[] = $arg; |
||
1509 | |||
1510 | if (! $this->literal(',')) { |
||
1511 | break; |
||
1512 | } |
||
1513 | |||
1514 | $args[] = [Type::T_STRING, '', [', ']]; |
||
1515 | } |
||
1516 | |||
1517 | if (! $this->literal(')') || ! count($args)) { |
||
1518 | $this->seek($s); |
||
1519 | |||
1520 | return false; |
||
1521 | } |
||
1522 | |||
1523 | $out = $args; |
||
1524 | |||
1525 | return true; |
||
1526 | } |
||
1527 | |||
1528 | /** |
||
1529 | * Parse mixin/function definition argument list |
||
1530 | * |
||
1531 | * @param array $out |
||
1532 | * |
||
1533 | * @return boolean |
||
1534 | */ |
||
1535 | protected function argumentDef(&$out) |
||
1536 | { |
||
1537 | $s = $this->seek(); |
||
1538 | $this->literal('('); |
||
1539 | |||
1540 | $args = []; |
||
1541 | |||
1542 | while ($this->variable($var)) { |
||
1543 | $arg = [$var[1], null, false]; |
||
1544 | |||
1545 | $ss = $this->seek(); |
||
1546 | |||
1547 | if ($this->literal(':') && $this->genericList($defaultVal, 'expression')) { |
||
1548 | $arg[1] = $defaultVal; |
||
1549 | } else { |
||
1550 | $this->seek($ss); |
||
1551 | } |
||
1552 | |||
1553 | $ss = $this->seek(); |
||
1554 | |||
1555 | if ($this->literal('...')) { |
||
1556 | $sss = $this->seek(); |
||
1557 | |||
1558 | if (! $this->literal(')')) { |
||
1559 | $this->throwParseError('... has to be after the final argument'); |
||
1560 | } |
||
1561 | |||
1562 | $arg[2] = true; |
||
1563 | $this->seek($sss); |
||
1564 | } else { |
||
1565 | $this->seek($ss); |
||
1566 | } |
||
1567 | |||
1568 | $args[] = $arg; |
||
1569 | |||
1570 | if (! $this->literal(',')) { |
||
1571 | break; |
||
1572 | } |
||
1573 | } |
||
1574 | |||
1575 | if (! $this->literal(')')) { |
||
1576 | $this->seek($s); |
||
1577 | |||
1578 | return false; |
||
1579 | } |
||
1580 | |||
1581 | $out = $args; |
||
1582 | |||
1583 | return true; |
||
1584 | } |
||
1585 | |||
1586 | /** |
||
1587 | * Parse map |
||
1588 | * |
||
1589 | * @param array $out |
||
1590 | * |
||
1591 | * @return boolean |
||
1592 | */ |
||
1593 | protected function map(&$out) |
||
1594 | { |
||
1595 | $s = $this->seek(); |
||
1596 | |||
1597 | if (! $this->literal('(')) { |
||
1598 | return false; |
||
1599 | } |
||
1600 | |||
1601 | $keys = []; |
||
1602 | $values = []; |
||
1603 | |||
1604 | while ($this->genericList($key, 'expression') && $this->literal(':') && |
||
1605 | $this->genericList($value, 'expression') |
||
1606 | ) { |
||
1607 | $keys[] = $key; |
||
1608 | $values[] = $value; |
||
1609 | |||
1610 | if (! $this->literal(',')) { |
||
1611 | break; |
||
1612 | } |
||
1613 | } |
||
1614 | |||
1615 | if (! count($keys) || ! $this->literal(')')) { |
||
1616 | $this->seek($s); |
||
1617 | |||
1618 | return false; |
||
1619 | } |
||
1620 | |||
1621 | $out = [Type::T_MAP, $keys, $values]; |
||
1622 | |||
1623 | return true; |
||
1624 | } |
||
1625 | |||
1626 | /** |
||
1627 | * Parse color |
||
1628 | * |
||
1629 | * @param array $out |
||
1630 | * |
||
1631 | * @return boolean |
||
1653 | } |
||
1654 | |||
1655 | $out = $color; |
||
1656 | |||
1657 | return true; |
||
1658 | } |
||
1659 | |||
1660 | return false; |
||
1661 | } |
||
1662 | |||
1663 | /** |
||
1664 | * Parse number with unit |
||
1665 | * |
||
1666 | * @param array $out |
||
1667 | * |
||
1668 | * @return boolean |
||
1669 | */ |
||
1670 | protected function unit(&$unit) |
||
1671 | { |
||
1672 | if ($this->match('([0-9]*(\.)?[0-9]+)([%a-zA-Z]+)?', $m)) { |
||
1673 | $unit = new Node\Number($m[1], empty($m[3]) ? '' : $m[3]); |
||
1674 | |||
1675 | return true; |
||
1676 | } |
||
1677 | |||
1678 | return false; |
||
1679 | } |
||
1680 | |||
1681 | /** |
||
1682 | * Parse string |
||
1683 | * |
||
1684 | * @param array $out |
||
1685 | * |
||
1686 | * @return boolean |
||
1687 | */ |
||
1688 | protected function string(&$out) |
||
1689 | { |
||
1690 | $s = $this->seek(); |
||
1691 | |||
1692 | if ($this->literal('"', false)) { |
||
1693 | $delim = '"'; |
||
1694 | } elseif ($this->literal("'", false)) { |
||
1695 | $delim = "'"; |
||
1696 | } else { |
||
1697 | return false; |
||
1698 | } |
||
1699 | |||
1700 | $content = []; |
||
1701 | $oldWhite = $this->eatWhiteDefault; |
||
1702 | $this->eatWhiteDefault = false; |
||
1703 | $hasInterpolation = false; |
||
1704 | |||
1705 | while ($this->matchString($m, $delim)) { |
||
1706 | if ($m[1] !== '') { |
||
1707 | $content[] = $m[1]; |
||
1708 | } |
||
1709 | |||
1710 | if ($m[2] === '#{') { |
||
1711 | $this->count -= strlen($m[2]); |
||
1712 | |||
1713 | if ($this->interpolation($inter, false)) { |
||
1714 | $content[] = $inter; |
||
1715 | $hasInterpolation = true; |
||
1716 | } else { |
||
1717 | $this->count += strlen($m[2]); |
||
1718 | $content[] = '#{'; // ignore it |
||
1719 | } |
||
1720 | } elseif ($m[2] === '\\') { |
||
1721 | if ($this->literal('"', false)) { |
||
1722 | $content[] = $m[2] . '"'; |
||
1723 | } elseif ($this->literal("'", false)) { |
||
1724 | $content[] = $m[2] . "'"; |
||
1725 | } else { |
||
1726 | $content[] = $m[2]; |
||
1727 | } |
||
1728 | } else { |
||
1729 | $this->count -= strlen($delim); |
||
1730 | break; // delim |
||
1731 | } |
||
1732 | } |
||
1733 | |||
1734 | $this->eatWhiteDefault = $oldWhite; |
||
1735 | |||
1736 | if ($this->literal($delim)) { |
||
1737 | if ($hasInterpolation) { |
||
1738 | $delim = '"'; |
||
1739 | |||
1740 | foreach ($content as &$string) { |
||
1741 | if ($string === "\\'") { |
||
1742 | $string = "'"; |
||
1743 | } elseif ($string === '\\"') { |
||
1744 | $string = '"'; |
||
1745 | } |
||
1746 | } |
||
1747 | } |
||
1748 | |||
1749 | $out = [Type::T_STRING, $delim, $content]; |
||
1750 | |||
1751 | return true; |
||
1752 | } |
||
1753 | |||
1754 | $this->seek($s); |
||
1755 | |||
1756 | return false; |
||
1757 | } |
||
1758 | |||
1759 | /** |
||
1760 | * Parse keyword or interpolation |
||
1761 | * |
||
1762 | * @param array $out |
||
1763 | * |
||
1764 | * @return boolean |
||
1765 | */ |
||
1766 | protected function mixedKeyword(&$out) |
||
1767 | { |
||
1768 | $parts = []; |
||
1769 | |||
1770 | $oldWhite = $this->eatWhiteDefault; |
||
1771 | $this->eatWhiteDefault = false; |
||
1772 | |||
1773 | for (;;) { |
||
1774 | if ($this->keyword($key)) { |
||
1775 | $parts[] = $key; |
||
1776 | continue; |
||
1777 | } |
||
1778 | |||
1779 | if ($this->interpolation($inter)) { |
||
1780 | $parts[] = $inter; |
||
1781 | continue; |
||
1782 | } |
||
1783 | |||
1784 | break; |
||
1785 | } |
||
1786 | |||
1787 | $this->eatWhiteDefault = $oldWhite; |
||
1788 | |||
1789 | if (count($parts) === 0) { |
||
1790 | return false; |
||
1791 | } |
||
1792 | |||
1793 | if ($this->eatWhiteDefault) { |
||
1794 | $this->whitespace(); |
||
1795 | } |
||
1796 | |||
1797 | $out = $parts; |
||
1798 | |||
1799 | return true; |
||
1800 | } |
||
1801 | |||
1802 | /** |
||
1803 | * Parse an unbounded string stopped by $end |
||
1804 | * |
||
1805 | * @param string $end |
||
1806 | * @param array $out |
||
1807 | * @param string $nestingOpen |
||
1808 | * |
||
1809 | * @return boolean |
||
1810 | */ |
||
1811 | protected function openString($end, &$out, $nestingOpen = null) |
||
1812 | { |
||
1813 | $oldWhite = $this->eatWhiteDefault; |
||
1814 | $this->eatWhiteDefault = false; |
||
1815 | |||
1816 | $patt = '(.*?)([\'"]|#\{|' . $this->pregQuote($end) . '|' . static::$commentPattern . ')'; |
||
1817 | |||
1818 | $nestingLevel = 0; |
||
1819 | |||
1820 | $content = []; |
||
1821 | |||
1822 | while ($this->match($patt, $m, false)) { |
||
1823 | if (isset($m[1]) && $m[1] !== '') { |
||
1824 | $content[] = $m[1]; |
||
1825 | |||
1826 | if ($nestingOpen) { |
||
1827 | $nestingLevel += substr_count($m[1], $nestingOpen); |
||
1828 | } |
||
1829 | } |
||
1830 | |||
1831 | $tok = $m[2]; |
||
1832 | |||
1833 | $this->count-= strlen($tok); |
||
1834 | |||
1835 | if ($tok === $end && ! $nestingLevel--) { |
||
1836 | break; |
||
1837 | } |
||
1838 | |||
1839 | if (($tok === "'" || $tok === '"') && $this->string($str)) { |
||
1840 | $content[] = $str; |
||
1841 | continue; |
||
1842 | } |
||
1843 | |||
1844 | if ($tok === '#{' && $this->interpolation($inter)) { |
||
1845 | $content[] = $inter; |
||
1846 | continue; |
||
1847 | } |
||
1848 | |||
1849 | $content[] = $tok; |
||
1850 | $this->count+= strlen($tok); |
||
1851 | } |
||
1852 | |||
1853 | $this->eatWhiteDefault = $oldWhite; |
||
1854 | |||
1855 | if (count($content) === 0) { |
||
1856 | return false; |
||
1857 | } |
||
1858 | |||
1859 | // trim the end |
||
1860 | if (is_string(end($content))) { |
||
1861 | $content[count($content) - 1] = rtrim(end($content)); |
||
1862 | } |
||
1863 | |||
1864 | $out = [Type::T_STRING, '', $content]; |
||
1865 | |||
1866 | return true; |
||
1867 | } |
||
1868 | |||
1869 | /** |
||
1870 | * Parser interpolation |
||
1871 | * |
||
1872 | * @param array $out |
||
1873 | * @param boolean $lookWhite save information about whitespace before and after |
||
1874 | * |
||
1875 | * @return boolean |
||
1876 | */ |
||
1877 | protected function interpolation(&$out, $lookWhite = true) |
||
1878 | { |
||
1879 | $oldWhite = $this->eatWhiteDefault; |
||
1880 | $this->eatWhiteDefault = true; |
||
1881 | |||
1882 | $s = $this->seek(); |
||
1883 | |||
1884 | if ($this->literal('#{') && $this->valueList($value) && $this->literal('}', false)) { |
||
1885 | if ($lookWhite) { |
||
1886 | $left = preg_match('/\s/', $this->buffer[$s - 1]) ? ' ' : ''; |
||
1887 | $right = preg_match('/\s/', $this->buffer[$this->count]) ? ' ': ''; |
||
1888 | } else { |
||
1889 | $left = $right = false; |
||
1890 | } |
||
1891 | |||
1892 | $out = [Type::T_INTERPOLATE, $value, $left, $right]; |
||
1893 | $this->eatWhiteDefault = $oldWhite; |
||
1894 | |||
1895 | if ($this->eatWhiteDefault) { |
||
1896 | $this->whitespace(); |
||
1897 | } |
||
1898 | |||
1899 | return true; |
||
1900 | } |
||
1901 | |||
1902 | $this->seek($s); |
||
1903 | $this->eatWhiteDefault = $oldWhite; |
||
1904 | |||
1905 | return false; |
||
1906 | } |
||
1907 | |||
1908 | /** |
||
1909 | * Parse property name (as an array of parts or a string) |
||
1910 | * |
||
1911 | * @param array $out |
||
1912 | * |
||
1913 | * @return boolean |
||
1914 | */ |
||
1915 | protected function propertyName(&$out) |
||
1916 | { |
||
1917 | $parts = []; |
||
1918 | |||
1919 | $oldWhite = $this->eatWhiteDefault; |
||
1920 | $this->eatWhiteDefault = false; |
||
1921 | |||
1922 | for (;;) { |
||
1923 | if ($this->interpolation($inter)) { |
||
1924 | $parts[] = $inter; |
||
1925 | continue; |
||
1926 | } |
||
1927 | |||
1928 | if ($this->keyword($text)) { |
||
1929 | $parts[] = $text; |
||
1930 | continue; |
||
1931 | } |
||
1932 | |||
1933 | if (count($parts) === 0 && $this->match('[:.#]', $m, false)) { |
||
1934 | // css hacks |
||
1935 | $parts[] = $m[0]; |
||
1936 | continue; |
||
1937 | } |
||
1938 | |||
1939 | break; |
||
1940 | } |
||
1941 | |||
1942 | $this->eatWhiteDefault = $oldWhite; |
||
1943 | |||
1944 | if (count($parts) === 0) { |
||
1945 | return false; |
||
1946 | } |
||
1947 | |||
1948 | // match comment hack |
||
1949 | if (preg_match( |
||
1950 | static::$whitePattern, |
||
1951 | $this->buffer, |
||
1952 | $m, |
||
1953 | null, |
||
1954 | $this->count |
||
1955 | )) { |
||
1956 | if (! empty($m[0])) { |
||
1957 | $parts[] = $m[0]; |
||
1958 | $this->count += strlen($m[0]); |
||
1959 | } |
||
1960 | } |
||
1961 | |||
1962 | $this->whitespace(); // get any extra whitespace |
||
1963 | |||
1964 | $out = [Type::T_STRING, '', $parts]; |
||
1965 | |||
1966 | return true; |
||
1967 | } |
||
1968 | |||
1969 | /** |
||
1970 | * Parse comma separated selector list |
||
1971 | * |
||
1972 | * @param array $out |
||
1973 | * |
||
1974 | * @return boolean |
||
1975 | */ |
||
1976 | protected function selectors(&$out) |
||
1977 | { |
||
1978 | $s = $this->seek(); |
||
1979 | $selectors = []; |
||
1980 | |||
1981 | while ($this->selector($sel)) { |
||
1982 | $selectors[] = $sel; |
||
1983 | |||
1984 | if (! $this->literal(',')) { |
||
1985 | break; |
||
1986 | } |
||
1987 | |||
1988 | while ($this->literal(',')) { |
||
1989 | ; // ignore extra |
||
1990 | } |
||
1991 | } |
||
1992 | |||
1993 | if (count($selectors) === 0) { |
||
1994 | $this->seek($s); |
||
1995 | |||
1996 | return false; |
||
1997 | } |
||
1998 | |||
1999 | $out = $selectors; |
||
2000 | |||
2001 | return true; |
||
2002 | } |
||
2003 | |||
2004 | /** |
||
2005 | * Parse whitespace separated selector list |
||
2006 | * |
||
2007 | * @param array $out |
||
2008 | * |
||
2009 | * @return boolean |
||
2010 | */ |
||
2011 | protected function selector(&$out) |
||
2012 | { |
||
2013 | $selector = []; |
||
2014 | |||
2015 | for (;;) { |
||
2016 | if ($this->match('[>+~]+', $m)) { |
||
2017 | $selector[] = [$m[0]]; |
||
2018 | continue; |
||
2019 | } |
||
2020 | |||
2021 | if ($this->selectorSingle($part)) { |
||
2022 | $selector[] = $part; |
||
2023 | $this->match('\s+', $m); |
||
2024 | continue; |
||
2025 | } |
||
2026 | |||
2027 | if ($this->match('\/[^\/]+\/', $m)) { |
||
2028 | $selector[] = [$m[0]]; |
||
2029 | continue; |
||
2030 | } |
||
2031 | |||
2032 | break; |
||
2033 | } |
||
2034 | |||
2035 | if (count($selector) === 0) { |
||
2036 | return false; |
||
2037 | } |
||
2038 | |||
2039 | $out = $selector; |
||
2040 | return true; |
||
2041 | } |
||
2042 | |||
2043 | /** |
||
2044 | * Parse the parts that make up a selector |
||
2045 | * |
||
2046 | * {@internal |
||
2047 | * div[yes=no]#something.hello.world:nth-child(-2n+1)%placeholder |
||
2048 | * }} |
||
2049 | * |
||
2050 | * @param array $out |
||
2051 | * |
||
2052 | * @return boolean |
||
2053 | */ |
||
2054 | protected function selectorSingle(&$out) |
||
2055 | { |
||
2056 | $oldWhite = $this->eatWhiteDefault; |
||
2057 | $this->eatWhiteDefault = false; |
||
2058 | |||
2059 | $parts = []; |
||
2060 | |||
2061 | if ($this->literal('*', false)) { |
||
2062 | $parts[] = '*'; |
||
2063 | } |
||
2064 | |||
2065 | for (;;) { |
||
2066 | // see if we can stop early |
||
2067 | if ($this->match('\s*[{,]', $m)) { |
||
2068 | $this->count--; |
||
2069 | break; |
||
2070 | } |
||
2071 | |||
2072 | $s = $this->seek(); |
||
2073 | |||
2074 | // self |
||
2075 | if ($this->literal('&', false)) { |
||
2076 | $parts[] = Compiler::$selfSelector; |
||
2077 | continue; |
||
2078 | } |
||
2079 | |||
2080 | if ($this->literal('.', false)) { |
||
2081 | $parts[] = '.'; |
||
2082 | continue; |
||
2083 | } |
||
2084 | |||
2085 | if ($this->literal('|', false)) { |
||
2086 | $parts[] = '|'; |
||
2087 | continue; |
||
2088 | } |
||
2089 | |||
2090 | if ($this->match('\\\\\S', $m)) { |
||
2091 | $parts[] = $m[0]; |
||
2092 | continue; |
||
2093 | } |
||
2094 | |||
2095 | // for keyframes |
||
2096 | if ($this->unit($unit)) { |
||
2097 | $parts[] = $unit; |
||
2098 | continue; |
||
2099 | } |
||
2100 | |||
2101 | if ($this->keyword($name)) { |
||
2102 | $parts[] = $name; |
||
2103 | continue; |
||
2104 | } |
||
2105 | |||
2106 | if ($this->interpolation($inter)) { |
||
2107 | $parts[] = $inter; |
||
2108 | continue; |
||
2109 | } |
||
2110 | |||
2111 | if ($this->literal('%', false) && $this->placeholder($placeholder)) { |
||
2112 | $parts[] = '%'; |
||
2113 | $parts[] = $placeholder; |
||
2114 | continue; |
||
2115 | } |
||
2116 | |||
2117 | if ($this->literal('#', false)) { |
||
2118 | $parts[] = '#'; |
||
2119 | continue; |
||
2120 | } |
||
2121 | |||
2122 | // a pseudo selector |
||
2123 | if ($this->match('::?', $m) && $this->mixedKeyword($nameParts)) { |
||
2124 | $parts[] = $m[0]; |
||
2125 | |||
2126 | foreach ($nameParts as $sub) { |
||
2127 | $parts[] = $sub; |
||
2128 | } |
||
2129 | |||
2130 | $ss = $this->seek(); |
||
2131 | |||
2132 | if ($this->literal('(') && |
||
2133 | ($this->openString(')', $str, '(') || true) && |
||
2134 | $this->literal(')') |
||
2135 | ) { |
||
2136 | $parts[] = '('; |
||
2137 | |||
2138 | if (! empty($str)) { |
||
2139 | $parts[] = $str; |
||
2140 | } |
||
2141 | |||
2142 | $parts[] = ')'; |
||
2143 | } else { |
||
2144 | $this->seek($ss); |
||
2145 | } |
||
2146 | |||
2147 | continue; |
||
2148 | } |
||
2149 | |||
2150 | $this->seek($s); |
||
2151 | |||
2152 | // attribute selector |
||
2153 | if ($this->literal('[') && |
||
2154 | ($this->openString(']', $str, '[') || true) && |
||
2155 | $this->literal(']') |
||
2156 | ) { |
||
2157 | $parts[] = '['; |
||
2158 | |||
2159 | if (! empty($str)) { |
||
2160 | $parts[] = $str; |
||
2161 | } |
||
2162 | |||
2163 | $parts[] = ']'; |
||
2164 | |||
2165 | continue; |
||
2166 | } |
||
2167 | |||
2168 | $this->seek($s); |
||
2169 | |||
2170 | break; |
||
2171 | } |
||
2172 | |||
2173 | $this->eatWhiteDefault = $oldWhite; |
||
2174 | |||
2175 | if (count($parts) === 0) { |
||
2176 | return false; |
||
2177 | } |
||
2178 | |||
2179 | $out = $parts; |
||
2180 | |||
2181 | return true; |
||
2182 | } |
||
2183 | |||
2184 | /** |
||
2185 | * Parse a variable |
||
2186 | * |
||
2187 | * @param array $out |
||
2188 | * |
||
2189 | * @return boolean |
||
2190 | */ |
||
2191 | protected function variable(&$out) |
||
2192 | { |
||
2193 | $s = $this->seek(); |
||
2194 | |||
2195 | if ($this->literal('$', false) && $this->keyword($name)) { |
||
2196 | $out = [Type::T_VARIABLE, $name]; |
||
2197 | |||
2198 | return true; |
||
2199 | } |
||
2200 | |||
2201 | $this->seek($s); |
||
2202 | |||
2203 | return false; |
||
2204 | } |
||
2205 | |||
2206 | /** |
||
2207 | * Parse a keyword |
||
2208 | * |
||
2209 | * @param string $word |
||
2210 | * @param boolean $eatWhitespace |
||
2211 | * |
||
2212 | * @return boolean |
||
2213 | */ |
||
2214 | protected function keyword(&$word, $eatWhitespace = null) |
||
2215 | { |
||
2216 | if ($this->match( |
||
2217 | $this->utf8 |
||
2218 | ? '(([\pL\w_\-\*!"\']|[\\\\].)([\pL\w\-_"\']|[\\\\].)*)' |
||
2219 | : '(([\w_\-\*!"\']|[\\\\].)([\w\-_"\']|[\\\\].)*)', |
||
2220 | $m, |
||
2221 | $eatWhitespace |
||
2222 | )) { |
||
2223 | $word = $m[1]; |
||
2224 | |||
2225 | return true; |
||
2226 | } |
||
2227 | |||
2228 | return false; |
||
2229 | } |
||
2230 | |||
2231 | /** |
||
2232 | * Parse a placeholder |
||
2233 | * |
||
2234 | * @param string $placeholder |
||
2235 | * |
||
2236 | * @return boolean |
||
2237 | */ |
||
2238 | protected function placeholder(&$placeholder) |
||
2239 | { |
||
2240 | if ($this->match( |
||
2241 | $this->utf8 |
||
2242 | ? '([\pL\w\-_]+|#[{][$][\pL\w\-_]+[}])' |
||
2243 | : '([\w\-_]+|#[{][$][\w\-_]+[}])', |
||
2244 | $m |
||
2245 | )) { |
||
2246 | $placeholder = $m[1]; |
||
2247 | |||
2248 | return true; |
||
2249 | } |
||
2250 | |||
2251 | return false; |
||
2252 | } |
||
2253 | |||
2254 | /** |
||
2255 | * Parse a url |
||
2256 | * |
||
2257 | * @param array $out |
||
2258 | * |
||
2259 | * @return boolean |
||
2260 | */ |
||
2261 | protected function url(&$out) |
||
2262 | { |
||
2263 | if ($this->match('(url\(\s*(["\']?)([^)]+)\2\s*\))', $m)) { |
||
2264 | $out = [Type::T_STRING, '', ['url(' . $m[2] . $m[3] . $m[2] . ')']]; |
||
2265 | |||
2266 | return true; |
||
2267 | } |
||
2268 | |||
2269 | return false; |
||
2270 | } |
||
2271 | |||
2272 | /** |
||
2273 | * Consume an end of statement delimiter |
||
2274 | * |
||
2275 | * @return boolean |
||
2276 | */ |
||
2277 | protected function end() |
||
2278 | { |
||
2279 | if ($this->literal(';')) { |
||
2280 | return true; |
||
2311 | |||
2312 | $flags[] = $lastNode[1]; |
||
2313 | |||
2314 | $lastNode = $node; |
||
2315 | } |
||
2316 | } |
||
2317 | |||
2318 | return $flags; |
||
2319 | } |
||
2320 | |||
2321 | /** |
||
2322 | * Strip optional flag from selector list |
||
2323 | * |
||
2324 | * @param array $selectors |
||
2325 | * |
||
2326 | * @return string |
||
2327 | */ |
||
2328 | protected function stripOptionalFlag(&$selectors) |
||
2329 | { |
||
2330 | $optional = false; |
||
2331 | |||
2332 | $selector = end($selectors); |
||
2333 | $part = end($selector); |
||
2334 | |||
2335 | if ($part === ['!optional']) { |
||
2336 | array_pop($selectors[count($selectors) - 1]); |
||
2337 | |||
2338 | $optional = true; |
||
2339 | } |
||
2340 | |||
2341 | return $optional; |
||
2342 | } |
||
2343 | |||
2344 | /** |
||
2345 | * Turn list of length 1 into value type |
||
2346 | * |
||
2347 | * @param array $value |
||
2348 | * |
||
2349 | * @return array |
||
2350 | */ |
||
2351 | protected function flattenList($value) |
||
2352 | { |
||
2353 | if ($value[0] === Type::T_LIST && count($value[2]) === 1) { |
||
2354 | return $this->flattenList($value[2][0]); |
||
2355 | } |
||
2356 | |||
2357 | return $value; |
||
2358 | } |
||
2359 | |||
2360 | /** |
||
2361 | * @deprecated |
||
2362 | * |
||
2363 | * {@internal |
||
2364 | * advance counter to next occurrence of $what |
||
2365 | * $until - don't include $what in advance |
||
2366 | * $allowNewline, if string, will be used as valid char set |
||
2367 | * }} |
||
2368 | */ |
||
2369 | protected function to($what, &$out, $until = false, $allowNewline = false) |
||
2370 | { |
||
2371 | if (is_string($allowNewline)) { |
||
2372 | $validChars = $allowNewline; |
||
2373 | } else { |
||
2374 | $validChars = $allowNewline ? '.' : "[^\n]"; |
||
2375 | } |
||
2376 | |||
2377 | if (! $this->match('(' . $validChars . '*?)' . $this->pregQuote($what), $m, ! $until)) { |
||
2378 | return false; |
||
2379 | } |
||
2380 | |||
2381 | if ($until) { |
||
2382 | $this->count -= strlen($what); // give back $what |
||
2383 | } |
||
2384 | |||
2385 | $out = $m[1]; |
||
2386 | |||
2387 | return true; |
||
2388 | } |
||
2389 | |||
2390 | /** |
||
2391 | * @deprecated |
||
2392 | */ |
||
2393 | protected function show() |
||
2394 | { |
||
2395 | if ($this->peek("(.*?)(\n|$)", $m, $this->count)) { |
||
2396 | return $m[1]; |
||
2397 | } |
||
2398 | |||
2399 | return ''; |
||
2400 | } |
||
2401 | |||
2402 | /** |
||
2403 | * Quote regular expression |
||
2404 | * |
||
2405 | * @param string $what |
||
2406 | * |
||
2407 | * @return string |
||
2408 | */ |
||
2409 | private function pregQuote($what) |
||
2410 | { |
||
2411 | return preg_quote($what, '/'); |
||
2412 | } |
||
2413 | |||
2414 | /** |
||
2415 | * Extract line numbers from buffer |
||
2416 | * |
||
2417 | * @param string $buffer |
||
2418 | */ |
||
2419 | private function extractLineNumbers($buffer) |
||
2420 | { |
||
2421 | $this->sourcePositions = [0 => 0]; |
||
2422 | $prev = 0; |
||
2423 | |||
2424 | while (($pos = strpos($buffer, "\n", $prev)) !== false) { |
||
2425 | $this->sourcePositions[] = $pos; |
||
2426 | $prev = $pos + 1; |
||
2427 | } |
||
2428 | |||
2429 | $this->sourcePositions[] = strlen($buffer); |
||
2430 | |||
2431 | if (substr($buffer, -1) !== "\n") { |
||
2432 | $this->sourcePositions[] = strlen($buffer) + 1; |
||
2433 | } |
||
2434 | } |
||
2435 | |||
2436 | /** |
||
2437 | * Get source line number and column (given character position in the buffer) |
||
2438 | * |
||
2439 | * @param integer $pos |
||
2440 | * |
||
2441 | * @return integer |
||
2442 | */ |
||
2443 | private function getSourcePosition($pos) |
||
2444 | { |
||
2445 | $low = 0; |
||
2446 | $high = count($this->sourcePositions); |
||
2447 | |||
2448 | while ($low < $high) { |
||
2449 | $mid = (int) (($high + $low) / 2); |
||
2450 | |||
2451 | if ($pos < $this->sourcePositions[$mid]) { |
||
2452 | $high = $mid - 1; |
||
2453 | continue; |
||
2454 | } |
||
2455 | |||
2456 | if ($pos >= $this->sourcePositions[$mid + 1]) { |
||
2457 | $low = $mid + 1; |
||
2458 | continue; |
||
2459 | } |
||
2460 | |||
2461 | return [$mid + 1, $pos - $this->sourcePositions[$mid]]; |
||
2462 | } |
||
2463 | |||
2464 | return [$low + 1, $pos - $this->sourcePositions[$low]]; |
||
2465 | } |
||
2466 | |||
2467 | /** |
||
2468 | * Save internal encoding |
||
2469 | */ |
||
2470 | private function saveEncoding() |
||
2471 | { |
||
2472 | if (version_compare(PHP_VERSION, '7.2.0') >= 0) { |
||
2473 | return; |
||
2474 | } |
||
2475 | |||
2476 | $iniDirective = 'mbstring' . '.func_overload'; // deprecated in PHP 7.2 |
||
2477 | |||
2478 | if (ini_get($iniDirective) & 2) { |
||
2479 | $this->encoding = mb_internal_encoding(); |
||
2480 | |||
2481 | mb_internal_encoding('iso-8859-1'); |
||
2482 | } |
||
2483 | } |
||
2484 | |||
2485 | /** |
||
2486 | * Restore internal encoding |
||
2487 | */ |
||
2488 | private function restoreEncoding() |
||
2489 | { |
||
2490 | if ($this->encoding) { |
||
2491 | mb_internal_encoding($this->encoding); |
||
2492 | } |
||
2493 | } |
||
2494 | } |
||
2495 |