Automatically generated installer lang files
[moodle.git] / lib / scssphp / Parser.php
1 <?php
2 /**
3  * SCSSPHP
4  *
5  * @copyright 2012-2019 Leaf Corcoran
6  *
7  * @license http://opensource.org/licenses/MIT MIT
8  *
9  * @link http://scssphp.github.io/scssphp
10  */
12 namespace ScssPhp\ScssPhp;
14 use ScssPhp\ScssPhp\Block;
15 use ScssPhp\ScssPhp\Cache;
16 use ScssPhp\ScssPhp\Compiler;
17 use ScssPhp\ScssPhp\Exception\ParserException;
18 use ScssPhp\ScssPhp\Node;
19 use ScssPhp\ScssPhp\Type;
21 /**
22  * Parser
23  *
24  * @author Leaf Corcoran <leafot@gmail.com>
25  */
26 class Parser
27 {
28     const SOURCE_INDEX  = -1;
29     const SOURCE_LINE   = -2;
30     const SOURCE_COLUMN = -3;
32     /**
33      * @var array
34      */
35     protected static $precedence = [
36         '='   => 0,
37         'or'  => 1,
38         'and' => 2,
39         '=='  => 3,
40         '!='  => 3,
41         '<=>' => 3,
42         '<='  => 4,
43         '>='  => 4,
44         '<'   => 4,
45         '>'   => 4,
46         '+'   => 5,
47         '-'   => 5,
48         '*'   => 6,
49         '/'   => 6,
50         '%'   => 6,
51     ];
53     protected static $commentPattern;
54     protected static $operatorPattern;
55     protected static $whitePattern;
57     protected $cache;
59     private $sourceName;
60     private $sourceIndex;
61     private $sourcePositions;
62     private $charset;
63     private $count;
64     private $env;
65     private $inParens;
66     private $eatWhiteDefault;
67     private $discardComments;
68     private $buffer;
69     private $utf8;
70     private $encoding;
71     private $patternModifiers;
72     private $commentsSeen;
74     /**
75      * Constructor
76      *
77      * @api
78      *
79      * @param string                 $sourceName
80      * @param integer                $sourceIndex
81      * @param string                 $encoding
82      * @param \ScssPhp\ScssPhp\Cache $cache
83      */
84     public function __construct($sourceName, $sourceIndex = 0, $encoding = 'utf-8', $cache = null)
85     {
86         $this->sourceName       = $sourceName ?: '(stdin)';
87         $this->sourceIndex      = $sourceIndex;
88         $this->charset          = null;
89         $this->utf8             = ! $encoding || strtolower($encoding) === 'utf-8';
90         $this->patternModifiers = $this->utf8 ? 'Aisu' : 'Ais';
91         $this->commentsSeen     = [];
92         $this->discardComments  = false;
94         if (empty(static::$operatorPattern)) {
95             static::$operatorPattern = '([*\/%+-]|[!=]\=|\>\=?|\<\=\>|\<\=?|and|or)';
97             $commentSingle      = '\/\/';
98             $commentMultiLeft   = '\/\*';
99             $commentMultiRight  = '\*\/';
101             static::$commentPattern = $commentMultiLeft . '.*?' . $commentMultiRight;
102             static::$whitePattern = $this->utf8
103                 ? '/' . $commentSingle . '[^\n]*\s*|(' . static::$commentPattern . ')\s*|\s+/AisuS'
104                 : '/' . $commentSingle . '[^\n]*\s*|(' . static::$commentPattern . ')\s*|\s+/AisS';
105         }
107         if ($cache) {
108             $this->cache = $cache;
109         }
110     }
112     /**
113      * Get source file name
114      *
115      * @api
116      *
117      * @return string
118      */
119     public function getSourceName()
120     {
121         return $this->sourceName;
122     }
124     /**
125      * Throw parser error
126      *
127      * @api
128      *
129      * @param string $msg
130      *
131      * @throws \ScssPhp\ScssPhp\Exception\ParserException
132      */
133     public function throwParseError($msg = 'parse error')
134     {
135         list($line, $column) = $this->getSourcePosition($this->count);
137         $loc = empty($this->sourceName)
138              ? "line: $line, column: $column"
139              : "$this->sourceName on line $line, at column $column";
141         if ($this->peek("(.*?)(\n|$)", $m, $this->count)) {
142             throw new ParserException("$msg: failed at `$m[1]` $loc");
143         }
145         throw new ParserException("$msg: $loc");
146     }
148     /**
149      * Parser buffer
150      *
151      * @api
152      *
153      * @param string $buffer
154      *
155      * @return \ScssPhp\ScssPhp\Block
156      */
157     public function parse($buffer)
158     {
159         if ($this->cache) {
160             $cacheKey = $this->sourceName . ":" . md5($buffer);
161             $parseOptions = [
162                 'charset' => $this->charset,
163                 'utf8' => $this->utf8,
164             ];
165             $v = $this->cache->getCache("parse", $cacheKey, $parseOptions);
167             if (! is_null($v)) {
168                 return $v;
169             }
170         }
172         // strip BOM (byte order marker)
173         if (substr($buffer, 0, 3) === "\xef\xbb\xbf") {
174             $buffer = substr($buffer, 3);
175         }
177         $this->buffer          = rtrim($buffer, "\x00..\x1f");
178         $this->count           = 0;
179         $this->env             = null;
180         $this->inParens        = false;
181         $this->eatWhiteDefault = true;
183         $this->saveEncoding();
184         $this->extractLineNumbers($buffer);
186         $this->pushBlock(null); // root block
187         $this->whitespace();
188         $this->pushBlock(null);
189         $this->popBlock();
191         while ($this->parseChunk()) {
192             ;
193         }
195         if ($this->count !== strlen($this->buffer)) {
196             $this->throwParseError();
197         }
199         if (! empty($this->env->parent)) {
200             $this->throwParseError('unclosed block');
201         }
203         if ($this->charset) {
204             array_unshift($this->env->children, $this->charset);
205         }
207         $this->restoreEncoding();
209         if ($this->cache) {
210             $this->cache->setCache("parse", $cacheKey, $this->env, $parseOptions);
211         }
213         return $this->env;
214     }
216     /**
217      * Parse a value or value list
218      *
219      * @api
220      *
221      * @param string $buffer
222      * @param string $out
223      *
224      * @return boolean
225      */
226     public function parseValue($buffer, &$out)
227     {
228         $this->count           = 0;
229         $this->env             = null;
230         $this->inParens        = false;
231         $this->eatWhiteDefault = true;
232         $this->buffer          = (string) $buffer;
234         $this->saveEncoding();
236         $list = $this->valueList($out);
238         $this->restoreEncoding();
240         return $list;
241     }
243     /**
244      * Parse a selector or selector list
245      *
246      * @api
247      *
248      * @param string $buffer
249      * @param string $out
250      *
251      * @return boolean
252      */
253     public function parseSelector($buffer, &$out)
254     {
255         $this->count           = 0;
256         $this->env             = null;
257         $this->inParens        = false;
258         $this->eatWhiteDefault = true;
259         $this->buffer          = (string) $buffer;
261         $this->saveEncoding();
263         $selector = $this->selectors($out);
265         $this->restoreEncoding();
267         return $selector;
268     }
270     /**
271      * Parse a media Query
272      *
273      * @api
274      *
275      * @param string $buffer
276      * @param string $out
277      *
278      * @return array
279      */
280     public function parseMediaQueryList($buffer, &$out)
281     {
282         $this->count           = 0;
283         $this->env             = null;
284         $this->inParens        = false;
285         $this->eatWhiteDefault = true;
286         $this->buffer          = (string) $buffer;
288         $this->saveEncoding();
291         $isMediaQuery = $this->mediaQueryList($out);
293         $this->restoreEncoding();
295         return $isMediaQuery;
296     }
298     /**
299      * Parse a single chunk off the head of the buffer and append it to the
300      * current parse environment.
301      *
302      * Returns false when the buffer is empty, or when there is an error.
303      *
304      * This function is called repeatedly until the entire document is
305      * parsed.
306      *
307      * This parser is most similar to a recursive descent parser. Single
308      * functions represent discrete grammatical rules for the language, and
309      * they are able to capture the text that represents those rules.
310      *
311      * Consider the function Compiler::keyword(). (All parse functions are
312      * structured the same.)
313      *
314      * The function takes a single reference argument. When calling the
315      * function it will attempt to match a keyword on the head of the buffer.
316      * If it is successful, it will place the keyword in the referenced
317      * argument, advance the position in the buffer, and return true. If it
318      * fails then it won't advance the buffer and it will return false.
319      *
320      * All of these parse functions are powered by Compiler::match(), which behaves
321      * the same way, but takes a literal regular expression. Sometimes it is
322      * more convenient to use match instead of creating a new function.
323      *
324      * Because of the format of the functions, to parse an entire string of
325      * grammatical rules, you can chain them together using &&.
326      *
327      * But, if some of the rules in the chain succeed before one fails, then
328      * the buffer position will be left at an invalid state. In order to
329      * avoid this, Compiler::seek() is used to remember and set buffer positions.
330      *
331      * Before parsing a chain, use $s = $this->count to remember the current
332      * position into $s. Then if a chain fails, use $this->seek($s) to
333      * go back where we started.
334      *
335      * @return boolean
336      */
337     protected function parseChunk()
338     {
339         $s = $this->count;
341         // the directives
342         if (isset($this->buffer[$this->count]) && $this->buffer[$this->count] === '@') {
343             if ($this->literal('@at-root', 8) &&
344                 ($this->selectors($selector) || true) &&
345                 ($this->map($with) || true) &&
346                 $this->matchChar('{', false)
347             ) {
348                 $atRoot = $this->pushSpecialBlock(Type::T_AT_ROOT, $s);
349                 $atRoot->selector = $selector;
350                 $atRoot->with = $with;
352                 return true;
353             }
355             $this->seek($s);
357             if ($this->literal('@media', 6) && $this->mediaQueryList($mediaQueryList) && $this->matchChar('{', false)) {
358                 $media = $this->pushSpecialBlock(Type::T_MEDIA, $s);
359                 $media->queryList = $mediaQueryList[2];
361                 return true;
362             }
364             $this->seek($s);
366             if ($this->literal('@mixin', 6) &&
367                 $this->keyword($mixinName) &&
368                 ($this->argumentDef($args) || true) &&
369                 $this->matchChar('{', false)
370             ) {
371                 $mixin = $this->pushSpecialBlock(Type::T_MIXIN, $s);
372                 $mixin->name = $mixinName;
373                 $mixin->args = $args;
375                 return true;
376             }
378             $this->seek($s);
380             if ($this->literal('@include', 8) &&
381                 $this->keyword($mixinName) &&
382                 ($this->matchChar('(') &&
383                     ($this->argValues($argValues) || true) &&
384                     $this->matchChar(')') || true) &&
385                 ($this->end() ||
386                     $this->matchChar('{') && $hasBlock = true)
387             ) {
388                 $child = [Type::T_INCLUDE, $mixinName, isset($argValues) ? $argValues : null, null];
390                 if (! empty($hasBlock)) {
391                     $include = $this->pushSpecialBlock(Type::T_INCLUDE, $s);
392                     $include->child = $child;
393                 } else {
394                     $this->append($child, $s);
395                 }
397                 return true;
398             }
400             $this->seek($s);
402             if ($this->literal('@scssphp-import-once', 20) &&
403                 $this->valueList($importPath) &&
404                 $this->end()
405             ) {
406                 $this->append([Type::T_SCSSPHP_IMPORT_ONCE, $importPath], $s);
408                 return true;
409             }
411             $this->seek($s);
413             if ($this->literal('@import', 7) &&
414                 $this->valueList($importPath) &&
415                 $this->end()
416             ) {
417                 $this->append([Type::T_IMPORT, $importPath], $s);
419                 return true;
420             }
422             $this->seek($s);
424             if ($this->literal('@import', 7) &&
425                 $this->url($importPath) &&
426                 $this->end()
427             ) {
428                 $this->append([Type::T_IMPORT, $importPath], $s);
430                 return true;
431             }
433             $this->seek($s);
435             if ($this->literal('@extend', 7) &&
436                 $this->selectors($selectors) &&
437                 $this->end()
438             ) {
439                 // check for '!flag'
440                 $optional = $this->stripOptionalFlag($selectors);
441                 $this->append([Type::T_EXTEND, $selectors, $optional], $s);
443                 return true;
444             }
446             $this->seek($s);
448             if ($this->literal('@function', 9) &&
449                 $this->keyword($fnName) &&
450                 $this->argumentDef($args) &&
451                 $this->matchChar('{', false)
452             ) {
453                 $func = $this->pushSpecialBlock(Type::T_FUNCTION, $s);
454                 $func->name = $fnName;
455                 $func->args = $args;
457                 return true;
458             }
460             $this->seek($s);
462             if ($this->literal('@break', 6) && $this->end()) {
463                 $this->append([Type::T_BREAK], $s);
465                 return true;
466             }
468             $this->seek($s);
470             if ($this->literal('@continue', 9) && $this->end()) {
471                 $this->append([Type::T_CONTINUE], $s);
473                 return true;
474             }
476             $this->seek($s);
478             if ($this->literal('@return', 7) && ($this->valueList($retVal) || true) && $this->end()) {
479                 $this->append([Type::T_RETURN, isset($retVal) ? $retVal : [Type::T_NULL]], $s);
481                 return true;
482             }
484             $this->seek($s);
486             if ($this->literal('@each', 5) &&
487                 $this->genericList($varNames, 'variable', ',', false) &&
488                 $this->literal('in', 2) &&
489                 $this->valueList($list) &&
490                 $this->matchChar('{', false)
491             ) {
492                 $each = $this->pushSpecialBlock(Type::T_EACH, $s);
494                 foreach ($varNames[2] as $varName) {
495                     $each->vars[] = $varName[1];
496                 }
498                 $each->list = $list;
500                 return true;
501             }
503             $this->seek($s);
505             if ($this->literal('@while', 6) &&
506                 $this->expression($cond) &&
507                 $this->matchChar('{', false)
508             ) {
509                 $while = $this->pushSpecialBlock(Type::T_WHILE, $s);
510                 $while->cond = $cond;
512                 return true;
513             }
515             $this->seek($s);
517             if ($this->literal('@for', 4) &&
518                 $this->variable($varName) &&
519                 $this->literal('from', 4) &&
520                 $this->expression($start) &&
521                 ($this->literal('through', 7) ||
522                     ($forUntil = true && $this->literal('to', 2))) &&
523                 $this->expression($end) &&
524                 $this->matchChar('{', false)
525             ) {
526                 $for = $this->pushSpecialBlock(Type::T_FOR, $s);
527                 $for->var = $varName[1];
528                 $for->start = $start;
529                 $for->end = $end;
530                 $for->until = isset($forUntil);
532                 return true;
533             }
535             $this->seek($s);
537             if ($this->literal('@if', 3) && $this->valueList($cond) && $this->matchChar('{', false)) {
538                 $if = $this->pushSpecialBlock(Type::T_IF, $s);
539                 $if->cond = $cond;
540                 $if->cases = [];
542                 return true;
543             }
545             $this->seek($s);
547             if ($this->literal('@debug', 6) &&
548                 $this->valueList($value) &&
549                 $this->end()
550             ) {
551                 $this->append([Type::T_DEBUG, $value], $s);
553                 return true;
554             }
556             $this->seek($s);
558             if ($this->literal('@warn', 5) &&
559                 $this->valueList($value) &&
560                 $this->end()
561             ) {
562                 $this->append([Type::T_WARN, $value], $s);
564                 return true;
565             }
567             $this->seek($s);
569             if ($this->literal('@error', 6) &&
570                 $this->valueList($value) &&
571                 $this->end()
572             ) {
573                 $this->append([Type::T_ERROR, $value], $s);
575                 return true;
576             }
578             $this->seek($s);
580             if ($this->literal('@content', 8) && $this->end()) {
581                 $this->append([Type::T_MIXIN_CONTENT], $s);
583                 return true;
584             }
586             $this->seek($s);
588             $last = $this->last();
590             if (isset($last) && $last[0] === Type::T_IF) {
591                 list(, $if) = $last;
593                 if ($this->literal('@else', 5)) {
594                     if ($this->matchChar('{', false)) {
595                         $else = $this->pushSpecialBlock(Type::T_ELSE, $s);
596                     } elseif ($this->literal('if', 2) && $this->valueList($cond) && $this->matchChar('{', false)) {
597                         $else = $this->pushSpecialBlock(Type::T_ELSEIF, $s);
598                         $else->cond = $cond;
599                     }
601                     if (isset($else)) {
602                         $else->dontAppend = true;
603                         $if->cases[] = $else;
605                         return true;
606                     }
607                 }
609                 $this->seek($s);
610             }
612             // only retain the first @charset directive encountered
613             if ($this->literal('@charset', 8) &&
614                 $this->valueList($charset) &&
615                 $this->end()
616             ) {
617                 if (! isset($this->charset)) {
618                     $statement = [Type::T_CHARSET, $charset];
620                     list($line, $column) = $this->getSourcePosition($s);
622                     $statement[static::SOURCE_LINE]   = $line;
623                     $statement[static::SOURCE_COLUMN] = $column;
624                     $statement[static::SOURCE_INDEX]  = $this->sourceIndex;
626                     $this->charset = $statement;
627                 }
629                 return true;
630             }
632             $this->seek($s);
634             if ($this->literal('@supports', 9) &&
635                 ($t1=$this->supportsQuery($supportQuery)) &&
636                 ($t2=$this->matchChar('{', false)) ) {
637                 $directive = $this->pushSpecialBlock(Type::T_DIRECTIVE, $s);
638                 $directive->name = 'supports';
639                 $directive->value = $supportQuery;
641                 return true;
642             }
644             $this->seek($s);
646             // doesn't match built in directive, do generic one
647             if ($this->matchChar('@', false) &&
648                 $this->keyword($dirName) &&
649                 ($this->variable($dirValue) || $this->openString('{', $dirValue) || true) &&
650                 $this->matchChar('{', false)
651             ) {
652                 if ($dirName === 'media') {
653                     $directive = $this->pushSpecialBlock(Type::T_MEDIA, $s);
654                 } else {
655                     $directive = $this->pushSpecialBlock(Type::T_DIRECTIVE, $s);
656                     $directive->name = $dirName;
657                 }
659                 if (isset($dirValue)) {
660                     $directive->value = $dirValue;
661                 }
663                 return true;
664             }
666             $this->seek($s);
668             return false;
669         }
671         // property shortcut
672         // captures most properties before having to parse a selector
673         if ($this->keyword($name, false) &&
674             $this->literal(': ', 2) &&
675             $this->valueList($value) &&
676             $this->end()
677         ) {
678             $name = [Type::T_STRING, '', [$name]];
679             $this->append([Type::T_ASSIGN, $name, $value], $s);
681             return true;
682         }
684         $this->seek($s);
686         // variable assigns
687         if ($this->variable($name) &&
688             $this->matchChar(':') &&
689             $this->valueList($value) &&
690             $this->end()
691         ) {
692             // check for '!flag'
693             $assignmentFlags = $this->stripAssignmentFlags($value);
694             $this->append([Type::T_ASSIGN, $name, $value, $assignmentFlags], $s);
696             return true;
697         }
699         $this->seek($s);
701         // misc
702         if ($this->literal('-->', 3)) {
703             return true;
704         }
706         // opening css block
707         if ($this->selectors($selectors) && $this->matchChar('{', false)) {
708             $this->pushBlock($selectors, $s);
710             if ($this->eatWhiteDefault) {
711                 $this->whitespace();
712                 $this->append(null); // collect comments at the begining if needed
713             }
715             return true;
716         }
718         $this->seek($s);
720         // property assign, or nested assign
721         if ($this->propertyName($name) && $this->matchChar(':')) {
722             $foundSomething = false;
724             if ($this->valueList($value)) {
725                 if (empty($this->env->parent)) {
726                     $this->throwParseError('expected "{"');
727                 }
729                 $this->append([Type::T_ASSIGN, $name, $value], $s);
730                 $foundSomething = true;
731             }
733             if ($this->matchChar('{', false)) {
734                 $propBlock = $this->pushSpecialBlock(Type::T_NESTED_PROPERTY, $s);
735                 $propBlock->prefix = $name;
736                 $propBlock->hasValue = $foundSomething;
738                 $foundSomething = true;
739             } elseif ($foundSomething) {
740                 $foundSomething = $this->end();
741             }
743             if ($foundSomething) {
744                 return true;
745             }
746         }
748         $this->seek($s);
750         // closing a block
751         if ($this->matchChar('}', false)) {
752             $block = $this->popBlock();
754             if (!isset($block->type) || $block->type !== Type::T_IF) {
755                 if ($this->env->parent) {
756                     $this->append(null); // collect comments before next statement if needed
757                 }
758             }
760             if (isset($block->type) && $block->type === Type::T_INCLUDE) {
761                 $include = $block->child;
762                 unset($block->child);
763                 $include[3] = $block;
764                 $this->append($include, $s);
765             } elseif (empty($block->dontAppend)) {
766                 $type = isset($block->type) ? $block->type : Type::T_BLOCK;
767                 $this->append([$type, $block], $s);
768             }
770             // collect comments just after the block closing if needed
771             if ($this->eatWhiteDefault) {
772                 $this->whitespace();
774                 if ($this->env->comments) {
775                     $this->append(null);
776                 }
777             }
779             return true;
780         }
782         // extra stuff
783         if ($this->matchChar(';') ||
784             $this->literal('<!--', 4)
785         ) {
786             return true;
787         }
789         return false;
790     }
792     /**
793      * Push block onto parse tree
794      *
795      * @param array   $selectors
796      * @param integer $pos
797      *
798      * @return \ScssPhp\ScssPhp\Block
799      */
800     protected function pushBlock($selectors, $pos = 0)
801     {
802         list($line, $column) = $this->getSourcePosition($pos);
804         $b = new Block;
805         $b->sourceName   = $this->sourceName;
806         $b->sourceLine   = $line;
807         $b->sourceColumn = $column;
808         $b->sourceIndex  = $this->sourceIndex;
809         $b->selectors    = $selectors;
810         $b->comments     = [];
811         $b->parent       = $this->env;
813         if (! $this->env) {
814             $b->children = [];
815         } elseif (empty($this->env->children)) {
816             $this->env->children = $this->env->comments;
817             $b->children = [];
818             $this->env->comments = [];
819         } else {
820             $b->children = $this->env->comments;
821             $this->env->comments = [];
822         }
824         $this->env = $b;
826         // collect comments at the begining of a block if needed
827         if ($this->eatWhiteDefault) {
828             $this->whitespace();
830             if ($this->env->comments) {
831                 $this->append(null);
832             }
833         }
835         return $b;
836     }
838     /**
839      * Push special (named) block onto parse tree
840      *
841      * @param string  $type
842      * @param integer $pos
843      *
844      * @return \ScssPhp\ScssPhp\Block
845      */
846     protected function pushSpecialBlock($type, $pos)
847     {
848         $block = $this->pushBlock(null, $pos);
849         $block->type = $type;
851         return $block;
852     }
854     /**
855      * Pop scope and return last block
856      *
857      * @return \ScssPhp\ScssPhp\Block
858      *
859      * @throws \Exception
860      */
861     protected function popBlock()
862     {
864         // collect comments ending just before of a block closing
865         if ($this->env->comments) {
866             $this->append(null);
867         }
869         // pop the block
870         $block = $this->env;
872         if (empty($block->parent)) {
873             $this->throwParseError('unexpected }');
874         }
876         if ($block->type == Type::T_AT_ROOT) {
877             // keeps the parent in case of self selector &
878             $block->selfParent = $block->parent;
879         }
881         $this->env = $block->parent;
883         unset($block->parent);
885         return $block;
886     }
888     /**
889      * Peek input stream
890      *
891      * @param string  $regex
892      * @param array   $out
893      * @param integer $from
894      *
895      * @return integer
896      */
897     protected function peek($regex, &$out, $from = null)
898     {
899         if (! isset($from)) {
900             $from = $this->count;
901         }
903         $r = '/' . $regex . '/' . $this->patternModifiers;
904         $result = preg_match($r, $this->buffer, $out, null, $from);
906         return $result;
907     }
909     /**
910      * Seek to position in input stream (or return current position in input stream)
911      *
912      * @param integer $where
913      */
914     protected function seek($where)
915     {
916         $this->count = $where;
917     }
919     /**
920      * Match string looking for either ending delim, escape, or string interpolation
921      *
922      * {@internal This is a workaround for preg_match's 250K string match limit. }}
923      *
924      * @param array  $m     Matches (passed by reference)
925      * @param string $delim Delimeter
926      *
927      * @return boolean True if match; false otherwise
928      */
929     protected function matchString(&$m, $delim)
930     {
931         $token = null;
933         $end = strlen($this->buffer);
935         // look for either ending delim, escape, or string interpolation
936         foreach (['#{', '\\', $delim] as $lookahead) {
937             $pos = strpos($this->buffer, $lookahead, $this->count);
939             if ($pos !== false && $pos < $end) {
940                 $end = $pos;
941                 $token = $lookahead;
942             }
943         }
945         if (! isset($token)) {
946             return false;
947         }
949         $match = substr($this->buffer, $this->count, $end - $this->count);
950         $m = [
951             $match . $token,
952             $match,
953             $token
954         ];
955         $this->count = $end + strlen($token);
957         return true;
958     }
960     /**
961      * Try to match something on head of buffer
962      *
963      * @param string  $regex
964      * @param array   $out
965      * @param boolean $eatWhitespace
966      *
967      * @return boolean
968      */
969     protected function match($regex, &$out, $eatWhitespace = null)
970     {
971         $r = '/' . $regex . '/' . $this->patternModifiers;
973         if (! preg_match($r, $this->buffer, $out, null, $this->count)) {
974             return false;
975         }
977         $this->count += strlen($out[0]);
979         if (! isset($eatWhitespace)) {
980             $eatWhitespace = $this->eatWhiteDefault;
981         }
983         if ($eatWhitespace) {
984             $this->whitespace();
985         }
987         return true;
988     }
990     /**
991      * Match a single string
992      *
993      * @param string  $char
994      * @param boolean $eatWhitespace
995      *
996      * @return boolean
997      */
998     protected function matchChar($char, $eatWhitespace = null)
999     {
1000         if (! isset($this->buffer[$this->count]) || $this->buffer[$this->count] !== $char) {
1001             return false;
1002         }
1004         $this->count++;
1006         if (! isset($eatWhitespace)) {
1007             $eatWhitespace = $this->eatWhiteDefault;
1008         }
1010         if ($eatWhitespace) {
1011             $this->whitespace();
1012         }
1014         return true;
1015     }
1017     /**
1018      * Match literal string
1019      *
1020      * @param string  $what
1021      * @param integer $len
1022      * @param boolean $eatWhitespace
1023      *
1024      * @return boolean
1025      */
1026     protected function literal($what, $len, $eatWhitespace = null)
1027     {
1028         if (strcasecmp(substr($this->buffer, $this->count, $len), $what) !== 0) {
1029             return false;
1030         }
1032         $this->count += $len;
1034         if (! isset($eatWhitespace)) {
1035             $eatWhitespace = $this->eatWhiteDefault;
1036         }
1038         if ($eatWhitespace) {
1039             $this->whitespace();
1040         }
1042         return true;
1043     }
1045     /**
1046      * Match some whitespace
1047      *
1048      * @return boolean
1049      */
1050     protected function whitespace()
1051     {
1052         $gotWhite = false;
1054         while (preg_match(static::$whitePattern, $this->buffer, $m, null, $this->count)) {
1055             if (isset($m[1]) && empty($this->commentsSeen[$this->count])) {
1056                 // comment that are kept in the output CSS
1057                 $comment = [];
1058                 $endCommentCount = $this->count + strlen($m[1]);
1060                 // find interpolations in comment
1061                 $p = strpos($this->buffer, '#{', $this->count);
1063                 while ($p !== false && $p < $endCommentCount) {
1064                     $c = substr($this->buffer, $this->count, $p - $this->count);
1065                     $comment[] = $c;
1066                     $this->count = $p;
1067                     $out = null;
1069                     if ($this->interpolation($out)) {
1070                         // keep right spaces in the following string part
1071                         if ($out[3]) {
1072                             while ($this->buffer[$this->count-1] !== '}') {
1073                                 $this->count--;
1074                             }
1076                             $out[3] = '';
1077                         }
1079                         $comment[] = $out;
1080                     } else {
1081                         $comment[] = substr($this->buffer, $this->count, 2);
1083                         $this->count += 2;
1084                     }
1086                     $p = strpos($this->buffer, '#{', $this->count);
1087                 }
1089                 // remaining part
1090                 $c = substr($this->buffer, $this->count, $endCommentCount - $this->count);
1092                 if (! $comment) {
1093                     // single part static comment
1094                     $this->appendComment([Type::T_COMMENT, $c]);
1095                 } else {
1096                     $comment[] = $c;
1097                     $this->appendComment([Type::T_COMMENT, [Type::T_STRING, '', $comment]]);
1098                 }
1100                 $this->commentsSeen[$this->count] = true;
1101                 $this->count = $endCommentCount;
1102             } else {
1103                 // comment that are ignored and not kept in the output css
1104                 $this->count += strlen($m[0]);
1105             }
1107             $gotWhite = true;
1108         }
1110         return $gotWhite;
1111     }
1113     /**
1114      * Append comment to current block
1115      *
1116      * @param array $comment
1117      */
1118     protected function appendComment($comment)
1119     {
1120         if (! $this->discardComments) {
1121             if ($comment[0] === Type::T_COMMENT && is_string($comment[1])) {
1122                 $comment[1] = substr(preg_replace(['/^\s+/m', '/^(.)/m'], ['', ' \1'], $comment[1]), 1);
1123             }
1125             $this->env->comments[] = $comment;
1126         }
1127     }
1129     /**
1130      * Append statement to current block
1131      *
1132      * @param array   $statement
1133      * @param integer $pos
1134      */
1135     protected function append($statement, $pos = null)
1136     {
1137         if (! is_null($statement)) {
1138             if ($pos !== null) {
1139                 list($line, $column) = $this->getSourcePosition($pos);
1141                 $statement[static::SOURCE_LINE]   = $line;
1142                 $statement[static::SOURCE_COLUMN] = $column;
1143                 $statement[static::SOURCE_INDEX]  = $this->sourceIndex;
1144             }
1146             $this->env->children[] = $statement;
1147         }
1149         $comments = $this->env->comments;
1151         if ($comments) {
1152             $this->env->children = array_merge($this->env->children, $comments);
1153             $this->env->comments = [];
1154         }
1155     }
1157     /**
1158      * Returns last child was appended
1159      *
1160      * @return array|null
1161      */
1162     protected function last()
1163     {
1164         $i = count($this->env->children) - 1;
1166         if (isset($this->env->children[$i])) {
1167             return $this->env->children[$i];
1168         }
1169     }
1171     /**
1172      * Parse media query list
1173      *
1174      * @param array $out
1175      *
1176      * @return boolean
1177      */
1178     protected function mediaQueryList(&$out)
1179     {
1180         return $this->genericList($out, 'mediaQuery', ',', false);
1181     }
1183     /**
1184      * Parse media query
1185      *
1186      * @param array $out
1187      *
1188      * @return boolean
1189      */
1190     protected function mediaQuery(&$out)
1191     {
1192         $expressions = null;
1193         $parts = [];
1195         if (($this->literal('only', 4) && ($only = true) || $this->literal('not', 3) && ($not = true) || true) &&
1196             $this->mixedKeyword($mediaType)
1197         ) {
1198             $prop = [Type::T_MEDIA_TYPE];
1200             if (isset($only)) {
1201                 $prop[] = [Type::T_KEYWORD, 'only'];
1202             }
1204             if (isset($not)) {
1205                 $prop[] = [Type::T_KEYWORD, 'not'];
1206             }
1208             $media = [Type::T_LIST, '', []];
1210             foreach ((array) $mediaType as $type) {
1211                 if (is_array($type)) {
1212                     $media[2][] = $type;
1213                 } else {
1214                     $media[2][] = [Type::T_KEYWORD, $type];
1215                 }
1216             }
1218             $prop[]  = $media;
1219             $parts[] = $prop;
1220         }
1222         if (empty($parts) || $this->literal('and', 3)) {
1223             $this->genericList($expressions, 'mediaExpression', 'and', false);
1225             if (is_array($expressions)) {
1226                 $parts = array_merge($parts, $expressions[2]);
1227             }
1228         }
1230         $out = $parts;
1232         return true;
1233     }
1235     /**
1236      * Parse supports query
1237      *
1238      * @param array $out
1239      *
1240      * @return boolean
1241      */
1242     protected function supportsQuery(&$out)
1243     {
1244         $expressions = null;
1245         $parts = [];
1247         $s = $this->count;
1249         $not = false;
1250         if (($this->literal('not', 3) && ($not = true) || true) &&
1251             $this->matchChar('(') &&
1252             ($this->expression($property)) &&
1253             $this->literal(': ', 2) &&
1254             $this->valueList($value) &&
1255             $this->matchChar(')')) {
1256             $support = [Type::T_STRING, '', [[Type::T_KEYWORD, ($not ? 'not ' : '') . '(']]];
1257             $support[2][] = $property;
1258             $support[2][] = [Type::T_KEYWORD, ': '];
1259             $support[2][] = $value;
1260             $support[2][] = [Type::T_KEYWORD, ')'];
1262             $parts[] = $support;
1263             $s = $this->count;
1264         } else {
1265             $this->seek($s);
1266         }
1268         if ($this->matchChar('(') &&
1269             $this->supportsQuery($subQuery) &&
1270             $this->matchChar(')')) {
1271             $parts[] = [Type::T_STRING, '', [[Type::T_KEYWORD, '('], $subQuery, [Type::T_KEYWORD, ')']]];
1272             $s = $this->count;
1273         } else {
1274             $this->seek($s);
1275         }
1277         if ($this->literal('not', 3) &&
1278             $this->supportsQuery($subQuery)) {
1279             $parts[] = [Type::T_STRING, '', [[Type::T_KEYWORD, 'not '], $subQuery]];
1280             $s = $this->count;
1281         } else {
1282             $this->seek($s);
1283         }
1285         if ($this->literal('selector(', 9) &&
1286             $this->selector($selector) &&
1287             $this->matchChar(')')) {
1288             $support = [Type::T_STRING, '', [[Type::T_KEYWORD, 'selector(']]];
1290             $selectorList = [Type::T_LIST, '', []];
1291             foreach ($selector as $sc) {
1292                 $compound = [Type::T_STRING, '', []];
1293                 foreach ($sc as $scp) {
1294                     if (is_array($scp)) {
1295                         $compound[2][] = $scp;
1296                     } else {
1297                         $compound[2][] = [Type::T_KEYWORD, $scp];
1298                     }
1299                 }
1300                 $selectorList[2][] = $compound;
1301             }
1302             $support[2][] = $selectorList;
1303             $support[2][] = [Type::T_KEYWORD, ')'];
1304             $parts[] = $support;
1305             $s = $this->count;
1306         } else {
1307             $this->seek($s);
1308         }
1310         if ($this->variable($var) or $this->interpolation($var)) {
1311             $parts[] = $var;
1312             $s = $this->count;
1313         } else {
1314             $this->seek($s);
1315         }
1317         if ($this->literal('and', 3) &&
1318             $this->genericList($expressions, 'supportsQuery', ' and', false)) {
1319             array_unshift($expressions[2], [Type::T_STRING, '', $parts]);
1320             $parts = [$expressions];
1321             $s = $this->count;
1322         } else {
1323             $this->seek($s);
1324         }
1326         if ($this->literal('or', 2) &&
1327             $this->genericList($expressions, 'supportsQuery', ' or', false)) {
1328             array_unshift($expressions[2], [Type::T_STRING, '', $parts]);
1329             $parts = [$expressions];
1330             $s = $this->count;
1331         } else {
1332             $this->seek($s);
1333         }
1335         if (count($parts)) {
1336             if ($this->eatWhiteDefault) {
1337                 $this->whitespace();
1338             }
1339             $out = [Type::T_STRING, '', $parts];
1340             return true;
1341         }
1343         return false;
1344     }
1347     /**
1348      * Parse media expression
1349      *
1350      * @param array $out
1351      *
1352      * @return boolean
1353      */
1354     protected function mediaExpression(&$out)
1355     {
1356         $s = $this->count;
1357         $value = null;
1359         if ($this->matchChar('(') &&
1360             $this->expression($feature) &&
1361             ($this->matchChar(':') && $this->expression($value) || true) &&
1362             $this->matchChar(')')
1363         ) {
1364             $out = [Type::T_MEDIA_EXPRESSION, $feature];
1366             if ($value) {
1367                 $out[] = $value;
1368             }
1370             return true;
1371         }
1373         $this->seek($s);
1375         return false;
1376     }
1378     /**
1379      * Parse argument values
1380      *
1381      * @param array $out
1382      *
1383      * @return boolean
1384      */
1385     protected function argValues(&$out)
1386     {
1387         if ($this->genericList($list, 'argValue', ',', false)) {
1388             $out = $list[2];
1390             return true;
1391         }
1393         return false;
1394     }
1396     /**
1397      * Parse argument value
1398      *
1399      * @param array $out
1400      *
1401      * @return boolean
1402      */
1403     protected function argValue(&$out)
1404     {
1405         $s = $this->count;
1407         $keyword = null;
1409         if (! $this->variable($keyword) || ! $this->matchChar(':')) {
1410             $this->seek($s);
1411             $keyword = null;
1412         }
1414         if ($this->genericList($value, 'expression')) {
1415             $out = [$keyword, $value, false];
1416             $s = $this->count;
1418             if ($this->literal('...', 3)) {
1419                 $out[2] = true;
1420             } else {
1421                 $this->seek($s);
1422             }
1424             return true;
1425         }
1427         return false;
1428     }
1430     /**
1431      * Parse comma separated value list
1432      *
1433      * @param array $out
1434      *
1435      * @return boolean
1436      */
1437     protected function valueList(&$out)
1438     {
1439         return $this->genericList($out, 'spaceList', ',');
1440     }
1442     /**
1443      * Parse space separated value list
1444      *
1445      * @param array $out
1446      *
1447      * @return boolean
1448      */
1449     protected function spaceList(&$out)
1450     {
1451         return $this->genericList($out, 'expression');
1452     }
1454     /**
1455      * Parse generic list
1456      *
1457      * @param array    $out
1458      * @param callable $parseItem
1459      * @param string   $delim
1460      * @param boolean  $flatten
1461      *
1462      * @return boolean
1463      */
1464     protected function genericList(&$out, $parseItem, $delim = '', $flatten = true)
1465     {
1466         $s = $this->count;
1467         $items = [];
1468         $value = null;
1470         while ($this->$parseItem($value)) {
1471             $items[] = $value;
1473             if ($delim) {
1474                 if (! $this->literal($delim, strlen($delim))) {
1475                     break;
1476                 }
1477             }
1478         }
1480         if (! $items) {
1481             $this->seek($s);
1483             return false;
1484         }
1486         if ($flatten && count($items) === 1) {
1487             $out = $items[0];
1488         } else {
1489             $out = [Type::T_LIST, $delim, $items];
1490         }
1492         return true;
1493     }
1495     /**
1496      * Parse expression
1497      *
1498      * @param array $out
1499      *
1500      * @return boolean
1501      */
1502     protected function expression(&$out)
1503     {
1504         $s = $this->count;
1505         $discard = $this->discardComments;
1506         $this->discardComments = true;
1508         if ($this->matchChar('(')) {
1509             if ($this->parenExpression($out, $s, ")")) {
1510                 $this->discardComments = $discard;
1511                 return true;
1512             }
1514             $this->seek($s);
1515         }
1517         if ($this->matchChar('[')) {
1518             if ($this->parenExpression($out, $s, "]", [Type::T_LIST, Type::T_KEYWORD])) {
1519                 if ($out[0] !== Type::T_LIST && $out[0] !== Type::T_MAP) {
1520                     $out = [Type::T_STRING, '', [ '[', $out, ']' ]];
1521                 }
1523                 $this->discardComments = $discard;
1524                 return true;
1525             }
1527             $this->seek($s);
1528         }
1530         if ($this->value($lhs)) {
1531             $out = $this->expHelper($lhs, 0);
1533             $this->discardComments = $discard;
1534             return true;
1535         }
1537         $this->discardComments = $discard;
1538         return false;
1539     }
1541     /**
1542      * Parse expression specifically checking for lists in parenthesis or brackets
1543      *
1544      * @param array   $out
1545      * @param integer $s
1546      * @param string  $closingParen
1547      * @param array   $allowedTypes
1548      *
1549      * @return boolean
1550      */
1551     protected function parenExpression(&$out, $s, $closingParen = ")", $allowedTypes = [Type::T_LIST, Type::T_MAP])
1552     {
1553         if ($this->matchChar($closingParen)) {
1554             $out = [Type::T_LIST, '', []];
1556             return true;
1557         }
1559         if ($this->valueList($out) && $this->matchChar($closingParen) && in_array($out[0], $allowedTypes)) {
1560             return true;
1561         }
1563         $this->seek($s);
1565         if (in_array(Type::T_MAP, $allowedTypes) && $this->map($out)) {
1566             return true;
1567         }
1569         return false;
1570     }
1572     /**
1573      * Parse left-hand side of subexpression
1574      *
1575      * @param array   $lhs
1576      * @param integer $minP
1577      *
1578      * @return array
1579      */
1580     protected function expHelper($lhs, $minP)
1581     {
1582         $operators = static::$operatorPattern;
1584         $ss = $this->count;
1585         $whiteBefore = isset($this->buffer[$this->count - 1]) &&
1586             ctype_space($this->buffer[$this->count - 1]);
1588         while ($this->match($operators, $m, false) && static::$precedence[$m[1]] >= $minP) {
1589             $whiteAfter = isset($this->buffer[$this->count]) &&
1590                 ctype_space($this->buffer[$this->count]);
1591             $varAfter = isset($this->buffer[$this->count]) &&
1592                 $this->buffer[$this->count] === '$';
1594             $this->whitespace();
1596             $op = $m[1];
1598             // don't turn negative numbers into expressions
1599             if ($op === '-' && $whiteBefore && ! $whiteAfter && ! $varAfter) {
1600                 break;
1601             }
1603             if (! $this->value($rhs)) {
1604                 break;
1605             }
1607             // peek and see if rhs belongs to next operator
1608             if ($this->peek($operators, $next) && static::$precedence[$next[1]] > static::$precedence[$op]) {
1609                 $rhs = $this->expHelper($rhs, static::$precedence[$next[1]]);
1610             }
1612             $lhs = [Type::T_EXPRESSION, $op, $lhs, $rhs, $this->inParens, $whiteBefore, $whiteAfter];
1613             $ss = $this->count;
1614             $whiteBefore = isset($this->buffer[$this->count - 1]) &&
1615                 ctype_space($this->buffer[$this->count - 1]);
1616         }
1618         $this->seek($ss);
1620         return $lhs;
1621     }
1623     /**
1624      * Parse value
1625      *
1626      * @param array $out
1627      *
1628      * @return boolean
1629      */
1630     protected function value(&$out)
1631     {
1632         if (! isset($this->buffer[$this->count])) {
1633             return false;
1634         }
1636         $s = $this->count;
1637         $char = $this->buffer[$this->count];
1639         if ($this->literal('url(', 4) && $this->match('data:([a-z]+)\/([a-z0-9.+-]+);base64,', $m, false)) {
1640             $len = strspn(
1641                 $this->buffer,
1642                 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwyxz0123456789+/=',
1643                 $this->count
1644             );
1646             $this->count += $len;
1648             if ($this->matchChar(')')) {
1649                 $content = substr($this->buffer, $s, $this->count - $s);
1650                 $out = [Type::T_KEYWORD, $content];
1652                 return true;
1653             }
1654         }
1656         $this->seek($s);
1658         if ($this->literal('url(', 4, false) && $this->match('\s*(\/\/\S+)\s*', $m)) {
1659             $content = 'url(' . $m[1];
1661             if ($this->matchChar(')')) {
1662                 $content .= ')';
1663                 $out = [Type::T_KEYWORD, $content];
1665                 return true;
1666             }
1667         }
1669         $this->seek($s);
1671         // not
1672         if ($char === 'n' && $this->literal('not', 3, false)) {
1673             if ($this->whitespace() && $this->value($inner)) {
1674                 $out = [Type::T_UNARY, 'not', $inner, $this->inParens];
1676                 return true;
1677             }
1679             $this->seek($s);
1681             if ($this->parenValue($inner)) {
1682                 $out = [Type::T_UNARY, 'not', $inner, $this->inParens];
1684                 return true;
1685             }
1687             $this->seek($s);
1688         }
1690         // addition
1691         if ($char === '+') {
1692             $this->count++;
1694             if ($this->value($inner)) {
1695                 $out = [Type::T_UNARY, '+', $inner, $this->inParens];
1697                 return true;
1698             }
1700             $this->count--;
1702             return false;
1703         }
1705         // negation
1706         if ($char === '-') {
1707             $this->count++;
1709             if ($this->variable($inner) || $this->unit($inner) || $this->parenValue($inner)) {
1710                 $out = [Type::T_UNARY, '-', $inner, $this->inParens];
1712                 return true;
1713             }
1715             $this->count--;
1716         }
1718         // paren
1719         if ($char === '(' && $this->parenValue($out)) {
1720             return true;
1721         }
1723         if ($char === '#') {
1724             if ($this->interpolation($out) || $this->color($out)) {
1725                 return true;
1726             }
1727         }
1729         if ($this->matchChar('&', true)) {
1730             $out = [Type::T_SELF];
1732             return true;
1733         }
1735         if ($char === '$' && $this->variable($out)) {
1736             return true;
1737         }
1739         if ($char === 'p' && $this->progid($out)) {
1740             return true;
1741         }
1743         if (($char === '"' || $char === "'") && $this->string($out)) {
1744             return true;
1745         }
1747         if ($this->unit($out)) {
1748             return true;
1749         }
1751         // unicode range with wildcards
1752         if ($this->literal('U+', 2) && $this->match('([0-9A-F]+\?*)(-([0-9A-F]+))?', $m, false)) {
1753             $out = [Type::T_KEYWORD, 'U+' . $m[0]];
1754             return true;
1755         }
1757         if ($this->keyword($keyword, false)) {
1758             if ($this->func($keyword, $out)) {
1759                 return true;
1760             }
1762             $this->whitespace();
1764             if ($keyword === 'null') {
1765                 $out = [Type::T_NULL];
1766             } else {
1767                 $out = [Type::T_KEYWORD, $keyword];
1768             }
1770             return true;
1771         }
1773         return false;
1774     }
1776     /**
1777      * Parse parenthesized value
1778      *
1779      * @param array $out
1780      *
1781      * @return boolean
1782      */
1783     protected function parenValue(&$out)
1784     {
1785         $s = $this->count;
1787         $inParens = $this->inParens;
1789         if ($this->matchChar('(')) {
1790             if ($this->matchChar(')')) {
1791                 $out = [Type::T_LIST, '', []];
1793                 return true;
1794             }
1796             $this->inParens = true;
1798             if ($this->expression($exp) && $this->matchChar(')')) {
1799                 $out = $exp;
1800                 $this->inParens = $inParens;
1802                 return true;
1803             }
1804         }
1806         $this->inParens = $inParens;
1807         $this->seek($s);
1809         return false;
1810     }
1812     /**
1813      * Parse "progid:"
1814      *
1815      * @param array $out
1816      *
1817      * @return boolean
1818      */
1819     protected function progid(&$out)
1820     {
1821         $s = $this->count;
1823         if ($this->literal('progid:', 7, false) &&
1824             $this->openString('(', $fn) &&
1825             $this->matchChar('(')
1826         ) {
1827             $this->openString(')', $args, '(');
1829             if ($this->matchChar(')')) {
1830                 $out = [Type::T_STRING, '', [
1831                     'progid:', $fn, '(', $args, ')'
1832                 ]];
1834                 return true;
1835             }
1836         }
1838         $this->seek($s);
1840         return false;
1841     }
1843     /**
1844      * Parse function call
1845      *
1846      * @param string $name
1847      * @param array  $func
1848      *
1849      * @return boolean
1850      */
1851     protected function func($name, &$func)
1852     {
1853         $s = $this->count;
1855         if ($this->matchChar('(')) {
1856             if ($name === 'alpha' && $this->argumentList($args)) {
1857                 $func = [Type::T_FUNCTION, $name, [Type::T_STRING, '', $args]];
1859                 return true;
1860             }
1862             if ($name !== 'expression' && ! preg_match('/^(-[a-z]+-)?calc$/', $name)) {
1863                 $ss = $this->count;
1865                 if ($this->argValues($args) && $this->matchChar(')')) {
1866                     $func = [Type::T_FUNCTION_CALL, $name, $args];
1868                     return true;
1869                 }
1871                 $this->seek($ss);
1872             }
1874             if (($this->openString(')', $str, '(') || true) &&
1875                 $this->matchChar(')')
1876             ) {
1877                 $args = [];
1879                 if (! empty($str)) {
1880                     $args[] = [null, [Type::T_STRING, '', [$str]]];
1881                 }
1883                 $func = [Type::T_FUNCTION_CALL, $name, $args];
1885                 return true;
1886             }
1887         }
1889         $this->seek($s);
1891         return false;
1892     }
1894     /**
1895      * Parse function call argument list
1896      *
1897      * @param array $out
1898      *
1899      * @return boolean
1900      */
1901     protected function argumentList(&$out)
1902     {
1903         $s = $this->count;
1904         $this->matchChar('(');
1906         $args = [];
1908         while ($this->keyword($var)) {
1909             if ($this->matchChar('=') && $this->expression($exp)) {
1910                 $args[] = [Type::T_STRING, '', [$var . '=']];
1911                 $arg = $exp;
1912             } else {
1913                 break;
1914             }
1916             $args[] = $arg;
1918             if (! $this->matchChar(',')) {
1919                 break;
1920             }
1922             $args[] = [Type::T_STRING, '', [', ']];
1923         }
1925         if (! $this->matchChar(')') || ! $args) {
1926             $this->seek($s);
1928             return false;
1929         }
1931         $out = $args;
1933         return true;
1934     }
1936     /**
1937      * Parse mixin/function definition  argument list
1938      *
1939      * @param array $out
1940      *
1941      * @return boolean
1942      */
1943     protected function argumentDef(&$out)
1944     {
1945         $s = $this->count;
1946         $this->matchChar('(');
1948         $args = [];
1950         while ($this->variable($var)) {
1951             $arg = [$var[1], null, false];
1953             $ss = $this->count;
1955             if ($this->matchChar(':') && $this->genericList($defaultVal, 'expression')) {
1956                 $arg[1] = $defaultVal;
1957             } else {
1958                 $this->seek($ss);
1959             }
1961             $ss = $this->count;
1963             if ($this->literal('...', 3)) {
1964                 $sss = $this->count;
1966                 if (! $this->matchChar(')')) {
1967                     $this->throwParseError('... has to be after the final argument');
1968                 }
1970                 $arg[2] = true;
1971                 $this->seek($sss);
1972             } else {
1973                 $this->seek($ss);
1974             }
1976             $args[] = $arg;
1978             if (! $this->matchChar(',')) {
1979                 break;
1980             }
1981         }
1983         if (! $this->matchChar(')')) {
1984             $this->seek($s);
1986             return false;
1987         }
1989         $out = $args;
1991         return true;
1992     }
1994     /**
1995      * Parse map
1996      *
1997      * @param array $out
1998      *
1999      * @return boolean
2000      */
2001     protected function map(&$out)
2002     {
2003         $s = $this->count;
2005         if (! $this->matchChar('(')) {
2006             return false;
2007         }
2009         $keys = [];
2010         $values = [];
2012         while ($this->genericList($key, 'expression') && $this->matchChar(':') &&
2013             $this->genericList($value, 'expression')
2014         ) {
2015             $keys[] = $key;
2016             $values[] = $value;
2018             if (! $this->matchChar(',')) {
2019                 break;
2020             }
2021         }
2023         if (! $keys || ! $this->matchChar(')')) {
2024             $this->seek($s);
2026             return false;
2027         }
2029         $out = [Type::T_MAP, $keys, $values];
2031         return true;
2032     }
2034     /**
2035      * Parse color
2036      *
2037      * @param array $out
2038      *
2039      * @return boolean
2040      */
2041     protected function color(&$out)
2042     {
2043         $color = [Type::T_COLOR];
2044         $s     = $this->count;
2046         if ($this->match('(#([0-9a-f]+))', $m)) {
2047             $nofValues = strlen($m[2]);
2048             $hasAlpha  = $nofValues === 4 || $nofValues === 8;
2049             $channels  = $hasAlpha ? [4, 3, 2, 1] : [3, 2, 1];
2051             switch ($nofValues) {
2052                 case 3:
2053                 case 4:
2054                     $num = hexdec($m[2]);
2056                     foreach ($channels as $i) {
2057                         $t = $num & 0xf;
2058                         $color[$i] = $t << 4 | $t;
2059                         $num >>= 4;
2060                     }
2062                     break;
2064                 case 6:
2065                 case 8:
2066                     $num = hexdec($m[2]);
2068                     foreach ($channels as $i) {
2069                         $color[$i] = $num & 0xff;
2070                         $num >>= 8;
2071                     }
2073                     break;
2075                 default:
2076                     $this->seek($s);
2078                     return false;
2079             }
2081             if ($hasAlpha) {
2082                 if ($color[4] === 255) {
2083                     $color[4] = 1; // fully opaque
2084                 } else {
2085                     $color[4] = round($color[4] / 255, 3);
2086                 }
2087             }
2089             $out = $color;
2091             return true;
2092         }
2094         return false;
2095     }
2097     /**
2098      * Parse number with unit
2099      *
2100      * @param array $unit
2101      *
2102      * @return boolean
2103      */
2104     protected function unit(&$unit)
2105     {
2106         $s = $this->count;
2108         if ($this->match('([0-9]*(\.)?[0-9]+)([%a-zA-Z]+)?', $m, false)) {
2109             if (strlen($this->buffer) === $this->count || ! ctype_digit($this->buffer[$this->count])) {
2110                 $this->whitespace();
2112                 $unit = new Node\Number($m[1], empty($m[3]) ? '' : $m[3]);
2114                 return true;
2115             }
2117             $this->seek($s);
2118         }
2120         return false;
2121     }
2123     /**
2124      * Parse string
2125      *
2126      * @param array $out
2127      *
2128      * @return boolean
2129      */
2130     protected function string(&$out)
2131     {
2132         $s = $this->count;
2134         if ($this->matchChar('"', false)) {
2135             $delim = '"';
2136         } elseif ($this->matchChar("'", false)) {
2137             $delim = "'";
2138         } else {
2139             return false;
2140         }
2142         $content = [];
2143         $oldWhite = $this->eatWhiteDefault;
2144         $this->eatWhiteDefault = false;
2145         $hasInterpolation = false;
2147         while ($this->matchString($m, $delim)) {
2148             if ($m[1] !== '') {
2149                 $content[] = $m[1];
2150             }
2152             if ($m[2] === '#{') {
2153                 $this->count -= strlen($m[2]);
2155                 if ($this->interpolation($inter, false)) {
2156                     $content[] = $inter;
2157                     $hasInterpolation = true;
2158                 } else {
2159                     $this->count += strlen($m[2]);
2160                     $content[] = '#{'; // ignore it
2161                 }
2162             } elseif ($m[2] === '\\') {
2163                 if ($this->matchChar('"', false)) {
2164                     $content[] = $m[2] . '"';
2165                 } elseif ($this->matchChar("'", false)) {
2166                     $content[] = $m[2] . "'";
2167                 } elseif ($this->literal("\\", 1, false)) {
2168                     $content[] = $m[2] . "\\";
2169                 } elseif ($this->literal("\r\n", 2, false) ||
2170                   $this->matchChar("\r", false) ||
2171                   $this->matchChar("\n", false) ||
2172                   $this->matchChar("\f", false)
2173                 ) {
2174                     // this is a continuation escaping, to be ignored
2175                 } else {
2176                     $content[] = $m[2];
2177                 }
2178             } else {
2179                 $this->count -= strlen($delim);
2180                 break; // delim
2181             }
2182         }
2184         $this->eatWhiteDefault = $oldWhite;
2186         if ($this->literal($delim, strlen($delim))) {
2187             if ($hasInterpolation) {
2188                 $delim = '"';
2190                 foreach ($content as &$string) {
2191                     if ($string === "\\\\") {
2192                         $string = "\\";
2193                     } elseif ($string === "\\'") {
2194                         $string = "'";
2195                     } elseif ($string === '\\"') {
2196                         $string = '"';
2197                     }
2198                 }
2199             }
2201             $out = [Type::T_STRING, $delim, $content];
2203             return true;
2204         }
2206         $this->seek($s);
2208         return false;
2209     }
2211     /**
2212      * Parse keyword or interpolation
2213      *
2214      * @param array   $out
2215      * @param boolean $restricted
2216      *
2217      * @return boolean
2218      */
2219     protected function mixedKeyword(&$out, $restricted = false)
2220     {
2221         $parts = [];
2223         $oldWhite = $this->eatWhiteDefault;
2224         $this->eatWhiteDefault = false;
2226         for (;;) {
2227             if ($restricted ? $this->restrictedKeyword($key) : $this->keyword($key)) {
2228                 $parts[] = $key;
2229                 continue;
2230             }
2232             if ($this->interpolation($inter)) {
2233                 $parts[] = $inter;
2234                 continue;
2235             }
2237             break;
2238         }
2240         $this->eatWhiteDefault = $oldWhite;
2242         if (! $parts) {
2243             return false;
2244         }
2246         if ($this->eatWhiteDefault) {
2247             $this->whitespace();
2248         }
2250         $out = $parts;
2252         return true;
2253     }
2255     /**
2256      * Parse an unbounded string stopped by $end
2257      *
2258      * @param string $end
2259      * @param array  $out
2260      * @param string $nestingOpen
2261      *
2262      * @return boolean
2263      */
2264     protected function openString($end, &$out, $nestingOpen = null)
2265     {
2266         $oldWhite = $this->eatWhiteDefault;
2267         $this->eatWhiteDefault = false;
2269         $patt = '(.*?)([\'"]|#\{|' . $this->pregQuote($end) . '|' . static::$commentPattern . ')';
2271         $nestingLevel = 0;
2273         $content = [];
2275         while ($this->match($patt, $m, false)) {
2276             if (isset($m[1]) && $m[1] !== '') {
2277                 $content[] = $m[1];
2279                 if ($nestingOpen) {
2280                     $nestingLevel += substr_count($m[1], $nestingOpen);
2281                 }
2282             }
2284             $tok = $m[2];
2286             $this->count-= strlen($tok);
2288             if ($tok === $end && ! $nestingLevel--) {
2289                 break;
2290             }
2292             if (($tok === "'" || $tok === '"') && $this->string($str)) {
2293                 $content[] = $str;
2294                 continue;
2295             }
2297             if ($tok === '#{' && $this->interpolation($inter)) {
2298                 $content[] = $inter;
2299                 continue;
2300             }
2302             $content[] = $tok;
2303             $this->count+= strlen($tok);
2304         }
2306         $this->eatWhiteDefault = $oldWhite;
2308         if (! $content) {
2309             return false;
2310         }
2312         // trim the end
2313         if (is_string(end($content))) {
2314             $content[count($content) - 1] = rtrim(end($content));
2315         }
2317         $out = [Type::T_STRING, '', $content];
2319         return true;
2320     }
2322     /**
2323      * Parser interpolation
2324      *
2325      * @param array   $out
2326      * @param boolean $lookWhite save information about whitespace before and after
2327      *
2328      * @return boolean
2329      */
2330     protected function interpolation(&$out, $lookWhite = true)
2331     {
2332         $oldWhite = $this->eatWhiteDefault;
2333         $this->eatWhiteDefault = true;
2335         $s = $this->count;
2337         if ($this->literal('#{', 2) && $this->valueList($value) && $this->matchChar('}', false)) {
2338             if ($value === [Type::T_SELF]) {
2339                 $out = $value;
2340             } else {
2341                 if ($lookWhite) {
2342                     $left = ($s > 0 && preg_match('/\s/', $this->buffer[$s - 1])) ? ' ' : '';
2343                     $right = preg_match('/\s/', $this->buffer[$this->count]) ? ' ': '';
2344                 } else {
2345                     $left = $right = false;
2346                 }
2348                 $out = [Type::T_INTERPOLATE, $value, $left, $right];
2349             }
2351             $this->eatWhiteDefault = $oldWhite;
2353             if ($this->eatWhiteDefault) {
2354                 $this->whitespace();
2355             }
2357             return true;
2358         }
2360         $this->seek($s);
2362         $this->eatWhiteDefault = $oldWhite;
2364         return false;
2365     }
2367     /**
2368      * Parse property name (as an array of parts or a string)
2369      *
2370      * @param array $out
2371      *
2372      * @return boolean
2373      */
2374     protected function propertyName(&$out)
2375     {
2376         $parts = [];
2378         $oldWhite = $this->eatWhiteDefault;
2379         $this->eatWhiteDefault = false;
2381         for (;;) {
2382             if ($this->interpolation($inter)) {
2383                 $parts[] = $inter;
2384                 continue;
2385             }
2387             if ($this->keyword($text)) {
2388                 $parts[] = $text;
2389                 continue;
2390             }
2392             if (! $parts && $this->match('[:.#]', $m, false)) {
2393                 // css hacks
2394                 $parts[] = $m[0];
2395                 continue;
2396             }
2398             break;
2399         }
2401         $this->eatWhiteDefault = $oldWhite;
2403         if (! $parts) {
2404             return false;
2405         }
2407         // match comment hack
2408         if (preg_match(
2409             static::$whitePattern,
2410             $this->buffer,
2411             $m,
2412             null,
2413             $this->count
2414         )) {
2415             if (! empty($m[0])) {
2416                 $parts[] = $m[0];
2417                 $this->count += strlen($m[0]);
2418             }
2419         }
2421         $this->whitespace(); // get any extra whitespace
2423         $out = [Type::T_STRING, '', $parts];
2425         return true;
2426     }
2428     /**
2429      * Parse comma separated selector list
2430      *
2431      * @param array $out
2432      *
2433      * @return boolean
2434      */
2435     protected function selectors(&$out, $subSelector = false)
2436     {
2437         $s = $this->count;
2438         $selectors = [];
2440         while ($this->selector($sel, $subSelector)) {
2441             $selectors[] = $sel;
2443             if (! $this->matchChar(',', true)) {
2444                 break;
2445             }
2447             while ($this->matchChar(',', true)) {
2448                 ; // ignore extra
2449             }
2450         }
2452         if (! $selectors) {
2453             $this->seek($s);
2455             return false;
2456         }
2458         $out = $selectors;
2460         return true;
2461     }
2463     /**
2464      * Parse whitespace separated selector list
2465      *
2466      * @param array $out
2467      *
2468      * @return boolean
2469      */
2470     protected function selector(&$out, $subSelector = false)
2471     {
2472         $selector = [];
2474         for (;;) {
2475             if ($this->match('[>+~]+', $m, true)) {
2476                 $selector[] = [$m[0]];
2477                 continue;
2478             }
2480             if ($this->selectorSingle($part, $subSelector)) {
2481                 $selector[] = $part;
2482                 $this->match('\s+', $m);
2483                 continue;
2484             }
2486             if ($this->match('\/[^\/]+\/', $m, true)) {
2487                 $selector[] = [$m[0]];
2488                 continue;
2489             }
2491             break;
2492         }
2494         if (! $selector) {
2495             return false;
2496         }
2498         $out = $selector;
2500         return true;
2501     }
2503     /**
2504      * Parse the parts that make up a selector
2505      *
2506      * {@internal
2507      *     div[yes=no]#something.hello.world:nth-child(-2n+1)%placeholder
2508      * }}
2509      *
2510      * @param array $out
2511      *
2512      * @return boolean
2513      */
2514     protected function selectorSingle(&$out, $subSelector = false)
2515     {
2516         $oldWhite = $this->eatWhiteDefault;
2517         $this->eatWhiteDefault = false;
2519         $parts = [];
2521         if ($this->matchChar('*', false)) {
2522             $parts[] = '*';
2523         }
2525         for (;;) {
2526             if (! isset($this->buffer[$this->count])) {
2527                 break;
2528             }
2530             $s = $this->count;
2531             $char = $this->buffer[$this->count];
2533             // see if we can stop early
2534             if ($char === '{' || $char === ',' || $char === ';' || $char === '}' || $char === '@') {
2535                 break;
2536             }
2538             // parsing a sub selector in () stop with the closing )
2539             if ($subSelector && $char === ')') {
2540                 break;
2541             }
2543             //self
2544             switch ($char) {
2545                 case '&':
2546                     $parts[] = Compiler::$selfSelector;
2547                     $this->count++;
2548                     continue 2;
2550                 case '.':
2551                     $parts[] = '.';
2552                     $this->count++;
2553                     continue 2;
2555                 case '|':
2556                     $parts[] = '|';
2557                     $this->count++;
2558                     continue 2;
2559             }
2561             if ($char === '\\' && $this->match('\\\\\S', $m)) {
2562                 $parts[] = $m[0];
2563                 continue;
2564             }
2566             if ($char === '%') {
2567                 $this->count++;
2569                 if ($this->placeholder($placeholder)) {
2570                     $parts[] = '%';
2571                     $parts[] = $placeholder;
2572                     continue;
2573                 }
2575                 break;
2576             }
2578             if ($char === '#') {
2579                 if ($this->interpolation($inter)) {
2580                     $parts[] = $inter;
2581                     continue;
2582                 }
2584                 $parts[] = '#';
2585                 $this->count++;
2586                 continue;
2587             }
2589             // a pseudo selector
2590             if ($char === ':') {
2591                 if ($this->buffer[$this->count + 1] === ':') {
2592                     $this->count += 2;
2593                     $part = '::';
2594                 } else {
2595                     $this->count++;
2596                     $part = ':';
2597                 }
2599                 if ($this->mixedKeyword($nameParts, true)) {
2600                     $parts[] = $part;
2602                     foreach ($nameParts as $sub) {
2603                         $parts[] = $sub;
2604                     }
2606                     $ss = $this->count;
2608                     if ($nameParts === ['not'] || $nameParts === ['is'] ||
2609                         $nameParts === ['has'] || $nameParts === ['where']
2610                     ) {
2611                         if ($this->matchChar('(') &&
2612                           ($this->selectors($subs, true) || true) &&
2613                           $this->matchChar(')')
2614                         ) {
2615                             $parts[] = '(';
2617                             while ($sub = array_shift($subs)) {
2618                                 while ($ps = array_shift($sub)) {
2619                                     foreach ($ps as &$p) {
2620                                         $parts[] = $p;
2621                                     }
2622                                     if (count($sub) && reset($sub)) {
2623                                         $parts[] = ' ';
2624                                     }
2625                                 }
2626                                 if (count($subs) && reset($subs)) {
2627                                     $parts[] = ', ';
2628                                 }
2629                             }
2631                             $parts[] = ')';
2632                         } else {
2633                             $this->seek($ss);
2634                         }
2635                     } else {
2636                         if ($this->matchChar('(') &&
2637                           ($this->openString(')', $str, '(') || true) &&
2638                           $this->matchChar(')')
2639                         ) {
2640                             $parts[] = '(';
2642                             if (! empty($str)) {
2643                                 $parts[] = $str;
2644                             }
2646                             $parts[] = ')';
2647                         } else {
2648                             $this->seek($ss);
2649                         }
2650                     }
2652                     continue;
2653                 }
2654             }
2656             $this->seek($s);
2658             // attribute selector
2659             if ($char === '[' &&
2660                 $this->matchChar('[') &&
2661                 ($this->openString(']', $str, '[') || true) &&
2662                 $this->matchChar(']')
2663             ) {
2664                 $parts[] = '[';
2666                 if (! empty($str)) {
2667                     $parts[] = $str;
2668                 }
2670                 $parts[] = ']';
2671                 continue;
2672             }
2674             $this->seek($s);
2676             // for keyframes
2677             if ($this->unit($unit)) {
2678                 $parts[] = $unit;
2679                 continue;
2680             }
2682             if ($this->restrictedKeyword($name)) {
2683                 $parts[] = $name;
2684                 continue;
2685             }
2687             break;
2688         }
2690         $this->eatWhiteDefault = $oldWhite;
2692         if (! $parts) {
2693             return false;
2694         }
2696         $out = $parts;
2698         return true;
2699     }
2701     /**
2702      * Parse a variable
2703      *
2704      * @param array $out
2705      *
2706      * @return boolean
2707      */
2708     protected function variable(&$out)
2709     {
2710         $s = $this->count;
2712         if ($this->matchChar('$', false) && $this->keyword($name)) {
2713             $out = [Type::T_VARIABLE, $name];
2715             return true;
2716         }
2718         $this->seek($s);
2720         return false;
2721     }
2723     /**
2724      * Parse a keyword
2725      *
2726      * @param string  $word
2727      * @param boolean $eatWhitespace
2728      *
2729      * @return boolean
2730      */
2731     protected function keyword(&$word, $eatWhitespace = null)
2732     {
2733         if ($this->match(
2734             $this->utf8
2735                 ? '(([\pL\w\x{00A0}-\x{10FFFF}_\-\*!"\']|[\\\\].)([\pL\w\x{00A0}-\x{10FFFF}\-_"\']|[\\\\].)*)'
2736                 : '(([\w_\-\*!"\']|[\\\\].)([\w\-_"\']|[\\\\].)*)',
2737             $m,
2738             $eatWhitespace
2739         )) {
2740             $word = $m[1];
2742             return true;
2743         }
2745         return false;
2746     }
2748     /**
2749      * Parse a keyword that should not start with a number
2750      *
2751      * @param string  $word
2752      * @param boolean $eatWhitespace
2753      *
2754      * @return boolean
2755      */
2756     protected function restrictedKeyword(&$word, $eatWhitespace = null)
2757     {
2758         $s = $this->count;
2760         if ($this->keyword($word, $eatWhitespace) && (ord($word[0]) > 57 || ord($word[0]) < 48)) {
2761             return true;
2762         }
2764         $this->seek($s);
2766         return false;
2767     }
2769     /**
2770      * Parse a placeholder
2771      *
2772      * @param string $placeholder
2773      *
2774      * @return boolean
2775      */
2776     protected function placeholder(&$placeholder)
2777     {
2778         if ($this->match(
2779             $this->utf8
2780                 ? '([\pL\w\-_]+)'
2781                 : '([\w\-_]+)',
2782             $m
2783         )) {
2784             $placeholder = $m[1];
2786             return true;
2787         }
2788         if ($this->interpolation($placeholder)) {
2789             return true;
2790         }
2792         return false;
2793     }
2795     /**
2796      * Parse a url
2797      *
2798      * @param array $out
2799      *
2800      * @return boolean
2801      */
2802     protected function url(&$out)
2803     {
2804         if ($this->match('(url\(\s*(["\']?)([^)]+)\2\s*\))', $m)) {
2805             $out = [Type::T_STRING, '', ['url(' . $m[2] . $m[3] . $m[2] . ')']];
2807             return true;
2808         }
2810         return false;
2811     }
2813     /**
2814      * Consume an end of statement delimiter
2815      *
2816      * @return boolean
2817      */
2818     protected function end()
2819     {
2820         if ($this->matchChar(';')) {
2821             return true;
2822         }
2824         if ($this->count === strlen($this->buffer) || $this->buffer[$this->count] === '}') {
2825             // if there is end of file or a closing block next then we don't need a ;
2826             return true;
2827         }
2829         return false;
2830     }
2832     /**
2833      * Strip assignment flag from the list
2834      *
2835      * @param array $value
2836      *
2837      * @return array
2838      */
2839     protected function stripAssignmentFlags(&$value)
2840     {
2841         $flags = [];
2843         for ($token = &$value; $token[0] === Type::T_LIST && ($s = count($token[2])); $token = &$lastNode) {
2844             $lastNode = &$token[2][$s - 1];
2846             while ($lastNode[0] === Type::T_KEYWORD && in_array($lastNode[1], ['!default', '!global'])) {
2847                 array_pop($token[2]);
2849                 $node = end($token[2]);
2851                 $token = $this->flattenList($token);
2853                 $flags[] = $lastNode[1];
2855                 $lastNode = $node;
2856             }
2857         }
2859         return $flags;
2860     }
2862     /**
2863      * Strip optional flag from selector list
2864      *
2865      * @param array $selectors
2866      *
2867      * @return string
2868      */
2869     protected function stripOptionalFlag(&$selectors)
2870     {
2871         $optional = false;
2873         $selector = end($selectors);
2874         $part = end($selector);
2876         if ($part === ['!optional']) {
2877             array_pop($selectors[count($selectors) - 1]);
2879             $optional = true;
2880         }
2882         return $optional;
2883     }
2885     /**
2886      * Turn list of length 1 into value type
2887      *
2888      * @param array $value
2889      *
2890      * @return array
2891      */
2892     protected function flattenList($value)
2893     {
2894         if ($value[0] === Type::T_LIST && count($value[2]) === 1) {
2895             return $this->flattenList($value[2][0]);
2896         }
2898         return $value;
2899     }
2901     /**
2902      * @deprecated
2903      *
2904      * {@internal
2905      *     advance counter to next occurrence of $what
2906      *     $until - don't include $what in advance
2907      *     $allowNewline, if string, will be used as valid char set
2908      * }}
2909      */
2910     protected function to($what, &$out, $until = false, $allowNewline = false)
2911     {
2912         if (is_string($allowNewline)) {
2913             $validChars = $allowNewline;
2914         } else {
2915             $validChars = $allowNewline ? '.' : "[^\n]";
2916         }
2918         if (! $this->match('(' . $validChars . '*?)' . $this->pregQuote($what), $m, ! $until)) {
2919             return false;
2920         }
2922         if ($until) {
2923             $this->count -= strlen($what); // give back $what
2924         }
2926         $out = $m[1];
2928         return true;
2929     }
2931     /**
2932      * @deprecated
2933      */
2934     protected function show()
2935     {
2936         if ($this->peek("(.*?)(\n|$)", $m, $this->count)) {
2937             return $m[1];
2938         }
2940         return '';
2941     }
2943     /**
2944      * Quote regular expression
2945      *
2946      * @param string $what
2947      *
2948      * @return string
2949      */
2950     private function pregQuote($what)
2951     {
2952         return preg_quote($what, '/');
2953     }
2955     /**
2956      * Extract line numbers from buffer
2957      *
2958      * @param string $buffer
2959      */
2960     private function extractLineNumbers($buffer)
2961     {
2962         $this->sourcePositions = [0 => 0];
2963         $prev = 0;
2965         while (($pos = strpos($buffer, "\n", $prev)) !== false) {
2966             $this->sourcePositions[] = $pos;
2967             $prev = $pos + 1;
2968         }
2970         $this->sourcePositions[] = strlen($buffer);
2972         if (substr($buffer, -1) !== "\n") {
2973             $this->sourcePositions[] = strlen($buffer) + 1;
2974         }
2975     }
2977     /**
2978      * Get source line number and column (given character position in the buffer)
2979      *
2980      * @param integer $pos
2981      *
2982      * @return array
2983      */
2984     private function getSourcePosition($pos)
2985     {
2986         $low = 0;
2987         $high = count($this->sourcePositions);
2989         while ($low < $high) {
2990             $mid = (int) (($high + $low) / 2);
2992             if ($pos < $this->sourcePositions[$mid]) {
2993                 $high = $mid - 1;
2994                 continue;
2995             }
2997             if ($pos >= $this->sourcePositions[$mid + 1]) {
2998                 $low = $mid + 1;
2999                 continue;
3000             }
3002             return [$mid + 1, $pos - $this->sourcePositions[$mid]];
3003         }
3005         return [$low + 1, $pos - $this->sourcePositions[$low]];
3006     }
3008     /**
3009      * Save internal encoding
3010      */
3011     private function saveEncoding()
3012     {
3013         if (version_compare(PHP_VERSION, '7.2.0') >= 0) {
3014             return;
3015         }
3017         $iniDirective = 'mbstring' . '.func_overload'; // deprecated in PHP 7.2
3019         if (ini_get($iniDirective) & 2) {
3020             $this->encoding = mb_internal_encoding();
3022             mb_internal_encoding('iso-8859-1');
3023         }
3024     }
3026     /**
3027      * Restore internal encoding
3028      */
3029     private function restoreEncoding()
3030     {
3031         if ($this->encoding) {
3032             mb_internal_encoding($this->encoding);
3033         }
3034     }