PluginProbe
MaxButtons – Create buttons / 7.10
MaxButtons – Create buttons v7.10
6.23 6.24 6.25 6.26 6.26.1 6.27 6.28 6.3 6.4 6.5 6.6 6.7 6.8 6.9 7.0 7.1 7.1.1 7.1.2 7.1.3 7.10 7.11 7.13 7.13.1 7.13.2 7.13.3 All 100 releases
maxbuttons / assets / libraries / scssphp / src / Parser.php

Parser.php in MaxButtons – Create buttons 7.10, at assets/libraries/scssphp/src/Parser.php

2,510 lines 59.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * SCSSPHP
4 *
5 * @copyright 2012-2018 Leaf Corcoran
6 *
7 * @license http://opensource.org/licenses/MIT MIT
8 *
9 * @link http://leafo.github.io/scssphp
10 */
11
12 namespace Leafo\ScssPhp;
13
14 use Leafo\ScssPhp\Block;
15 use Leafo\ScssPhp\Compiler;
16 use Leafo\ScssPhp\Exception\ParserException;
17 use Leafo\ScssPhp\Node;
18 use Leafo\ScssPhp\Type;
19
20 /**
21 * Parser
22 *
23 * @author Leaf Corcoran <leafot@gmail.com>
24 */
25 class Parser
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('url(') && $this->match('data:([a-z]+)\/([a-z0-9.+-]+);base64,', $m, false)) {
1306 $len = strspn($this->buffer, 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwyxz0123456789+/=', $this->count);
1307
1308 $this->count += $len;
1309
1310 if ($this->literal(')')) {
1311 $content = substr($this->buffer, $s, $this->count - $s);
1312 $out = [Type::T_KEYWORD, $content];
1313
1314 return true;
1315 }
1316 }
1317
1318 $this->seek($s);
1319
1320 if ($this->literal('not', false) && $this->whitespace() && $this->value($inner)) {
1321 $out = [Type::T_UNARY, 'not', $inner, $this->inParens];
1322
1323 return true;
1324 }
1325
1326 $this->seek($s);
1327
1328 if ($this->literal('not', false) && $this->parenValue($inner)) {
1329 $out = [Type::T_UNARY, 'not', $inner, $this->inParens];
1330
1331 return true;
1332 }
1333
1334 $this->seek($s);
1335
1336 if ($this->literal('+') && $this->value($inner)) {
1337 $out = [Type::T_UNARY, '+', $inner, $this->inParens];
1338
1339 return true;
1340 }
1341
1342 $this->seek($s);
1343
1344 // negation
1345 if ($this->literal('-', false) &&
1346 ($this->variable($inner) ||
1347 $this->unit($inner) ||
1348 $this->parenValue($inner))
1349 ) {
1350 $out = [Type::T_UNARY, '-', $inner, $this->inParens];
1351
1352 return true;
1353 }
1354
1355 $this->seek($s);
1356
1357 if ($this->parenValue($out) ||
1358 $this->interpolation($out) ||
1359 $this->variable($out) ||
1360 $this->color($out) ||
1361 $this->unit($out) ||
1362 $this->string($out) ||
1363 $this->func($out) ||
1364 $this->progid($out)
1365 ) {
1366 return true;
1367 }
1368
1369 if ($this->keyword($keyword)) {
1370 if ($keyword === 'null') {
1371 $out = [Type::T_NULL];
1372 } else {
1373 $out = [Type::T_KEYWORD, $keyword];
1374 }
1375
1376 return true;
1377 }
1378
1379 return false;
1380 }
1381
1382 /**
1383 * Parse parenthesized value
1384 *
1385 * @param array $out
1386 *
1387 * @return boolean
1388 */
1389 protected function parenValue(&$out)
1390 {
1391 $s = $this->seek();
1392
1393 $inParens = $this->inParens;
1394
1395 if ($this->literal('(')) {
1396 if ($this->literal(')')) {
1397 $out = [Type::T_LIST, '', []];
1398
1399 return true;
1400 }
1401
1402 $this->inParens = true;
1403
1404 if ($this->expression($exp) && $this->literal(')')) {
1405 $out = $exp;
1406 $this->inParens = $inParens;
1407
1408 return true;
1409 }
1410 }
1411
1412 $this->inParens = $inParens;
1413 $this->seek($s);
1414
1415 return false;
1416 }
1417
1418 /**
1419 * Parse "progid:"
1420 *
1421 * @param array $out
1422 *
1423 * @return boolean
1424 */
1425 protected function progid(&$out)
1426 {
1427 $s = $this->seek();
1428
1429 if ($this->literal('progid:', false) &&
1430 $this->openString('(', $fn) &&
1431 $this->literal('(')
1432 ) {
1433 $this->openString(')', $args, '(');
1434
1435 if ($this->literal(')')) {
1436 $out = [Type::T_STRING, '', [
1437 'progid:', $fn, '(', $args, ')'
1438 ]];
1439
1440 return true;
1441 }
1442 }
1443
1444 $this->seek($s);
1445
1446 return false;
1447 }
1448
1449 /**
1450 * Parse function call
1451 *
1452 * @param array $out
1453 *
1454 * @return boolean
1455 */
1456 protected function func(&$func)
1457 {
1458 $s = $this->seek();
1459
1460 if ($this->keyword($name, false) &&
1461 $this->literal('(')
1462 ) {
1463 if ($name === 'alpha' && $this->argumentList($args)) {
1464 $func = [Type::T_FUNCTION, $name, [Type::T_STRING, '', $args]];
1465
1466 return true;
1467 }
1468
1469 if ($name !== 'expression' && ! preg_match('/^(-[a-z]+-)?calc$/', $name)) {
1470 $ss = $this->seek();
1471
1472 if ($this->argValues($args) && $this->literal(')')) {
1473 $func = [Type::T_FUNCTION_CALL, $name, $args];
1474
1475 return true;
1476 }
1477
1478 $this->seek($ss);
1479 }
1480
1481 if (($this->openString(')', $str, '(') || true) &&
1482 $this->literal(')')
1483 ) {
1484 $args = [];
1485
1486 if (! empty($str)) {
1487 $args[] = [null, [Type::T_STRING, '', [$str]]];
1488 }
1489
1490 $func = [Type::T_FUNCTION_CALL, $name, $args];
1491
1492 return true;
1493 }
1494 }
1495
1496 $this->seek($s);
1497
1498 return false;
1499 }
1500
1501 /**
1502 * Parse function call argument list
1503 *
1504 * @param array $out
1505 *
1506 * @return boolean
1507 */
1508 protected function argumentList(&$out)
1509 {
1510 $s = $this->seek();
1511 $this->literal('(');
1512
1513 $args = [];
1514
1515 while ($this->keyword($var)) {
1516 if ($this->literal('=') && $this->expression($exp)) {
1517 $args[] = [Type::T_STRING, '', [$var . '=']];
1518 $arg = $exp;
1519 } else {
1520 break;
1521 }
1522
1523 $args[] = $arg;
1524
1525 if (! $this->literal(',')) {
1526 break;
1527 }
1528
1529 $args[] = [Type::T_STRING, '', [', ']];
1530 }
1531
1532 if (! $this->literal(')') || ! count($args)) {
1533 $this->seek($s);
1534
1535 return false;
1536 }
1537
1538 $out = $args;
1539
1540 return true;
1541 }
1542
1543 /**
1544 * Parse mixin/function definition argument list
1545 *
1546 * @param array $out
1547 *
1548 * @return boolean
1549 */
1550 protected function argumentDef(&$out)
1551 {
1552 $s = $this->seek();
1553 $this->literal('(');
1554
1555 $args = [];
1556
1557 while ($this->variable($var)) {
1558 $arg = [$var[1], null, false];
1559
1560 $ss = $this->seek();
1561
1562 if ($this->literal(':') && $this->genericList($defaultVal, 'expression')) {
1563 $arg[1] = $defaultVal;
1564 } else {
1565 $this->seek($ss);
1566 }
1567
1568 $ss = $this->seek();
1569
1570 if ($this->literal('...')) {
1571 $sss = $this->seek();
1572
1573 if (! $this->literal(')')) {
1574 $this->throwParseError('... has to be after the final argument');
1575 }
1576
1577 $arg[2] = true;
1578 $this->seek($sss);
1579 } else {
1580 $this->seek($ss);
1581 }
1582
1583 $args[] = $arg;
1584
1585 if (! $this->literal(',')) {
1586 break;
1587 }
1588 }
1589
1590 if (! $this->literal(')')) {
1591 $this->seek($s);
1592
1593 return false;
1594 }
1595
1596 $out = $args;
1597
1598 return true;
1599 }
1600
1601 /**
1602 * Parse map
1603 *
1604 * @param array $out
1605 *
1606 * @return boolean
1607 */
1608 protected function map(&$out)
1609 {
1610 $s = $this->seek();
1611
1612 if (! $this->literal('(')) {
1613 return false;
1614 }
1615
1616 $keys = [];
1617 $values = [];
1618
1619 while ($this->genericList($key, 'expression') && $this->literal(':') &&
1620 $this->genericList($value, 'expression')
1621 ) {
1622 $keys[] = $key;
1623 $values[] = $value;
1624
1625 if (! $this->literal(',')) {
1626 break;
1627 }
1628 }
1629
1630 if (! count($keys) || ! $this->literal(')')) {
1631 $this->seek($s);
1632
1633 return false;
1634 }
1635
1636 $out = [Type::T_MAP, $keys, $values];
1637
1638 return true;
1639 }
1640
1641 /**
1642 * Parse color
1643 *
1644 * @param array $out
1645 *
1646 * @return boolean
1647 */
1648 protected function color(&$out)
1649 {
1650 $color = [Type::T_COLOR];
1651
1652 if ($this->match('(#([0-9a-f]{6})|#([0-9a-f]{3}))', $m)) {
1653 if (isset($m[3])) {
1654 $num = hexdec($m[3]);
1655
1656 foreach ([3, 2, 1] as $i) {
1657 $t = $num & 0xf;
1658 $color[$i] = $t << 4 | $t;
1659 $num >>= 4;
1660 }
1661 } else {
1662 $num = hexdec($m[2]);
1663
1664 foreach ([3, 2, 1] as $i) {
1665 $color[$i] = $num & 0xff;
1666 $num >>= 8;
1667 }
1668 }
1669
1670 $out = $color;
1671
1672 return true;
1673 }
1674
1675 return false;
1676 }
1677
1678 /**
1679 * Parse number with unit
1680 *
1681 * @param array $out
1682 *
1683 * @return boolean
1684 */
1685 protected function unit(&$unit)
1686 {
1687 if ($this->match('([0-9]*(\.)?[0-9]+)([%a-zA-Z]+)?', $m)) {
1688 $unit = new Node\Number($m[1], empty($m[3]) ? '' : $m[3]);
1689
1690 return true;
1691 }
1692
1693 return false;
1694 }
1695
1696 /**
1697 * Parse string
1698 *
1699 * @param array $out
1700 *
1701 * @return boolean
1702 */
1703 protected function string(&$out)
1704 {
1705 $s = $this->seek();
1706
1707 if ($this->literal('"', false)) {
1708 $delim = '"';
1709 } elseif ($this->literal("'", false)) {
1710 $delim = "'";
1711 } else {
1712 return false;
1713 }
1714
1715 $content = [];
1716 $oldWhite = $this->eatWhiteDefault;
1717 $this->eatWhiteDefault = false;
1718 $hasInterpolation = false;
1719
1720 while ($this->matchString($m, $delim)) {
1721 if ($m[1] !== '') {
1722 $content[] = $m[1];
1723 }
1724
1725 if ($m[2] === '#{') {
1726 $this->count -= strlen($m[2]);
1727
1728 if ($this->interpolation($inter, false)) {
1729 $content[] = $inter;
1730 $hasInterpolation = true;
1731 } else {
1732 $this->count += strlen($m[2]);
1733 $content[] = '#{'; // ignore it
1734 }
1735 } elseif ($m[2] === '\\') {
1736 if ($this->literal('"', false)) {
1737 $content[] = $m[2] . '"';
1738 } elseif ($this->literal("'", false)) {
1739 $content[] = $m[2] . "'";
1740 } else {
1741 $content[] = $m[2];
1742 }
1743 } else {
1744 $this->count -= strlen($delim);
1745 break; // delim
1746 }
1747 }
1748
1749 $this->eatWhiteDefault = $oldWhite;
1750
1751 if ($this->literal($delim)) {
1752 if ($hasInterpolation) {
1753 $delim = '"';
1754
1755 foreach ($content as &$string) {
1756 if ($string === "\\'") {
1757 $string = "'";
1758 } elseif ($string === '\\"') {
1759 $string = '"';
1760 }
1761 }
1762 }
1763
1764 $out = [Type::T_STRING, $delim, $content];
1765
1766 return true;
1767 }
1768
1769 $this->seek($s);
1770
1771 return false;
1772 }
1773
1774 /**
1775 * Parse keyword or interpolation
1776 *
1777 * @param array $out
1778 *
1779 * @return boolean
1780 */
1781 protected function mixedKeyword(&$out)
1782 {
1783 $parts = [];
1784
1785 $oldWhite = $this->eatWhiteDefault;
1786 $this->eatWhiteDefault = false;
1787
1788 for (;;) {
1789 if ($this->keyword($key)) {
1790 $parts[] = $key;
1791 continue;
1792 }
1793
1794 if ($this->interpolation($inter)) {
1795 $parts[] = $inter;
1796 continue;
1797 }
1798
1799 break;
1800 }
1801
1802 $this->eatWhiteDefault = $oldWhite;
1803
1804 if (count($parts) === 0) {
1805 return false;
1806 }
1807
1808 if ($this->eatWhiteDefault) {
1809 $this->whitespace();
1810 }
1811
1812 $out = $parts;
1813
1814 return true;
1815 }
1816
1817 /**
1818 * Parse an unbounded string stopped by $end
1819 *
1820 * @param string $end
1821 * @param array $out
1822 * @param string $nestingOpen
1823 *
1824 * @return boolean
1825 */
1826 protected function openString($end, &$out, $nestingOpen = null)
1827 {
1828 $oldWhite = $this->eatWhiteDefault;
1829 $this->eatWhiteDefault = false;
1830
1831 $patt = '(.*?)([\'"]|#\{|' . $this->pregQuote($end) . '|' . static::$commentPattern . ')';
1832
1833 $nestingLevel = 0;
1834
1835 $content = [];
1836
1837 while ($this->match($patt, $m, false)) {
1838 if (isset($m[1]) && $m[1] !== '') {
1839 $content[] = $m[1];
1840
1841 if ($nestingOpen) {
1842 $nestingLevel += substr_count($m[1], $nestingOpen);
1843 }
1844 }
1845
1846 $tok = $m[2];
1847
1848 $this->count-= strlen($tok);
1849
1850 if ($tok === $end && ! $nestingLevel--) {
1851 break;
1852 }
1853
1854 if (($tok === "'" || $tok === '"') && $this->string($str)) {
1855 $content[] = $str;
1856 continue;
1857 }
1858
1859 if ($tok === '#{' && $this->interpolation($inter)) {
1860 $content[] = $inter;
1861 continue;
1862 }
1863
1864 $content[] = $tok;
1865 $this->count+= strlen($tok);
1866 }
1867
1868 $this->eatWhiteDefault = $oldWhite;
1869
1870 if (count($content) === 0) {
1871 return false;
1872 }
1873
1874 // trim the end
1875 if (is_string(end($content))) {
1876 $content[count($content) - 1] = rtrim(end($content));
1877 }
1878
1879 $out = [Type::T_STRING, '', $content];
1880
1881 return true;
1882 }
1883
1884 /**
1885 * Parser interpolation
1886 *
1887 * @param array $out
1888 * @param boolean $lookWhite save information about whitespace before and after
1889 *
1890 * @return boolean
1891 */
1892 protected function interpolation(&$out, $lookWhite = true)
1893 {
1894 $oldWhite = $this->eatWhiteDefault;
1895 $this->eatWhiteDefault = true;
1896
1897 $s = $this->seek();
1898
1899 if ($this->literal('#{') && $this->valueList($value) && $this->literal('}', false)) {
1900 if ($lookWhite) {
1901 $left = preg_match('/\s/', $this->buffer[$s - 1]) ? ' ' : '';
1902 $right = preg_match('/\s/', $this->buffer[$this->count]) ? ' ': '';
1903 } else {
1904 $left = $right = false;
1905 }
1906
1907 $out = [Type::T_INTERPOLATE, $value, $left, $right];
1908 $this->eatWhiteDefault = $oldWhite;
1909
1910 if ($this->eatWhiteDefault) {
1911 $this->whitespace();
1912 }
1913
1914 return true;
1915 }
1916
1917 $this->seek($s);
1918 $this->eatWhiteDefault = $oldWhite;
1919
1920 return false;
1921 }
1922
1923 /**
1924 * Parse property name (as an array of parts or a string)
1925 *
1926 * @param array $out
1927 *
1928 * @return boolean
1929 */
1930 protected function propertyName(&$out)
1931 {
1932 $parts = [];
1933
1934 $oldWhite = $this->eatWhiteDefault;
1935 $this->eatWhiteDefault = false;
1936
1937 for (;;) {
1938 if ($this->interpolation($inter)) {
1939 $parts[] = $inter;
1940 continue;
1941 }
1942
1943 if ($this->keyword($text)) {
1944 $parts[] = $text;
1945 continue;
1946 }
1947
1948 if (count($parts) === 0 && $this->match('[:.#]', $m, false)) {
1949 // css hacks
1950 $parts[] = $m[0];
1951 continue;
1952 }
1953
1954 break;
1955 }
1956
1957 $this->eatWhiteDefault = $oldWhite;
1958
1959 if (count($parts) === 0) {
1960 return false;
1961 }
1962
1963 // match comment hack
1964 if (preg_match(
1965 static::$whitePattern,
1966 $this->buffer,
1967 $m,
1968 null,
1969 $this->count
1970 )) {
1971 if (! empty($m[0])) {
1972 $parts[] = $m[0];
1973 $this->count += strlen($m[0]);
1974 }
1975 }
1976
1977 $this->whitespace(); // get any extra whitespace
1978
1979 $out = [Type::T_STRING, '', $parts];
1980
1981 return true;
1982 }
1983
1984 /**
1985 * Parse comma separated selector list
1986 *
1987 * @param array $out
1988 *
1989 * @return boolean
1990 */
1991 protected function selectors(&$out)
1992 {
1993 $s = $this->seek();
1994 $selectors = [];
1995
1996 while ($this->selector($sel)) {
1997 $selectors[] = $sel;
1998
1999 if (! $this->literal(',')) {
2000 break;
2001 }
2002
2003 while ($this->literal(',')) {
2004 ; // ignore extra
2005 }
2006 }
2007
2008 if (count($selectors) === 0) {
2009 $this->seek($s);
2010
2011 return false;
2012 }
2013
2014 $out = $selectors;
2015
2016 return true;
2017 }
2018
2019 /**
2020 * Parse whitespace separated selector list
2021 *
2022 * @param array $out
2023 *
2024 * @return boolean
2025 */
2026 protected function selector(&$out)
2027 {
2028 $selector = [];
2029
2030 for (;;) {
2031 if ($this->match('[>+~]+', $m)) {
2032 $selector[] = [$m[0]];
2033 continue;
2034 }
2035
2036 if ($this->selectorSingle($part)) {
2037 $selector[] = $part;
2038 $this->match('\s+', $m);
2039 continue;
2040 }
2041
2042 if ($this->match('\/[^\/]+\/', $m)) {
2043 $selector[] = [$m[0]];
2044 continue;
2045 }
2046
2047 break;
2048 }
2049
2050 if (count($selector) === 0) {
2051 return false;
2052 }
2053
2054 $out = $selector;
2055 return true;
2056 }
2057
2058 /**
2059 * Parse the parts that make up a selector
2060 *
2061 * {@internal
2062 * div[yes=no]#something.hello.world:nth-child(-2n+1)%placeholder
2063 * }}
2064 *
2065 * @param array $out
2066 *
2067 * @return boolean
2068 */
2069 protected function selectorSingle(&$out)
2070 {
2071 $oldWhite = $this->eatWhiteDefault;
2072 $this->eatWhiteDefault = false;
2073
2074 $parts = [];
2075
2076 if ($this->literal('*', false)) {
2077 $parts[] = '*';
2078 }
2079
2080 for (;;) {
2081 // see if we can stop early
2082 if ($this->match('\s*[{,]', $m)) {
2083 $this->count--;
2084 break;
2085 }
2086
2087 $s = $this->seek();
2088
2089 // self
2090 if ($this->literal('&', false)) {
2091 $parts[] = Compiler::$selfSelector;
2092 continue;
2093 }
2094
2095 if ($this->literal('.', false)) {
2096 $parts[] = '.';
2097 continue;
2098 }
2099
2100 if ($this->literal('|', false)) {
2101 $parts[] = '|';
2102 continue;
2103 }
2104
2105 if ($this->match('\\\\\S', $m)) {
2106 $parts[] = $m[0];
2107 continue;
2108 }
2109
2110 // for keyframes
2111 if ($this->unit($unit)) {
2112 $parts[] = $unit;
2113 continue;
2114 }
2115
2116 if ($this->keyword($name)) {
2117 $parts[] = $name;
2118 continue;
2119 }
2120
2121 if ($this->interpolation($inter)) {
2122 $parts[] = $inter;
2123 continue;
2124 }
2125
2126 if ($this->literal('%', false) && $this->placeholder($placeholder)) {
2127 $parts[] = '%';
2128 $parts[] = $placeholder;
2129 continue;
2130 }
2131
2132 if ($this->literal('#', false)) {
2133 $parts[] = '#';
2134 continue;
2135 }
2136
2137 // a pseudo selector
2138 if ($this->match('::?', $m) && $this->mixedKeyword($nameParts)) {
2139 $parts[] = $m[0];
2140
2141 foreach ($nameParts as $sub) {
2142 $parts[] = $sub;
2143 }
2144
2145 $ss = $this->seek();
2146
2147 if ($this->literal('(') &&
2148 ($this->openString(')', $str, '(') || true) &&
2149 $this->literal(')')
2150 ) {
2151 $parts[] = '(';
2152
2153 if (! empty($str)) {
2154 $parts[] = $str;
2155 }
2156
2157 $parts[] = ')';
2158 } else {
2159 $this->seek($ss);
2160 }
2161
2162 continue;
2163 }
2164
2165 $this->seek($s);
2166
2167 // attribute selector
2168 if ($this->literal('[') &&
2169 ($this->openString(']', $str, '[') || true) &&
2170 $this->literal(']')
2171 ) {
2172 $parts[] = '[';
2173
2174 if (! empty($str)) {
2175 $parts[] = $str;
2176 }
2177
2178 $parts[] = ']';
2179
2180 continue;
2181 }
2182
2183 $this->seek($s);
2184
2185 break;
2186 }
2187
2188 $this->eatWhiteDefault = $oldWhite;
2189
2190 if (count($parts) === 0) {
2191 return false;
2192 }
2193
2194 $out = $parts;
2195
2196 return true;
2197 }
2198
2199 /**
2200 * Parse a variable
2201 *
2202 * @param array $out
2203 *
2204 * @return boolean
2205 */
2206 protected function variable(&$out)
2207 {
2208 $s = $this->seek();
2209
2210 if ($this->literal('$', false) && $this->keyword($name)) {
2211 $out = [Type::T_VARIABLE, $name];
2212
2213 return true;
2214 }
2215
2216 $this->seek($s);
2217
2218 return false;
2219 }
2220
2221 /**
2222 * Parse a keyword
2223 *
2224 * @param string $word
2225 * @param boolean $eatWhitespace
2226 *
2227 * @return boolean
2228 */
2229 protected function keyword(&$word, $eatWhitespace = null)
2230 {
2231 if ($this->match(
2232 $this->utf8
2233 ? '(([\pL\w_\-\*!"\']|[\\\\].)([\pL\w\-_"\']|[\\\\].)*)'
2234 : '(([\w_\-\*!"\']|[\\\\].)([\w\-_"\']|[\\\\].)*)',
2235 $m,
2236 $eatWhitespace
2237 )) {
2238 $word = $m[1];
2239
2240 return true;
2241 }
2242
2243 return false;
2244 }
2245
2246 /**
2247 * Parse a placeholder
2248 *
2249 * @param string $placeholder
2250 *
2251 * @return boolean
2252 */
2253 protected function placeholder(&$placeholder)
2254 {
2255 if ($this->match(
2256 $this->utf8
2257 ? '([\pL\w\-_]+|#[{][$][\pL\w\-_]+[}])'
2258 : '([\w\-_]+|#[{][$][\w\-_]+[}])',
2259 $m
2260 )) {
2261 $placeholder = $m[1];
2262
2263 return true;
2264 }
2265
2266 return false;
2267 }
2268
2269 /**
2270 * Parse a url
2271 *
2272 * @param array $out
2273 *
2274 * @return boolean
2275 */
2276 protected function url(&$out)
2277 {
2278 if ($this->match('(url\(\s*(["\']?)([^)]+)\2\s*\))', $m)) {
2279 $out = [Type::T_STRING, '', ['url(' . $m[2] . $m[3] . $m[2] . ')']];
2280
2281 return true;
2282 }
2283
2284 return false;
2285 }
2286
2287 /**
2288 * Consume an end of statement delimiter
2289 *
2290 * @return boolean
2291 */
2292 protected function end()
2293 {
2294 if ($this->literal(';')) {
2295 return true;
2296 }
2297
2298 if ($this->count === strlen($this->buffer) || $this->buffer[$this->count] === '}') {
2299 // if there is end of file or a closing block next then we don't need a ;
2300 return true;
2301 }
2302
2303 return false;
2304 }
2305
2306 /**
2307 * Strip assignment flag from the list
2308 *
2309 * @param array $value
2310 *
2311 * @return array
2312 */
2313 protected function stripAssignmentFlags(&$value)
2314 {
2315 $flags = [];
2316
2317 for ($token = &$value; $token[0] === Type::T_LIST && ($s = count($token[2])); $token = &$lastNode) {
2318 $lastNode = &$token[2][$s - 1];
2319
2320 while ($lastNode[0] === Type::T_KEYWORD && in_array($lastNode[1], ['!default', '!global'])) {
2321 array_pop($token[2]);
2322
2323 $node = end($token[2]);
2324
2325 $token = $this->flattenList($token);
2326
2327 $flags[] = $lastNode[1];
2328
2329 $lastNode = $node;
2330 }
2331 }
2332
2333 return $flags;
2334 }
2335
2336 /**
2337 * Strip optional flag from selector list
2338 *
2339 * @param array $selectors
2340 *
2341 * @return string
2342 */
2343 protected function stripOptionalFlag(&$selectors)
2344 {
2345 $optional = false;
2346
2347 $selector = end($selectors);
2348 $part = end($selector);
2349
2350 if ($part === ['!optional']) {
2351 array_pop($selectors[count($selectors) - 1]);
2352
2353 $optional = true;
2354 }
2355
2356 return $optional;
2357 }
2358
2359 /**
2360 * Turn list of length 1 into value type
2361 *
2362 * @param array $value
2363 *
2364 * @return array
2365 */
2366 protected function flattenList($value)
2367 {
2368 if ($value[0] === Type::T_LIST && count($value[2]) === 1) {
2369 return $this->flattenList($value[2][0]);
2370 }
2371
2372 return $value;
2373 }
2374
2375 /**
2376 * @deprecated
2377 *
2378 * {@internal
2379 * advance counter to next occurrence of $what
2380 * $until - don't include $what in advance
2381 * $allowNewline, if string, will be used as valid char set
2382 * }}
2383 */
2384 protected function to($what, &$out, $until = false, $allowNewline = false)
2385 {
2386 if (is_string($allowNewline)) {
2387 $validChars = $allowNewline;
2388 } else {
2389 $validChars = $allowNewline ? '.' : "[^\n]";
2390 }
2391
2392 if (! $this->match('(' . $validChars . '*?)' . $this->pregQuote($what), $m, ! $until)) {
2393 return false;
2394 }
2395
2396 if ($until) {
2397 $this->count -= strlen($what); // give back $what
2398 }
2399
2400 $out = $m[1];
2401
2402 return true;
2403 }
2404
2405 /**
2406 * @deprecated
2407 */
2408 protected function show()
2409 {
2410 if ($this->peek("(.*?)(\n|$)", $m, $this->count)) {
2411 return $m[1];
2412 }
2413
2414 return '';
2415 }
2416
2417 /**
2418 * Quote regular expression
2419 *
2420 * @param string $what
2421 *
2422 * @return string
2423 */
2424 private function pregQuote($what)
2425 {
2426 return preg_quote($what, '/');
2427 }
2428
2429 /**
2430 * Extract line numbers from buffer
2431 *
2432 * @param string $buffer
2433 */
2434 private function extractLineNumbers($buffer)
2435 {
2436 $this->sourcePositions = [0 => 0];
2437 $prev = 0;
2438
2439 while (($pos = strpos($buffer, "\n", $prev)) !== false) {
2440 $this->sourcePositions[] = $pos;
2441 $prev = $pos + 1;
2442 }
2443
2444 $this->sourcePositions[] = strlen($buffer);
2445
2446 if (substr($buffer, -1) !== "\n") {
2447 $this->sourcePositions[] = strlen($buffer) + 1;
2448 }
2449 }
2450
2451 /**
2452 * Get source line number and column (given character position in the buffer)
2453 *
2454 * @param integer $pos
2455 *
2456 * @return integer
2457 */
2458 private function getSourcePosition($pos)
2459 {
2460 $low = 0;
2461 $high = count($this->sourcePositions);
2462
2463 while ($low < $high) {
2464 $mid = (int) (($high + $low) / 2);
2465
2466 if ($pos < $this->sourcePositions[$mid]) {
2467 $high = $mid - 1;
2468 continue;
2469 }
2470
2471 if ($pos >= $this->sourcePositions[$mid + 1]) {
2472 $low = $mid + 1;
2473 continue;
2474 }
2475
2476 return [$mid + 1, $pos - $this->sourcePositions[$mid]];
2477 }
2478
2479 return [$low + 1, $pos - $this->sourcePositions[$low]];
2480 }
2481
2482 /**
2483 * Save internal encoding
2484 */
2485 private function saveEncoding()
2486 {
2487 if (version_compare(PHP_VERSION, '7.2.0') >= 0) {
2488 return;
2489 }
2490
2491 $iniDirective = 'mbstring' . '.func_overload'; // deprecated in PHP 7.2
2492
2493 if (ini_get($iniDirective) & 2) {
2494 $this->encoding = mb_internal_encoding();
2495
2496 mb_internal_encoding('iso-8859-1');
2497 }
2498 }
2499
2500 /**
2501 * Restore internal encoding
2502 */
2503 private function restoreEncoding()
2504 {
2505 if ($this->encoding) {
2506 mb_internal_encoding($this->encoding);
2507 }
2508 }
2509 }
2510