Merge branch 'MDL-58454-master' of git://github.com/junpataleta/moodle
[moodle.git] / lib / scssphp / Parser.php
blob6fdea3e262f1bc1eb15e0244dee55554300a7279
1 <?php
2 /**
3 * SCSSPHP
5 * @copyright 2012-2018 Leaf Corcoran
7 * @license http://opensource.org/licenses/MIT MIT
9 * @link http://leafo.github.io/scssphp
12 namespace Leafo\ScssPhp;
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;
20 /**
21 * Parser
23 * @author Leaf Corcoran <leafot@gmail.com>
25 class Parser
27 const SOURCE_INDEX = -1;
28 const SOURCE_LINE = -2;
29 const SOURCE_COLUMN = -3;
31 /**
32 * @var array
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,
52 protected static $commentPattern;
53 protected static $operatorPattern;
54 protected static $whitePattern;
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;
69 /**
70 * Constructor
72 * @api
74 * @param string $sourceName
75 * @param integer $sourceIndex
76 * @param string $encoding
78 public function __construct($sourceName, $sourceIndex = 0, $encoding = 'utf-8')
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';
86 if (empty(static::$operatorPattern)) {
87 static::$operatorPattern = '([*\/%+-]|[!=]\=|\>\=?|\<\=\>|\<\=?|and|or)';
89 $commentSingle = '\/\/';
90 $commentMultiLeft = '\/\*';
91 $commentMultiRight = '\*\/';
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';
101 * Get source file name
103 * @api
105 * @return string
107 public function getSourceName()
109 return $this->sourceName;
113 * Throw parser error
115 * @api
117 * @param string $msg
119 * @throws \Leafo\ScssPhp\Exception\ParserException
121 public function throwParseError($msg = 'parse error')
123 list($line, /* $column */) = $this->getSourcePosition($this->count);
125 $loc = empty($this->sourceName) ? "line: $line" : "$this->sourceName on line $line";
127 if ($this->peek("(.*?)(\n|$)", $m, $this->count)) {
128 throw new ParserException("$msg: failed at `$m[1]` $loc");
131 throw new ParserException("$msg: $loc");
135 * Parser buffer
137 * @api
139 * @param string $buffer
141 * @return \Leafo\ScssPhp\Block
143 public function parse($buffer)
145 // strip BOM (byte order marker)
146 if (substr($buffer, 0, 3) === "\xef\xbb\xbf") {
147 $buffer = substr($buffer, 3);
150 $this->buffer = rtrim($buffer, "\x00..\x1f");
151 $this->count = 0;
152 $this->env = null;
153 $this->inParens = false;
154 $this->eatWhiteDefault = true;
156 $this->saveEncoding();
157 $this->extractLineNumbers($buffer);
159 $this->pushBlock(null); // root block
160 $this->whitespace();
161 $this->pushBlock(null);
162 $this->popBlock();
164 while ($this->parseChunk()) {
168 if ($this->count !== strlen($this->buffer)) {
169 $this->throwParseError();
172 if (! empty($this->env->parent)) {
173 $this->throwParseError('unclosed block');
176 if ($this->charset) {
177 array_unshift($this->env->children, $this->charset);
180 $this->env->isRoot = true;
182 $this->restoreEncoding();
184 return $this->env;
188 * Parse a value or value list
190 * @api
192 * @param string $buffer
193 * @param string $out
195 * @return boolean
197 public function parseValue($buffer, &$out)
199 $this->count = 0;
200 $this->env = null;
201 $this->inParens = false;
202 $this->eatWhiteDefault = true;
203 $this->buffer = (string) $buffer;
205 $this->saveEncoding();
207 $list = $this->valueList($out);
209 $this->restoreEncoding();
211 return $list;
215 * Parse a selector or selector list
217 * @api
219 * @param string $buffer
220 * @param string $out
222 * @return boolean
224 public function parseSelector($buffer, &$out)
226 $this->count = 0;
227 $this->env = null;
228 $this->inParens = false;
229 $this->eatWhiteDefault = true;
230 $this->buffer = (string) $buffer;
232 $this->saveEncoding();
234 $selector = $this->selectors($out);
236 $this->restoreEncoding();
238 return $selector;
242 * Parse a single chunk off the head of the buffer and append it to the
243 * current parse environment.
245 * Returns false when the buffer is empty, or when there is an error.
247 * This function is called repeatedly until the entire document is
248 * parsed.
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.
254 * Consider the function Compiler::keyword(). (All parse functions are
255 * structured the same.)
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.
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.
267 * Because of the format of the functions, to parse an entire string of
268 * grammatical rules, you can chain them together using &&.
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.
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.
278 * @return boolean
280 protected function parseChunk()
282 $s = $this->seek();
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('{')
291 $atRoot = $this->pushSpecialBlock(Type::T_AT_ROOT, $s);
292 $atRoot->selector = $selector;
293 $atRoot->with = $with;
295 return true;
298 $this->seek($s);
300 if ($this->literal('@media') && $this->mediaQueryList($mediaQueryList) && $this->literal('{')) {
301 $media = $this->pushSpecialBlock(Type::T_MEDIA, $s);
302 $media->queryList = $mediaQueryList[2];
304 return true;
307 $this->seek($s);
309 if ($this->literal('@mixin') &&
310 $this->keyword($mixinName) &&
311 ($this->argumentDef($args) || true) &&
312 $this->literal('{')
314 $mixin = $this->pushSpecialBlock(Type::T_MIXIN, $s);
315 $mixin->name = $mixinName;
316 $mixin->args = $args;
318 return true;
321 $this->seek($s);
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)
331 $child = [Type::T_INCLUDE, $mixinName, isset($argValues) ? $argValues : null, null];
333 if (! empty($hasBlock)) {
334 $include = $this->pushSpecialBlock(Type::T_INCLUDE, $s);
335 $include->child = $child;
336 } else {
337 $this->append($child, $s);
340 return true;
343 $this->seek($s);
345 if ($this->literal('@scssphp-import-once') &&
346 $this->valueList($importPath) &&
347 $this->end()
349 $this->append([Type::T_SCSSPHP_IMPORT_ONCE, $importPath], $s);
351 return true;
354 $this->seek($s);
356 if ($this->literal('@import') &&
357 $this->valueList($importPath) &&
358 $this->end()
360 $this->append([Type::T_IMPORT, $importPath], $s);
362 return true;
365 $this->seek($s);
367 if ($this->literal('@import') &&
368 $this->url($importPath) &&
369 $this->end()
371 $this->append([Type::T_IMPORT, $importPath], $s);
373 return true;
376 $this->seek($s);
378 if ($this->literal('@extend') &&
379 $this->selectors($selectors) &&
380 $this->end()
382 // check for '!flag'
383 $optional = $this->stripOptionalFlag($selectors);
384 $this->append([Type::T_EXTEND, $selectors, $optional], $s);
386 return true;
389 $this->seek($s);
391 if ($this->literal('@function') &&
392 $this->keyword($fnName) &&
393 $this->argumentDef($args) &&
394 $this->literal('{')
396 $func = $this->pushSpecialBlock(Type::T_FUNCTION, $s);
397 $func->name = $fnName;
398 $func->args = $args;
400 return true;
403 $this->seek($s);
405 if ($this->literal('@break') && $this->end()) {
406 $this->append([Type::T_BREAK], $s);
408 return true;
411 $this->seek($s);
413 if ($this->literal('@continue') && $this->end()) {
414 $this->append([Type::T_CONTINUE], $s);
416 return true;
419 $this->seek($s);
422 if ($this->literal('@return') && ($this->valueList($retVal) || true) && $this->end()) {
423 $this->append([Type::T_RETURN, isset($retVal) ? $retVal : [Type::T_NULL]], $s);
425 return true;
428 $this->seek($s);
430 if ($this->literal('@each') &&
431 $this->genericList($varNames, 'variable', ',', false) &&
432 $this->literal('in') &&
433 $this->valueList($list) &&
434 $this->literal('{')
436 $each = $this->pushSpecialBlock(Type::T_EACH, $s);
438 foreach ($varNames[2] as $varName) {
439 $each->vars[] = $varName[1];
442 $each->list = $list;
444 return true;
447 $this->seek($s);
449 if ($this->literal('@while') &&
450 $this->expression($cond) &&
451 $this->literal('{')
453 $while = $this->pushSpecialBlock(Type::T_WHILE, $s);
454 $while->cond = $cond;
456 return true;
459 $this->seek($s);
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('{')
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);
476 return true;
479 $this->seek($s);
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 = [];
486 return true;
489 $this->seek($s);
491 if ($this->literal('@debug') &&
492 $this->valueList($value) &&
493 $this->end()
495 $this->append([Type::T_DEBUG, $value], $s);
497 return true;
500 $this->seek($s);
502 if ($this->literal('@warn') &&
503 $this->valueList($value) &&
504 $this->end()
506 $this->append([Type::T_WARN, $value], $s);
508 return true;
511 $this->seek($s);
513 if ($this->literal('@error') &&
514 $this->valueList($value) &&
515 $this->end()
517 $this->append([Type::T_ERROR, $value], $s);
519 return true;
522 $this->seek($s);
524 if ($this->literal('@content') && $this->end()) {
525 $this->append([Type::T_MIXIN_CONTENT], $s);
527 return true;
530 $this->seek($s);
532 $last = $this->last();
534 if (isset($last) && $last[0] === Type::T_IF) {
535 list(, $if) = $last;
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;
545 if (isset($else)) {
546 $else->dontAppend = true;
547 $if->cases[] = $else;
549 return true;
553 $this->seek($s);
556 // only retain the first @charset directive encountered
557 if ($this->literal('@charset') &&
558 $this->valueList($charset) &&
559 $this->end()
561 if (! isset($this->charset)) {
562 $statement = [Type::T_CHARSET, $charset];
564 list($line, $column) = $this->getSourcePosition($s);
566 $statement[static::SOURCE_LINE] = $line;
567 $statement[static::SOURCE_COLUMN] = $column;
568 $statement[static::SOURCE_INDEX] = $this->sourceIndex;
570 $this->charset = $statement;
573 return true;
576 $this->seek($s);
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('{')
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;
591 if (isset($dirValue)) {
592 $directive->value = $dirValue;
595 return true;
598 $this->seek($s);
600 return false;
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()
610 $name = [Type::T_STRING, '', [$name]];
611 $this->append([Type::T_ASSIGN, $name, $value], $s);
613 return true;
616 $this->seek($s);
618 // variable assigns
619 if ($this->variable($name) &&
620 $this->literal(':') &&
621 $this->valueList($value) &&
622 $this->end()
624 // check for '!flag'
625 $assignmentFlags = $this->stripAssignmentFlags($value);
626 $this->append([Type::T_ASSIGN, $name, $value, $assignmentFlags], $s);
628 return true;
631 $this->seek($s);
633 // misc
634 if ($this->literal('-->')) {
635 return true;
638 // opening css block
639 if ($this->selectors($selectors) && $this->literal('{')) {
640 $this->pushBlock($selectors, $s);
642 return true;
645 $this->seek($s);
647 // property assign, or nested assign
648 if ($this->propertyName($name) && $this->literal(':')) {
649 $foundSomething = false;
651 if ($this->valueList($value)) {
652 $this->append([Type::T_ASSIGN, $name, $value], $s);
653 $foundSomething = true;
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();
664 if ($foundSomething) {
665 return true;
669 $this->seek($s);
671 // closing a block
672 if ($this->literal('}')) {
673 $block = $this->popBlock();
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);
685 return true;
688 // extra stuff
689 if ($this->literal(';') ||
690 $this->literal('<!--')
692 return true;
695 return false;
699 * Push block onto parse tree
701 * @param array $selectors
702 * @param integer $pos
704 * @return \Leafo\ScssPhp\Block
706 protected function pushBlock($selectors, $pos = 0)
708 list($line, $column) = $this->getSourcePosition($pos);
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;
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 = [];
730 $this->env = $b;
732 return $b;
736 * Push special (named) block onto parse tree
738 * @param string $type
739 * @param integer $pos
741 * @return \Leafo\ScssPhp\Block
743 protected function pushSpecialBlock($type, $pos)
745 $block = $this->pushBlock(null, $pos);
746 $block->type = $type;
748 return $block;
752 * Pop scope and return last block
754 * @return \Leafo\ScssPhp\Block
756 * @throws \Exception
758 protected function popBlock()
760 $block = $this->env;
762 if (empty($block->parent)) {
763 $this->throwParseError('unexpected }');
766 $this->env = $block->parent;
767 unset($block->parent);
769 $comments = $block->comments;
770 if (count($comments)) {
771 $this->env->comments = $comments;
772 unset($block->comments);
775 return $block;
779 * Peek input stream
781 * @param string $regex
782 * @param array $out
783 * @param integer $from
785 * @return integer
787 protected function peek($regex, &$out, $from = null)
789 if (! isset($from)) {
790 $from = $this->count;
793 $r = '/' . $regex . '/' . $this->patternModifiers;
794 $result = preg_match($r, $this->buffer, $out, null, $from);
796 return $result;
800 * Seek to position in input stream (or return current position in input stream)
802 * @param integer $where
804 * @return integer
806 protected function seek($where = null)
808 if ($where === null) {
809 return $this->count;
812 $this->count = $where;
814 return true;
818 * Match string looking for either ending delim, escape, or string interpolation
820 * {@internal This is a workaround for preg_match's 250K string match limit. }}
822 * @param array $m Matches (passed by reference)
823 * @param string $delim Delimeter
825 * @return boolean True if match; false otherwise
827 protected function matchString(&$m, $delim)
829 $token = null;
831 $end = strlen($this->buffer);
833 // look for either ending delim, escape, or string interpolation
834 foreach (['#{', '\\', $delim] as $lookahead) {
835 $pos = strpos($this->buffer, $lookahead, $this->count);
837 if ($pos !== false && $pos < $end) {
838 $end = $pos;
839 $token = $lookahead;
843 if (! isset($token)) {
844 return false;
847 $match = substr($this->buffer, $this->count, $end - $this->count);
848 $m = [
849 $match . $token,
850 $match,
851 $token
853 $this->count = $end + strlen($token);
855 return true;
859 * Try to match something on head of buffer
861 * @param string $regex
862 * @param array $out
863 * @param boolean $eatWhitespace
865 * @return boolean
867 protected function match($regex, &$out, $eatWhitespace = null)
869 if (! isset($eatWhitespace)) {
870 $eatWhitespace = $this->eatWhiteDefault;
873 $r = '/' . $regex . '/' . $this->patternModifiers;
875 if (preg_match($r, $this->buffer, $out, null, $this->count)) {
876 $this->count += strlen($out[0]);
878 if ($eatWhitespace) {
879 $this->whitespace();
882 return true;
885 return false;
889 * Match literal string
891 * @param string $what
892 * @param boolean $eatWhitespace
894 * @return boolean
896 protected function literal($what, $eatWhitespace = null)
898 if (! isset($eatWhitespace)) {
899 $eatWhitespace = $this->eatWhiteDefault;
902 $len = strlen($what);
904 if (strcasecmp(substr($this->buffer, $this->count, $len), $what) === 0) {
905 $this->count += $len;
907 if ($eatWhitespace) {
908 $this->whitespace();
911 return true;
914 return false;
918 * Match some whitespace
920 * @return boolean
922 protected function whitespace()
924 $gotWhite = false;
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]]);
930 $this->commentsSeen[$this->count] = true;
933 $this->count += strlen($m[0]);
934 $gotWhite = true;
937 return $gotWhite;
941 * Append comment to current block
943 * @param array $comment
945 protected function appendComment($comment)
947 $comment[1] = substr(preg_replace(['/^\s+/m', '/^(.)/m'], ['', ' \1'], $comment[1]), 1);
949 $this->env->comments[] = $comment;
953 * Append statement to current block
955 * @param array $statement
956 * @param integer $pos
958 protected function append($statement, $pos = null)
960 if ($pos !== null) {
961 list($line, $column) = $this->getSourcePosition($pos);
963 $statement[static::SOURCE_LINE] = $line;
964 $statement[static::SOURCE_COLUMN] = $column;
965 $statement[static::SOURCE_INDEX] = $this->sourceIndex;
968 $this->env->children[] = $statement;
970 $comments = $this->env->comments;
972 if (count($comments)) {
973 $this->env->children = array_merge($this->env->children, $comments);
974 $this->env->comments = [];
979 * Returns last child was appended
981 * @return array|null
983 protected function last()
985 $i = count($this->env->children) - 1;
987 if (isset($this->env->children[$i])) {
988 return $this->env->children[$i];
993 * Parse media query list
995 * @param array $out
997 * @return boolean
999 protected function mediaQueryList(&$out)
1001 return $this->genericList($out, 'mediaQuery', ',', false);
1005 * Parse media query
1007 * @param array $out
1009 * @return boolean
1011 protected function mediaQuery(&$out)
1013 $expressions = null;
1014 $parts = [];
1016 if (($this->literal('only') && ($only = true) || $this->literal('not') && ($not = true) || true) &&
1017 $this->mixedKeyword($mediaType)
1019 $prop = [Type::T_MEDIA_TYPE];
1021 if (isset($only)) {
1022 $prop[] = [Type::T_KEYWORD, 'only'];
1025 if (isset($not)) {
1026 $prop[] = [Type::T_KEYWORD, 'not'];
1029 $media = [Type::T_LIST, '', []];
1031 foreach ((array) $mediaType as $type) {
1032 if (is_array($type)) {
1033 $media[2][] = $type;
1034 } else {
1035 $media[2][] = [Type::T_KEYWORD, $type];
1039 $prop[] = $media;
1040 $parts[] = $prop;
1043 if (empty($parts) || $this->literal('and')) {
1044 $this->genericList($expressions, 'mediaExpression', 'and', false);
1046 if (is_array($expressions)) {
1047 $parts = array_merge($parts, $expressions[2]);
1051 $out = $parts;
1053 return true;
1057 * Parse media expression
1059 * @param array $out
1061 * @return boolean
1063 protected function mediaExpression(&$out)
1065 $s = $this->seek();
1066 $value = null;
1068 if ($this->literal('(') &&
1069 $this->expression($feature) &&
1070 ($this->literal(':') && $this->expression($value) || true) &&
1071 $this->literal(')')
1073 $out = [Type::T_MEDIA_EXPRESSION, $feature];
1075 if ($value) {
1076 $out[] = $value;
1079 return true;
1082 $this->seek($s);
1084 return false;
1088 * Parse argument values
1090 * @param array $out
1092 * @return boolean
1094 protected function argValues(&$out)
1096 if ($this->genericList($list, 'argValue', ',', false)) {
1097 $out = $list[2];
1099 return true;
1102 return false;
1106 * Parse argument value
1108 * @param array $out
1110 * @return boolean
1112 protected function argValue(&$out)
1114 $s = $this->seek();
1116 $keyword = null;
1118 if (! $this->variable($keyword) || ! $this->literal(':')) {
1119 $this->seek($s);
1120 $keyword = null;
1123 if ($this->genericList($value, 'expression')) {
1124 $out = [$keyword, $value, false];
1125 $s = $this->seek();
1127 if ($this->literal('...')) {
1128 $out[2] = true;
1129 } else {
1130 $this->seek($s);
1133 return true;
1136 return false;
1140 * Parse comma separated value list
1142 * @param string $out
1144 * @return boolean
1146 protected function valueList(&$out)
1148 return $this->genericList($out, 'spaceList', ',');
1152 * Parse space separated value list
1154 * @param array $out
1156 * @return boolean
1158 protected function spaceList(&$out)
1160 return $this->genericList($out, 'expression');
1164 * Parse generic list
1166 * @param array $out
1167 * @param callable $parseItem
1168 * @param string $delim
1169 * @param boolean $flatten
1171 * @return boolean
1173 protected function genericList(&$out, $parseItem, $delim = '', $flatten = true)
1175 $s = $this->seek();
1176 $items = [];
1178 while ($this->$parseItem($value)) {
1179 $items[] = $value;
1181 if ($delim) {
1182 if (! $this->literal($delim)) {
1183 break;
1188 if (count($items) === 0) {
1189 $this->seek($s);
1191 return false;
1194 if ($flatten && count($items) === 1) {
1195 $out = $items[0];
1196 } else {
1197 $out = [Type::T_LIST, $delim, $items];
1200 return true;
1204 * Parse expression
1206 * @param array $out
1208 * @return boolean
1210 protected function expression(&$out)
1212 $s = $this->seek();
1214 if ($this->literal('(')) {
1215 if ($this->literal(')')) {
1216 $out = [Type::T_LIST, '', []];
1218 return true;
1221 if ($this->valueList($out) && $this->literal(')') && $out[0] === Type::T_LIST) {
1222 return true;
1225 $this->seek($s);
1227 if ($this->map($out)) {
1228 return true;
1231 $this->seek($s);
1234 if ($this->value($lhs)) {
1235 $out = $this->expHelper($lhs, 0);
1237 return true;
1240 return false;
1244 * Parse left-hand side of subexpression
1246 * @param array $lhs
1247 * @param integer $minP
1249 * @return array
1251 protected function expHelper($lhs, $minP)
1253 $operators = static::$operatorPattern;
1255 $ss = $this->seek();
1256 $whiteBefore = isset($this->buffer[$this->count - 1]) &&
1257 ctype_space($this->buffer[$this->count - 1]);
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] === '$';
1265 $this->whitespace();
1267 $op = $m[1];
1269 // don't turn negative numbers into expressions
1270 if ($op === '-' && $whiteBefore && ! $whiteAfter && ! $varAfter) {
1271 break;
1274 if (! $this->value($rhs)) {
1275 break;
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]]);
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]);
1289 $this->seek($ss);
1291 return $lhs;
1295 * Parse value
1297 * @param array $out
1299 * @return boolean
1301 protected function value(&$out)
1303 $s = $this->seek();
1305 if ($this->literal('not', false) && $this->whitespace() && $this->value($inner)) {
1306 $out = [Type::T_UNARY, 'not', $inner, $this->inParens];
1308 return true;
1311 $this->seek($s);
1313 if ($this->literal('not', false) && $this->parenValue($inner)) {
1314 $out = [Type::T_UNARY, 'not', $inner, $this->inParens];
1316 return true;
1319 $this->seek($s);
1321 if ($this->literal('+') && $this->value($inner)) {
1322 $out = [Type::T_UNARY, '+', $inner, $this->inParens];
1324 return true;
1327 $this->seek($s);
1329 // negation
1330 if ($this->literal('-', false) &&
1331 ($this->variable($inner) ||
1332 $this->unit($inner) ||
1333 $this->parenValue($inner))
1335 $out = [Type::T_UNARY, '-', $inner, $this->inParens];
1337 return true;
1340 $this->seek($s);
1342 if ($this->parenValue($out) ||
1343 $this->interpolation($out) ||
1344 $this->variable($out) ||
1345 $this->color($out) ||
1346 $this->unit($out) ||
1347 $this->string($out) ||
1348 $this->func($out) ||
1349 $this->progid($out)
1351 return true;
1354 if ($this->keyword($keyword)) {
1355 if ($keyword === 'null') {
1356 $out = [Type::T_NULL];
1357 } else {
1358 $out = [Type::T_KEYWORD, $keyword];
1361 return true;
1364 return false;
1368 * Parse parenthesized value
1370 * @param array $out
1372 * @return boolean
1374 protected function parenValue(&$out)
1376 $s = $this->seek();
1378 $inParens = $this->inParens;
1380 if ($this->literal('(')) {
1381 if ($this->literal(')')) {
1382 $out = [Type::T_LIST, '', []];
1384 return true;
1387 $this->inParens = true;
1389 if ($this->expression($exp) && $this->literal(')')) {
1390 $out = $exp;
1391 $this->inParens = $inParens;
1393 return true;
1397 $this->inParens = $inParens;
1398 $this->seek($s);
1400 return false;
1404 * Parse "progid:"
1406 * @param array $out
1408 * @return boolean
1410 protected function progid(&$out)
1412 $s = $this->seek();
1414 if ($this->literal('progid:', false) &&
1415 $this->openString('(', $fn) &&
1416 $this->literal('(')
1418 $this->openString(')', $args, '(');
1420 if ($this->literal(')')) {
1421 $out = [Type::T_STRING, '', [
1422 'progid:', $fn, '(', $args, ')'
1425 return true;
1429 $this->seek($s);
1431 return false;
1435 * Parse function call
1437 * @param array $out
1439 * @return boolean
1441 protected function func(&$func)
1443 $s = $this->seek();
1445 if ($this->keyword($name, false) &&
1446 $this->literal('(')
1448 if ($name === 'alpha' && $this->argumentList($args)) {
1449 $func = [Type::T_FUNCTION, $name, [Type::T_STRING, '', $args]];
1451 return true;
1454 if ($name !== 'expression' && ! preg_match('/^(-[a-z]+-)?calc$/', $name)) {
1455 $ss = $this->seek();
1457 if ($this->argValues($args) && $this->literal(')')) {
1458 $func = [Type::T_FUNCTION_CALL, $name, $args];
1460 return true;
1463 $this->seek($ss);
1466 if (($this->openString(')', $str, '(') || true) &&
1467 $this->literal(')')
1469 $args = [];
1471 if (! empty($str)) {
1472 $args[] = [null, [Type::T_STRING, '', [$str]]];
1475 $func = [Type::T_FUNCTION_CALL, $name, $args];
1477 return true;
1481 $this->seek($s);
1483 return false;
1487 * Parse function call argument list
1489 * @param array $out
1491 * @return boolean
1493 protected function argumentList(&$out)
1495 $s = $this->seek();
1496 $this->literal('(');
1498 $args = [];
1500 while ($this->keyword($var)) {
1501 if ($this->literal('=') && $this->expression($exp)) {
1502 $args[] = [Type::T_STRING, '', [$var . '=']];
1503 $arg = $exp;
1504 } else {
1505 break;
1508 $args[] = $arg;
1510 if (! $this->literal(',')) {
1511 break;
1514 $args[] = [Type::T_STRING, '', [', ']];
1517 if (! $this->literal(')') || ! count($args)) {
1518 $this->seek($s);
1520 return false;
1523 $out = $args;
1525 return true;
1529 * Parse mixin/function definition argument list
1531 * @param array $out
1533 * @return boolean
1535 protected function argumentDef(&$out)
1537 $s = $this->seek();
1538 $this->literal('(');
1540 $args = [];
1542 while ($this->variable($var)) {
1543 $arg = [$var[1], null, false];
1545 $ss = $this->seek();
1547 if ($this->literal(':') && $this->genericList($defaultVal, 'expression')) {
1548 $arg[1] = $defaultVal;
1549 } else {
1550 $this->seek($ss);
1553 $ss = $this->seek();
1555 if ($this->literal('...')) {
1556 $sss = $this->seek();
1558 if (! $this->literal(')')) {
1559 $this->throwParseError('... has to be after the final argument');
1562 $arg[2] = true;
1563 $this->seek($sss);
1564 } else {
1565 $this->seek($ss);
1568 $args[] = $arg;
1570 if (! $this->literal(',')) {
1571 break;
1575 if (! $this->literal(')')) {
1576 $this->seek($s);
1578 return false;
1581 $out = $args;
1583 return true;
1587 * Parse map
1589 * @param array $out
1591 * @return boolean
1593 protected function map(&$out)
1595 $s = $this->seek();
1597 if (! $this->literal('(')) {
1598 return false;
1601 $keys = [];
1602 $values = [];
1604 while ($this->genericList($key, 'expression') && $this->literal(':') &&
1605 $this->genericList($value, 'expression')
1607 $keys[] = $key;
1608 $values[] = $value;
1610 if (! $this->literal(',')) {
1611 break;
1615 if (! count($keys) || ! $this->literal(')')) {
1616 $this->seek($s);
1618 return false;
1621 $out = [Type::T_MAP, $keys, $values];
1623 return true;
1627 * Parse color
1629 * @param array $out
1631 * @return boolean
1633 protected function color(&$out)
1635 $color = [Type::T_COLOR];
1637 if ($this->match('(#([0-9a-f]{6})|#([0-9a-f]{3}))', $m)) {
1638 if (isset($m[3])) {
1639 $num = hexdec($m[3]);
1641 foreach ([3, 2, 1] as $i) {
1642 $t = $num & 0xf;
1643 $color[$i] = $t << 4 | $t;
1644 $num >>= 4;
1646 } else {
1647 $num = hexdec($m[2]);
1649 foreach ([3, 2, 1] as $i) {
1650 $color[$i] = $num & 0xff;
1651 $num >>= 8;
1655 $out = $color;
1657 return true;
1660 return false;
1664 * Parse number with unit
1666 * @param array $out
1668 * @return boolean
1670 protected function unit(&$unit)
1672 if ($this->match('([0-9]*(\.)?[0-9]+)([%a-zA-Z]+)?', $m)) {
1673 $unit = new Node\Number($m[1], empty($m[3]) ? '' : $m[3]);
1675 return true;
1678 return false;
1682 * Parse string
1684 * @param array $out
1686 * @return boolean
1688 protected function string(&$out)
1690 $s = $this->seek();
1692 if ($this->literal('"', false)) {
1693 $delim = '"';
1694 } elseif ($this->literal("'", false)) {
1695 $delim = "'";
1696 } else {
1697 return false;
1700 $content = [];
1701 $oldWhite = $this->eatWhiteDefault;
1702 $this->eatWhiteDefault = false;
1703 $hasInterpolation = false;
1705 while ($this->matchString($m, $delim)) {
1706 if ($m[1] !== '') {
1707 $content[] = $m[1];
1710 if ($m[2] === '#{') {
1711 $this->count -= strlen($m[2]);
1713 if ($this->interpolation($inter, false)) {
1714 $content[] = $inter;
1715 $hasInterpolation = true;
1716 } else {
1717 $this->count += strlen($m[2]);
1718 $content[] = '#{'; // ignore it
1720 } elseif ($m[2] === '\\') {
1721 if ($this->literal('"', false)) {
1722 $content[] = $m[2] . '"';
1723 } elseif ($this->literal("'", false)) {
1724 $content[] = $m[2] . "'";
1725 } else {
1726 $content[] = $m[2];
1728 } else {
1729 $this->count -= strlen($delim);
1730 break; // delim
1734 $this->eatWhiteDefault = $oldWhite;
1736 if ($this->literal($delim)) {
1737 if ($hasInterpolation) {
1738 $delim = '"';
1740 foreach ($content as &$string) {
1741 if ($string === "\\'") {
1742 $string = "'";
1743 } elseif ($string === '\\"') {
1744 $string = '"';
1749 $out = [Type::T_STRING, $delim, $content];
1751 return true;
1754 $this->seek($s);
1756 return false;
1760 * Parse keyword or interpolation
1762 * @param array $out
1764 * @return boolean
1766 protected function mixedKeyword(&$out)
1768 $parts = [];
1770 $oldWhite = $this->eatWhiteDefault;
1771 $this->eatWhiteDefault = false;
1773 for (;;) {
1774 if ($this->keyword($key)) {
1775 $parts[] = $key;
1776 continue;
1779 if ($this->interpolation($inter)) {
1780 $parts[] = $inter;
1781 continue;
1784 break;
1787 $this->eatWhiteDefault = $oldWhite;
1789 if (count($parts) === 0) {
1790 return false;
1793 if ($this->eatWhiteDefault) {
1794 $this->whitespace();
1797 $out = $parts;
1799 return true;
1803 * Parse an unbounded string stopped by $end
1805 * @param string $end
1806 * @param array $out
1807 * @param string $nestingOpen
1809 * @return boolean
1811 protected function openString($end, &$out, $nestingOpen = null)
1813 $oldWhite = $this->eatWhiteDefault;
1814 $this->eatWhiteDefault = false;
1816 $patt = '(.*?)([\'"]|#\{|' . $this->pregQuote($end) . '|' . static::$commentPattern . ')';
1818 $nestingLevel = 0;
1820 $content = [];
1822 while ($this->match($patt, $m, false)) {
1823 if (isset($m[1]) && $m[1] !== '') {
1824 $content[] = $m[1];
1826 if ($nestingOpen) {
1827 $nestingLevel += substr_count($m[1], $nestingOpen);
1831 $tok = $m[2];
1833 $this->count-= strlen($tok);
1835 if ($tok === $end && ! $nestingLevel--) {
1836 break;
1839 if (($tok === "'" || $tok === '"') && $this->string($str)) {
1840 $content[] = $str;
1841 continue;
1844 if ($tok === '#{' && $this->interpolation($inter)) {
1845 $content[] = $inter;
1846 continue;
1849 $content[] = $tok;
1850 $this->count+= strlen($tok);
1853 $this->eatWhiteDefault = $oldWhite;
1855 if (count($content) === 0) {
1856 return false;
1859 // trim the end
1860 if (is_string(end($content))) {
1861 $content[count($content) - 1] = rtrim(end($content));
1864 $out = [Type::T_STRING, '', $content];
1866 return true;
1870 * Parser interpolation
1872 * @param array $out
1873 * @param boolean $lookWhite save information about whitespace before and after
1875 * @return boolean
1877 protected function interpolation(&$out, $lookWhite = true)
1879 $oldWhite = $this->eatWhiteDefault;
1880 $this->eatWhiteDefault = true;
1882 $s = $this->seek();
1884 if ($this->literal('#{') && $this->valueList($value) && $this->literal('}', false)) {
1885 if ($lookWhite) {
1886 $left = preg_match('/\s/', $this->buffer[$s - 1]) ? ' ' : '';
1887 $right = preg_match('/\s/', $this->buffer[$this->count]) ? ' ': '';
1888 } else {
1889 $left = $right = false;
1892 $out = [Type::T_INTERPOLATE, $value, $left, $right];
1893 $this->eatWhiteDefault = $oldWhite;
1895 if ($this->eatWhiteDefault) {
1896 $this->whitespace();
1899 return true;
1902 $this->seek($s);
1903 $this->eatWhiteDefault = $oldWhite;
1905 return false;
1909 * Parse property name (as an array of parts or a string)
1911 * @param array $out
1913 * @return boolean
1915 protected function propertyName(&$out)
1917 $parts = [];
1919 $oldWhite = $this->eatWhiteDefault;
1920 $this->eatWhiteDefault = false;
1922 for (;;) {
1923 if ($this->interpolation($inter)) {
1924 $parts[] = $inter;
1925 continue;
1928 if ($this->keyword($text)) {
1929 $parts[] = $text;
1930 continue;
1933 if (count($parts) === 0 && $this->match('[:.#]', $m, false)) {
1934 // css hacks
1935 $parts[] = $m[0];
1936 continue;
1939 break;
1942 $this->eatWhiteDefault = $oldWhite;
1944 if (count($parts) === 0) {
1945 return false;
1948 // match comment hack
1949 if (preg_match(
1950 static::$whitePattern,
1951 $this->buffer,
1953 null,
1954 $this->count
1955 )) {
1956 if (! empty($m[0])) {
1957 $parts[] = $m[0];
1958 $this->count += strlen($m[0]);
1962 $this->whitespace(); // get any extra whitespace
1964 $out = [Type::T_STRING, '', $parts];
1966 return true;
1970 * Parse comma separated selector list
1972 * @param array $out
1974 * @return boolean
1976 protected function selectors(&$out)
1978 $s = $this->seek();
1979 $selectors = [];
1981 while ($this->selector($sel)) {
1982 $selectors[] = $sel;
1984 if (! $this->literal(',')) {
1985 break;
1988 while ($this->literal(',')) {
1989 ; // ignore extra
1993 if (count($selectors) === 0) {
1994 $this->seek($s);
1996 return false;
1999 $out = $selectors;
2001 return true;
2005 * Parse whitespace separated selector list
2007 * @param array $out
2009 * @return boolean
2011 protected function selector(&$out)
2013 $selector = [];
2015 for (;;) {
2016 if ($this->match('[>+~]+', $m)) {
2017 $selector[] = [$m[0]];
2018 continue;
2021 if ($this->selectorSingle($part)) {
2022 $selector[] = $part;
2023 $this->match('\s+', $m);
2024 continue;
2027 if ($this->match('\/[^\/]+\/', $m)) {
2028 $selector[] = [$m[0]];
2029 continue;
2032 break;
2035 if (count($selector) === 0) {
2036 return false;
2039 $out = $selector;
2040 return true;
2044 * Parse the parts that make up a selector
2046 * {@internal
2047 * div[yes=no]#something.hello.world:nth-child(-2n+1)%placeholder
2048 * }}
2050 * @param array $out
2052 * @return boolean
2054 protected function selectorSingle(&$out)
2056 $oldWhite = $this->eatWhiteDefault;
2057 $this->eatWhiteDefault = false;
2059 $parts = [];
2061 if ($this->literal('*', false)) {
2062 $parts[] = '*';
2065 for (;;) {
2066 // see if we can stop early
2067 if ($this->match('\s*[{,]', $m)) {
2068 $this->count--;
2069 break;
2072 $s = $this->seek();
2074 // self
2075 if ($this->literal('&', false)) {
2076 $parts[] = Compiler::$selfSelector;
2077 continue;
2080 if ($this->literal('.', false)) {
2081 $parts[] = '.';
2082 continue;
2085 if ($this->literal('|', false)) {
2086 $parts[] = '|';
2087 continue;
2090 if ($this->match('\\\\\S', $m)) {
2091 $parts[] = $m[0];
2092 continue;
2095 // for keyframes
2096 if ($this->unit($unit)) {
2097 $parts[] = $unit;
2098 continue;
2101 if ($this->keyword($name)) {
2102 $parts[] = $name;
2103 continue;
2106 if ($this->interpolation($inter)) {
2107 $parts[] = $inter;
2108 continue;
2111 if ($this->literal('%', false) && $this->placeholder($placeholder)) {
2112 $parts[] = '%';
2113 $parts[] = $placeholder;
2114 continue;
2117 if ($this->literal('#', false)) {
2118 $parts[] = '#';
2119 continue;
2122 // a pseudo selector
2123 if ($this->match('::?', $m) && $this->mixedKeyword($nameParts)) {
2124 $parts[] = $m[0];
2126 foreach ($nameParts as $sub) {
2127 $parts[] = $sub;
2130 $ss = $this->seek();
2132 if ($this->literal('(') &&
2133 ($this->openString(')', $str, '(') || true) &&
2134 $this->literal(')')
2136 $parts[] = '(';
2138 if (! empty($str)) {
2139 $parts[] = $str;
2142 $parts[] = ')';
2143 } else {
2144 $this->seek($ss);
2147 continue;
2150 $this->seek($s);
2152 // attribute selector
2153 if ($this->literal('[') &&
2154 ($this->openString(']', $str, '[') || true) &&
2155 $this->literal(']')
2157 $parts[] = '[';
2159 if (! empty($str)) {
2160 $parts[] = $str;
2163 $parts[] = ']';
2165 continue;
2168 $this->seek($s);
2170 break;
2173 $this->eatWhiteDefault = $oldWhite;
2175 if (count($parts) === 0) {
2176 return false;
2179 $out = $parts;
2181 return true;
2185 * Parse a variable
2187 * @param array $out
2189 * @return boolean
2191 protected function variable(&$out)
2193 $s = $this->seek();
2195 if ($this->literal('$', false) && $this->keyword($name)) {
2196 $out = [Type::T_VARIABLE, $name];
2198 return true;
2201 $this->seek($s);
2203 return false;
2207 * Parse a keyword
2209 * @param string $word
2210 * @param boolean $eatWhitespace
2212 * @return boolean
2214 protected function keyword(&$word, $eatWhitespace = null)
2216 if ($this->match(
2217 $this->utf8
2218 ? '(([\pL\w_\-\*!"\']|[\\\\].)([\pL\w\-_"\']|[\\\\].)*)'
2219 : '(([\w_\-\*!"\']|[\\\\].)([\w\-_"\']|[\\\\].)*)',
2221 $eatWhitespace
2222 )) {
2223 $word = $m[1];
2225 return true;
2228 return false;
2232 * Parse a placeholder
2234 * @param string $placeholder
2236 * @return boolean
2238 protected function placeholder(&$placeholder)
2240 if ($this->match(
2241 $this->utf8
2242 ? '([\pL\w\-_]+|#[{][$][\pL\w\-_]+[}])'
2243 : '([\w\-_]+|#[{][$][\w\-_]+[}])',
2245 )) {
2246 $placeholder = $m[1];
2248 return true;
2251 return false;
2255 * Parse a url
2257 * @param array $out
2259 * @return boolean
2261 protected function url(&$out)
2263 if ($this->match('(url\(\s*(["\']?)([^)]+)\2\s*\))', $m)) {
2264 $out = [Type::T_STRING, '', ['url(' . $m[2] . $m[3] . $m[2] . ')']];
2266 return true;
2269 return false;
2273 * Consume an end of statement delimiter
2275 * @return boolean
2277 protected function end()
2279 if ($this->literal(';')) {
2280 return true;
2283 if ($this->count === strlen($this->buffer) || $this->buffer[$this->count] === '}') {
2284 // if there is end of file or a closing block next then we don't need a ;
2285 return true;
2288 return false;
2292 * Strip assignment flag from the list
2294 * @param array $value
2296 * @return array
2298 protected function stripAssignmentFlags(&$value)
2300 $flags = [];
2302 for ($token = &$value; $token[0] === Type::T_LIST && ($s = count($token[2])); $token = &$lastNode) {
2303 $lastNode = &$token[2][$s - 1];
2305 while ($lastNode[0] === Type::T_KEYWORD && in_array($lastNode[1], ['!default', '!global'])) {
2306 array_pop($token[2]);
2308 $node = end($token[2]);
2310 $token = $this->flattenList($token);
2312 $flags[] = $lastNode[1];
2314 $lastNode = $node;
2318 return $flags;
2322 * Strip optional flag from selector list
2324 * @param array $selectors
2326 * @return string
2328 protected function stripOptionalFlag(&$selectors)
2330 $optional = false;
2332 $selector = end($selectors);
2333 $part = end($selector);
2335 if ($part === ['!optional']) {
2336 array_pop($selectors[count($selectors) - 1]);
2338 $optional = true;
2341 return $optional;
2345 * Turn list of length 1 into value type
2347 * @param array $value
2349 * @return array
2351 protected function flattenList($value)
2353 if ($value[0] === Type::T_LIST && count($value[2]) === 1) {
2354 return $this->flattenList($value[2][0]);
2357 return $value;
2361 * @deprecated
2363 * {@internal
2364 * advance counter to next occurrence of $what
2365 * $until - don't include $what in advance
2366 * $allowNewline, if string, will be used as valid char set
2367 * }}
2369 protected function to($what, &$out, $until = false, $allowNewline = false)
2371 if (is_string($allowNewline)) {
2372 $validChars = $allowNewline;
2373 } else {
2374 $validChars = $allowNewline ? '.' : "[^\n]";
2377 if (! $this->match('(' . $validChars . '*?)' . $this->pregQuote($what), $m, ! $until)) {
2378 return false;
2381 if ($until) {
2382 $this->count -= strlen($what); // give back $what
2385 $out = $m[1];
2387 return true;
2391 * @deprecated
2393 protected function show()
2395 if ($this->peek("(.*?)(\n|$)", $m, $this->count)) {
2396 return $m[1];
2399 return '';
2403 * Quote regular expression
2405 * @param string $what
2407 * @return string
2409 private function pregQuote($what)
2411 return preg_quote($what, '/');
2415 * Extract line numbers from buffer
2417 * @param string $buffer
2419 private function extractLineNumbers($buffer)
2421 $this->sourcePositions = [0 => 0];
2422 $prev = 0;
2424 while (($pos = strpos($buffer, "\n", $prev)) !== false) {
2425 $this->sourcePositions[] = $pos;
2426 $prev = $pos + 1;
2429 $this->sourcePositions[] = strlen($buffer);
2431 if (substr($buffer, -1) !== "\n") {
2432 $this->sourcePositions[] = strlen($buffer) + 1;
2437 * Get source line number and column (given character position in the buffer)
2439 * @param integer $pos
2441 * @return integer
2443 private function getSourcePosition($pos)
2445 $low = 0;
2446 $high = count($this->sourcePositions);
2448 while ($low < $high) {
2449 $mid = (int) (($high + $low) / 2);
2451 if ($pos < $this->sourcePositions[$mid]) {
2452 $high = $mid - 1;
2453 continue;
2456 if ($pos >= $this->sourcePositions[$mid + 1]) {
2457 $low = $mid + 1;
2458 continue;
2461 return [$mid + 1, $pos - $this->sourcePositions[$mid]];
2464 return [$low + 1, $pos - $this->sourcePositions[$low]];
2468 * Save internal encoding
2470 private function saveEncoding()
2472 if (version_compare(PHP_VERSION, '7.2.0') >= 0) {
2473 return;
2476 $iniDirective = 'mbstring' . '.func_overload'; // deprecated in PHP 7.2
2478 if (ini_get($iniDirective) & 2) {
2479 $this->encoding = mb_internal_encoding();
2481 mb_internal_encoding('iso-8859-1');
2486 * Restore internal encoding
2488 private function restoreEncoding()
2490 if ($this->encoding) {
2491 mb_internal_encoding($this->encoding);