6 * @copyright 2012-2020 Leaf Corcoran
8 * @license http://opensource.org/licenses/MIT MIT
10 * @link http://scssphp.github.io/scssphp
13 namespace ScssPhp\ScssPhp
;
15 use ScssPhp\ScssPhp\Block\AtRootBlock
;
16 use ScssPhp\ScssPhp\Block\CallableBlock
;
17 use ScssPhp\ScssPhp\Block\ContentBlock
;
18 use ScssPhp\ScssPhp\Block\DirectiveBlock
;
19 use ScssPhp\ScssPhp\Block\EachBlock
;
20 use ScssPhp\ScssPhp\Block\ElseBlock
;
21 use ScssPhp\ScssPhp\Block\ElseifBlock
;
22 use ScssPhp\ScssPhp\Block\ForBlock
;
23 use ScssPhp\ScssPhp\Block\IfBlock
;
24 use ScssPhp\ScssPhp\Block\MediaBlock
;
25 use ScssPhp\ScssPhp\Block\NestedPropertyBlock
;
26 use ScssPhp\ScssPhp\Block\WhileBlock
;
27 use ScssPhp\ScssPhp\Exception\ParserException
;
28 use ScssPhp\ScssPhp\Logger\LoggerInterface
;
29 use ScssPhp\ScssPhp\Logger\QuietLogger
;
34 * @author Leaf Corcoran <leafot@gmail.com>
40 const SOURCE_INDEX
= -1;
41 const SOURCE_LINE
= -2;
42 const SOURCE_COLUMN
= -3;
45 * @var array<string, int>
47 protected static $precedence = [
67 protected static $commentPattern;
71 protected static $operatorPattern;
75 protected static $whitePattern;
85 * @var array<int, int>
87 private $sourcePositions;
93 * The current offset in the buffer
109 private $eatWhiteDefault;
113 private $discardComments;
124 private $patternModifiers;
125 private $commentsSeen;
130 * @var LoggerInterface
139 * @param string|null $sourceName
140 * @param int $sourceIndex
141 * @param string|null $encoding
142 * @param Cache|null $cache
143 * @param bool $cssOnly
144 * @param LoggerInterface|null $logger
146 public function __construct($sourceName, $sourceIndex = 0, $encoding = 'utf-8', Cache
$cache = null, $cssOnly = false, LoggerInterface
$logger = null)
148 $this->sourceName
= $sourceName ?
: '(stdin)';
149 $this->sourceIndex
= $sourceIndex;
150 $this->charset
= null;
151 $this->utf8
= ! $encoding ||
strtolower($encoding) === 'utf-8';
152 $this->patternModifiers
= $this->utf8 ?
'Aisu' : 'Ais';
153 $this->commentsSeen
= [];
154 $this->commentsSeen
= [];
155 $this->allowVars
= true;
156 $this->cssOnly
= $cssOnly;
157 $this->logger
= $logger ?
: new QuietLogger();
159 if (empty(static::$operatorPattern)) {
160 static::$operatorPattern = '([*\/%+-]|[!=]\=|\>\=?|\<\=?|and|or)';
162 $commentSingle = '\/\/';
163 $commentMultiLeft = '\/\*';
164 $commentMultiRight = '\*\/';
166 static::$commentPattern = $commentMultiLeft . '.*?' . $commentMultiRight;
167 static::$whitePattern = $this->utf8
168 ?
'/' . $commentSingle . '[^\n]*\s*|(' . static::$commentPattern . ')\s*|\s+/AisuS'
169 : '/' . $commentSingle . '[^\n]*\s*|(' . static::$commentPattern . ')\s*|\s+/AisS';
172 $this->cache
= $cache;
176 * Get source file name
182 public function getSourceName()
184 return $this->sourceName
;
194 * @phpstan-return never-return
196 * @throws ParserException
198 * @deprecated use "parseError" and throw the exception in the caller instead.
200 public function throwParseError($msg = 'parse error')
203 'The method "throwParseError" is deprecated. Use "parseError" and throw the exception in the caller instead',
207 throw $this->parseError($msg);
211 * Creates a parser error
217 * @return ParserException
219 public function parseError($msg = 'parse error')
221 list($line, $column) = $this->getSourcePosition($this->count
);
223 $loc = empty($this->sourceName
)
224 ?
"line: $line, column: $column"
225 : "$this->sourceName on line $line, at column $column";
227 if ($this->peek('(.*?)(\n|$)', $m, $this->count
)) {
228 $this->restoreEncoding();
230 $e = new ParserException("$msg: failed at `$m[1]` $loc");
231 $e->setSourcePosition([$this->sourceName
, $line, $column]);
236 $this->restoreEncoding();
238 $e = new ParserException("$msg: $loc");
239 $e->setSourcePosition([$this->sourceName
, $line, $column]);
249 * @param string $buffer
253 public function parse($buffer)
256 $cacheKey = $this->sourceName
. ':' . md5($buffer);
258 'charset' => $this->charset
,
259 'utf8' => $this->utf8
,
261 $v = $this->cache
->getCache('parse', $cacheKey, $parseOptions);
263 if (! \
is_null($v)) {
268 // strip BOM (byte order marker)
269 if (substr($buffer, 0, 3) === "\xef\xbb\xbf") {
270 $buffer = substr($buffer, 3);
273 $this->buffer
= rtrim($buffer, "\x00..\x1f");
276 $this->inParens
= false;
277 $this->eatWhiteDefault
= true;
279 $this->saveEncoding();
280 $this->extractLineNumbers($buffer);
282 $this->pushBlock(null); // root block
284 $this->pushBlock(null);
287 while ($this->parseChunk()) {
291 if ($this->count
!== \
strlen($this->buffer
)) {
292 throw $this->parseError();
295 if (! empty($this->env
->parent
)) {
296 throw $this->parseError('unclosed block');
299 if ($this->charset
) {
300 array_unshift($this->env
->children
, $this->charset
);
303 $this->restoreEncoding();
306 $this->cache
->setCache('parse', $cacheKey, $this->env
, $parseOptions);
313 * Parse a value or value list
317 * @param string $buffer
318 * @param string|array $out
322 public function parseValue($buffer, &$out)
326 $this->inParens
= false;
327 $this->eatWhiteDefault
= true;
328 $this->buffer
= (string) $buffer;
330 $this->saveEncoding();
331 $this->extractLineNumbers($this->buffer
);
333 $list = $this->valueList($out);
335 $this->restoreEncoding();
341 * Parse a selector or selector list
345 * @param string $buffer
346 * @param string|array $out
347 * @param bool $shouldValidate
351 public function parseSelector($buffer, &$out, $shouldValidate = true)
355 $this->inParens
= false;
356 $this->eatWhiteDefault
= true;
357 $this->buffer
= (string) $buffer;
359 $this->saveEncoding();
360 $this->extractLineNumbers($this->buffer
);
362 // discard space/comments at the start
363 $this->discardComments
= true;
365 $this->discardComments
= false;
367 $selector = $this->selectors($out);
369 $this->restoreEncoding();
371 if ($shouldValidate && $this->count
!== strlen($buffer)) {
372 throw $this->parseError("`" . substr($buffer, $this->count
) . "` is not a valid Selector in `$buffer`");
379 * Parse a media Query
383 * @param string $buffer
384 * @param string|array $out
388 public function parseMediaQueryList($buffer, &$out)
392 $this->inParens
= false;
393 $this->eatWhiteDefault
= true;
394 $this->buffer
= (string) $buffer;
396 $this->saveEncoding();
397 $this->extractLineNumbers($this->buffer
);
399 $isMediaQuery = $this->mediaQueryList($out);
401 $this->restoreEncoding();
403 return $isMediaQuery;
407 * Parse a single chunk off the head of the buffer and append it to the
408 * current parse environment.
410 * Returns false when the buffer is empty, or when there is an error.
412 * This function is called repeatedly until the entire document is
415 * This parser is most similar to a recursive descent parser. Single
416 * functions represent discrete grammatical rules for the language, and
417 * they are able to capture the text that represents those rules.
419 * Consider the function Compiler::keyword(). (All parse functions are
420 * structured the same.)
422 * The function takes a single reference argument. When calling the
423 * function it will attempt to match a keyword on the head of the buffer.
424 * If it is successful, it will place the keyword in the referenced
425 * argument, advance the position in the buffer, and return true. If it
426 * fails then it won't advance the buffer and it will return false.
428 * All of these parse functions are powered by Compiler::match(), which behaves
429 * the same way, but takes a literal regular expression. Sometimes it is
430 * more convenient to use match instead of creating a new function.
432 * Because of the format of the functions, to parse an entire string of
433 * grammatical rules, you can chain them together using &&.
435 * But, if some of the rules in the chain succeed before one fails, then
436 * the buffer position will be left at an invalid state. In order to
437 * avoid this, Compiler::seek() is used to remember and set buffer positions.
439 * Before parsing a chain, use $s = $this->count to remember the current
440 * position into $s. Then if a chain fails, use $this->seek($s) to
441 * go back where we started.
445 protected function parseChunk()
450 if (isset($this->buffer
[$this->count
]) && $this->buffer
[$this->count
] === '@') {
452 $this->literal('@at-root', 8) &&
453 ($this->selectors($selector) ||
true) &&
454 ($this->map($with) ||
true) &&
455 (($this->matchChar('(') &&
456 $this->interpolation($with) &&
457 $this->matchChar(')')) ||
true) &&
458 $this->matchChar('{', false)
460 ! $this->cssOnly ||
$this->assertPlainCssValid(false, $s);
462 $atRoot = new AtRootBlock();
463 $this->registerPushedBlock($atRoot, $s);
464 $atRoot->selector
= $selector;
465 $atRoot->with
= $with;
473 $this->literal('@media', 6) &&
474 $this->mediaQueryList($mediaQueryList) &&
475 $this->matchChar('{', false)
477 $media = new MediaBlock();
478 $this->registerPushedBlock($media, $s);
479 $media->queryList
= $mediaQueryList[2];
487 $this->literal('@mixin', 6) &&
488 $this->keyword($mixinName) &&
489 ($this->argumentDef($args) ||
true) &&
490 $this->matchChar('{', false)
492 ! $this->cssOnly ||
$this->assertPlainCssValid(false, $s);
494 $mixin = new CallableBlock(Type
::T_MIXIN
);
495 $this->registerPushedBlock($mixin, $s);
496 $mixin->name
= $mixinName;
497 $mixin->args
= $args;
505 ($this->literal('@include', 8) &&
506 $this->keyword($mixinName) &&
507 ($this->matchChar('(') &&
508 ($this->argValues($argValues) ||
true) &&
509 $this->matchChar(')') ||
true) &&
511 ($this->literal('using', 5) &&
512 $this->argumentDef($argUsing) &&
513 ($this->end() ||
$this->matchChar('{') && $hasBlock = true)) ||
514 $this->matchChar('{') && $hasBlock = true)
516 ! $this->cssOnly ||
$this->assertPlainCssValid(false, $s);
521 isset($argValues) ?
$argValues : null,
523 isset($argUsing) ?
$argUsing : null
526 if (! empty($hasBlock)) {
527 $include = new ContentBlock();
528 $this->registerPushedBlock($include, $s);
529 $include->child
= $child;
531 $this->append($child, $s);
540 $this->literal('@scssphp-import-once', 20) &&
541 $this->valueList($importPath) &&
544 ! $this->cssOnly ||
$this->assertPlainCssValid(false, $s);
546 list($line, $column) = $this->getSourcePosition($s);
547 $file = $this->sourceName
;
548 $this->logger
->warn("The \"@scssphp-import-once\" directive is deprecated and will be removed in ScssPhp 2.0, in \"$file\", line $line, column $column.", true);
550 $this->append([Type
::T_SCSSPHP_IMPORT_ONCE
, $importPath], $s);
558 $this->literal('@import', 7) &&
559 $this->valueList($importPath) &&
560 $importPath[0] !== Type
::T_FUNCTION_CALL
&&
563 if ($this->cssOnly
) {
564 $this->assertPlainCssValid([Type
::T_IMPORT
, $importPath], $s);
565 $this->append([Type
::T_COMMENT
, rtrim(substr($this->buffer
, $s, $this->count
- $s))]);
569 $this->append([Type
::T_IMPORT
, $importPath], $s);
577 $this->literal('@import', 7) &&
578 $this->url($importPath) &&
581 if ($this->cssOnly
) {
582 $this->assertPlainCssValid([Type
::T_IMPORT
, $importPath], $s);
583 $this->append([Type
::T_COMMENT
, rtrim(substr($this->buffer
, $s, $this->count
- $s))]);
587 $this->append([Type
::T_IMPORT
, $importPath], $s);
595 $this->literal('@extend', 7) &&
596 $this->selectors($selectors) &&
599 ! $this->cssOnly ||
$this->assertPlainCssValid(false, $s);
602 $optional = $this->stripOptionalFlag($selectors);
603 $this->append([Type
::T_EXTEND
, $selectors, $optional], $s);
611 $this->literal('@function', 9) &&
612 $this->keyword($fnName) &&
613 $this->argumentDef($args) &&
614 $this->matchChar('{', false)
616 ! $this->cssOnly ||
$this->assertPlainCssValid(false, $s);
618 $func = new CallableBlock(Type
::T_FUNCTION
);
619 $this->registerPushedBlock($func, $s);
620 $func->name
= $fnName;
629 $this->literal('@return', 7) &&
630 ($this->valueList($retVal) ||
true) &&
633 ! $this->cssOnly ||
$this->assertPlainCssValid(false, $s);
635 $this->append([Type
::T_RETURN
, isset($retVal) ?
$retVal : [Type
::T_NULL
]], $s);
643 $this->literal('@each', 5) &&
644 $this->genericList($varNames, 'variable', ',', false) &&
645 $this->literal('in', 2) &&
646 $this->valueList($list) &&
647 $this->matchChar('{', false)
649 ! $this->cssOnly ||
$this->assertPlainCssValid(false, $s);
651 $each = new EachBlock();
652 $this->registerPushedBlock($each, $s);
654 foreach ($varNames[2] as $varName) {
655 $each->vars
[] = $varName[1];
666 $this->literal('@while', 6) &&
667 $this->expression($cond) &&
668 $this->matchChar('{', false)
670 ! $this->cssOnly ||
$this->assertPlainCssValid(false, $s);
673 $cond[0] === Type
::T_LIST
&&
674 ! empty($cond['enclosing']) &&
675 $cond['enclosing'] === 'parent' &&
676 \
count($cond[2]) == 1
678 $cond = reset($cond[2]);
681 $while = new WhileBlock();
682 $this->registerPushedBlock($while, $s);
683 $while->cond
= $cond;
691 $this->literal('@for', 4) &&
692 $this->variable($varName) &&
693 $this->literal('from', 4) &&
694 $this->expression($start) &&
695 ($this->literal('through', 7) ||
696 ($forUntil = true && $this->literal('to', 2))) &&
697 $this->expression($end) &&
698 $this->matchChar('{', false)
700 ! $this->cssOnly ||
$this->assertPlainCssValid(false, $s);
702 $for = new ForBlock();
703 $this->registerPushedBlock($for, $s);
704 $for->var = $varName[1];
705 $for->start
= $start;
707 $for->until
= isset($forUntil);
715 $this->literal('@if', 3) &&
716 $this->functionCallArgumentsList($cond, false, '{', false)
718 ! $this->cssOnly ||
$this->assertPlainCssValid(false, $s);
721 $this->registerPushedBlock($if, $s);
724 $cond[0] === Type
::T_LIST
&&
725 ! empty($cond['enclosing']) &&
726 $cond['enclosing'] === 'parent' &&
727 \
count($cond[2]) == 1
729 $cond = reset($cond[2]);
741 $this->literal('@debug', 6) &&
742 $this->functionCallArgumentsList($value, false)
744 ! $this->cssOnly ||
$this->assertPlainCssValid(false, $s);
746 $this->append([Type
::T_DEBUG
, $value], $s);
754 $this->literal('@warn', 5) &&
755 $this->functionCallArgumentsList($value, false)
757 ! $this->cssOnly ||
$this->assertPlainCssValid(false, $s);
759 $this->append([Type
::T_WARN
, $value], $s);
767 $this->literal('@error', 6) &&
768 $this->functionCallArgumentsList($value, false)
770 ! $this->cssOnly ||
$this->assertPlainCssValid(false, $s);
772 $this->append([Type
::T_ERROR
, $value], $s);
780 $this->literal('@content', 8) &&
782 $this->matchChar('(') &&
783 $this->argValues($argContent) &&
784 $this->matchChar(')') &&
787 ! $this->cssOnly ||
$this->assertPlainCssValid(false, $s);
789 $this->append([Type
::T_MIXIN_CONTENT
, isset($argContent) ?
$argContent : null], $s);
796 $last = $this->last();
798 if (isset($last) && $last[0] === Type
::T_IF
) {
800 assert($if instanceof IfBlock
);
802 if ($this->literal('@else', 5)) {
803 if ($this->matchChar('{', false)) {
804 $else = new ElseBlock();
806 $this->literal('if', 2) &&
807 $this->functionCallArgumentsList($cond, false, '{', false)
809 $else = new ElseifBlock();
814 $this->registerPushedBlock($else, $s);
815 $if->cases
[] = $else;
824 // only retain the first @charset directive encountered
826 $this->literal('@charset', 8) &&
827 $this->valueList($charset) &&
830 if (! isset($this->charset
)) {
831 $statement = [Type
::T_CHARSET
, $charset];
833 list($line, $column) = $this->getSourcePosition($s);
835 $statement[static::SOURCE_LINE
] = $line;
836 $statement[static::SOURCE_COLUMN
] = $column;
837 $statement[static::SOURCE_INDEX
] = $this->sourceIndex
;
839 $this->charset
= $statement;
848 $this->literal('@supports', 9) &&
849 ($t1 = $this->supportsQuery($supportQuery)) &&
850 ($t2 = $this->matchChar('{', false))
852 $directive = new DirectiveBlock();
853 $this->registerPushedBlock($directive, $s);
854 $directive->name
= 'supports';
855 $directive->value
= $supportQuery;
862 // doesn't match built in directive, do generic one
864 $this->matchChar('@', false) &&
865 $this->mixedKeyword($dirName) &&
866 $this->directiveValue($dirValue, '{')
868 if (count($dirName) === 1 && is_string(reset($dirName))) {
869 $dirName = reset($dirName);
871 $dirName = [Type
::T_STRING
, '', $dirName];
873 if ($dirName === 'media') {
874 $directive = new MediaBlock();
876 $directive = new DirectiveBlock();
877 $directive->name
= $dirName;
879 $this->registerPushedBlock($directive, $s);
881 if (isset($dirValue)) {
882 ! $this->cssOnly ||
($dirValue = $this->assertPlainCssValid($dirValue));
883 $directive->value
= $dirValue;
891 // maybe it's a generic blockless directive
893 $this->matchChar('@', false) &&
894 $this->mixedKeyword($dirName) &&
895 ! $this->isKnownGenericDirective($dirName) &&
896 ($this->end(false) ||
($this->directiveValue($dirValue, '') && $this->end(false)))
898 if (\
count($dirName) === 1 && \
is_string(\reset
($dirName))) {
899 $dirName = \reset
($dirName);
901 $dirName = [Type
::T_STRING
, '', $dirName];
904 ! empty($this->env
->parent
) &&
906 ! \
in_array($this->env
->type
, [Type
::T_DIRECTIVE
, Type
::T_MEDIA
])
908 $plain = \trim
(\
substr($this->buffer
, $s, $this->count
- $s));
909 throw $this->parseError(
910 "Unknown directive `{$plain}` not allowed in `" . $this->env
->type
. "` block"
913 // blockless directives with a blank line after keeps their blank lines after
914 // sass-spec compliance purpose
916 $hasBlankLine = false;
917 if ($this->match('\s*?\n\s*\n', $out, false)) {
918 $hasBlankLine = true;
921 $isNotRoot = ! empty($this->env
->parent
);
922 $this->append([Type
::T_DIRECTIVE
, [$dirName, $dirValue, $hasBlankLine, $isNotRoot]], $s);
933 $inCssSelector = null;
934 if ($this->cssOnly
) {
935 $inCssSelector = (! empty($this->env
->parent
) &&
936 ! in_array($this->env
->type
, [Type
::T_DIRECTIVE
, Type
::T_MEDIA
]));
938 // custom properties : right part is static
939 if (($this->customProperty($name) ) && $this->matchChar(':', false)) {
940 $start = $this->count
;
942 // but can be complex and finish with ; or }
943 foreach ([';','}'] as $ending) {
945 $this->openString($ending, $stringValue, '(', ')', false) &&
949 $value = $stringValue;
951 // check if we have only a partial value due to nested [] or { } to take in account
952 $nestingPairs = [['[', ']'], ['{', '}']];
954 foreach ($nestingPairs as $nestingPair) {
955 $p = strpos($this->buffer
, $nestingPair[0], $start);
957 if ($p && $p < $end) {
961 $this->openString($ending, $stringValue, $nestingPair[0], $nestingPair[1], false) &&
966 $value = $stringValue;
972 $this->append([Type
::T_CUSTOM_PROPERTY
, $name, $value], $s);
978 // TODO: output an error here if nothing found according to sass spec
984 // captures most properties before having to parse a selector
986 $this->keyword($name, false) &&
987 $this->literal(': ', 2) &&
988 $this->valueList($value) &&
991 $name = [Type
::T_STRING
, '', [$name]];
992 $this->append([Type
::T_ASSIGN
, $name, $value], $s);
1001 $this->variable($name) &&
1002 $this->matchChar(':') &&
1003 $this->valueList($value) &&
1006 ! $this->cssOnly ||
$this->assertPlainCssValid(false, $s);
1008 // check for '!flag'
1009 $assignmentFlags = $this->stripAssignmentFlags($value);
1010 $this->append([Type
::T_ASSIGN
, $name, $value, $assignmentFlags], $s);
1017 // opening css block
1019 $this->selectors($selectors) &&
1020 $this->matchChar('{', false)
1022 ! $this->cssOnly ||
! $inCssSelector ||
$this->assertPlainCssValid(false);
1024 $this->pushBlock($selectors, $s);
1026 if ($this->eatWhiteDefault
) {
1027 $this->whitespace();
1028 $this->append(null); // collect comments at the beginning if needed
1036 // property assign, or nested assign
1038 $this->propertyName($name) &&
1039 $this->matchChar(':')
1041 $foundSomething = false;
1043 if ($this->valueList($value)) {
1044 if (empty($this->env
->parent
)) {
1045 throw $this->parseError('expected "{"');
1048 $this->append([Type
::T_ASSIGN
, $name, $value], $s);
1049 $foundSomething = true;
1052 if ($this->matchChar('{', false)) {
1053 ! $this->cssOnly ||
$this->assertPlainCssValid(false);
1055 $propBlock = new NestedPropertyBlock();
1056 $this->registerPushedBlock($propBlock, $s);
1057 $propBlock->prefix
= $name;
1058 $propBlock->hasValue
= $foundSomething;
1060 $foundSomething = true;
1061 } elseif ($foundSomething) {
1062 $foundSomething = $this->end();
1065 if ($foundSomething) {
1073 if ($this->matchChar('}', false)) {
1074 $block = $this->popBlock();
1076 if (! isset($block->type
) ||
$block->type
!== Type
::T_IF
) {
1077 if ($this->env
->parent
) {
1078 $this->append(null); // collect comments before next statement if needed
1082 if ($block instanceof ContentBlock
) {
1083 $include = $block->child
;
1084 assert(\
is_array($include));
1085 unset($block->child
);
1086 $include[3] = $block;
1087 $this->append($include, $s);
1088 } elseif (!$block instanceof ElseBlock
&& !$block instanceof ElseifBlock
) {
1089 $type = isset($block->type
) ?
$block->type
: Type
::T_BLOCK
;
1090 $this->append([$type, $block], $s);
1093 // collect comments just after the block closing if needed
1094 if ($this->eatWhiteDefault
) {
1095 $this->whitespace();
1097 if ($this->env
->comments
) {
1098 $this->append(null);
1106 if ($this->matchChar(';')) {
1114 * Push block onto parse tree
1116 * @param array|null $selectors
1121 protected function pushBlock($selectors, $pos = 0)
1124 $b->selectors
= $selectors;
1126 $this->registerPushedBlock($b, $pos);
1137 private function registerPushedBlock(Block
$b, $pos)
1139 list($line, $column) = $this->getSourcePosition($pos);
1141 $b->sourceName
= $this->sourceName
;
1142 $b->sourceLine
= $line;
1143 $b->sourceColumn
= $column;
1144 $b->sourceIndex
= $this->sourceIndex
;
1146 $b->parent
= $this->env
;
1150 } elseif (empty($this->env
->children
)) {
1151 $this->env
->children
= $this->env
->comments
;
1153 $this->env
->comments
= [];
1155 $b->children
= $this->env
->comments
;
1156 $this->env
->comments
= [];
1161 // collect comments at the beginning of a block if needed
1162 if ($this->eatWhiteDefault
) {
1163 $this->whitespace();
1165 if ($this->env
->comments
) {
1166 $this->append(null);
1172 * Push special (named) block onto parse tree
1176 * @param string $type
1181 protected function pushSpecialBlock($type, $pos)
1183 $block = $this->pushBlock(null, $pos);
1184 $block->type
= $type;
1190 * Pop scope and return last block
1194 * @throws \Exception
1196 protected function popBlock()
1199 // collect comments ending just before of a block closing
1200 if ($this->env
->comments
) {
1201 $this->append(null);
1205 $block = $this->env
;
1207 if (empty($block->parent
)) {
1208 throw $this->parseError('unexpected }');
1211 if ($block->type
== Type
::T_AT_ROOT
) {
1212 // keeps the parent in case of self selector &
1213 $block->selfParent
= $block->parent
;
1216 $this->env
= $block->parent
;
1218 unset($block->parent
);
1226 * @param string $regex
1232 protected function peek($regex, &$out, $from = null)
1234 if (! isset($from)) {
1235 $from = $this->count
;
1238 $r = '/' . $regex . '/' . $this->patternModifiers
;
1239 $result = preg_match($r, $this->buffer
, $out, 0, $from);
1245 * Seek to position in input stream (or return current position in input stream)
1249 protected function seek($where)
1251 $this->count
= $where;
1255 * Assert a parsed part is plain CSS Valid
1257 * @param array|false $parsed
1258 * @param int $startPos
1260 * @throws ParserException
1262 protected function assertPlainCssValid($parsed, $startPos = null)
1267 $parsed = $this->isPlainCssValidElement($parsed);
1270 if (! \
is_null($startPos)) {
1271 $plain = rtrim(substr($this->buffer
, $startPos, $this->count
- $startPos));
1272 $message = "Error : `{$plain}` isn't allowed in plain CSS";
1274 $message = 'Error: SCSS syntax not allowed in CSS file';
1277 $message .= " ($type)";
1279 throw $this->parseError($message);
1286 * Check a parsed element is plain CSS Valid
1288 * @param array $parsed
1289 * @param bool $allowExpression
1291 * @return bool|array
1293 protected function isPlainCssValidElement($parsed, $allowExpression = false)
1295 // keep string as is
1296 if (is_string($parsed)) {
1301 \
in_array($parsed[0], [Type
::T_FUNCTION
, Type
::T_FUNCTION_CALL
]) &&
1302 !\
in_array($parsed[1], [
1317 'repeating-linear-gradient',
1318 'repeating-radial-gradient',
1325 Compiler
::isNativeFunction($parsed[1])
1330 switch ($parsed[0]) {
1332 case Type
::T_KEYWORD
:
1334 case Type
::T_NUMBER
:
1338 case Type
::T_COMMENT
:
1339 if (isset($parsed[2])) {
1344 case Type
::T_DIRECTIVE
:
1345 if (\
is_array($parsed[1])) {
1346 $parsed[1][1] = $this->isPlainCssValidElement($parsed[1][1]);
1347 if (! $parsed[1][1]) {
1354 case Type
::T_IMPORT
:
1355 if ($parsed[1][0] === Type
::T_LIST
) {
1358 $parsed[1] = $this->isPlainCssValidElement($parsed[1]);
1359 if ($parsed[1] === false) {
1364 case Type
::T_STRING
:
1365 foreach ($parsed[2] as $k => $substr) {
1366 if (\
is_array($substr)) {
1367 $parsed[2][$k] = $this->isPlainCssValidElement($substr);
1368 if (! $parsed[2][$k]) {
1376 if (!empty($parsed['enclosing'])) {
1379 foreach ($parsed[2] as $k => $listElement) {
1380 $parsed[2][$k] = $this->isPlainCssValidElement($listElement);
1381 if (! $parsed[2][$k]) {
1387 case Type
::T_ASSIGN
:
1388 foreach ([1, 2, 3] as $k) {
1389 if (! empty($parsed[$k])) {
1390 $parsed[$k] = $this->isPlainCssValidElement($parsed[$k]);
1391 if (! $parsed[$k]) {
1398 case Type
::T_EXPRESSION
:
1399 list( ,$op, $lhs, $rhs, $inParens, $whiteBefore, $whiteAfter) = $parsed;
1400 if (! $allowExpression && ! \
in_array($op, ['and', 'or', '/'])) {
1403 $lhs = $this->isPlainCssValidElement($lhs, true);
1407 $rhs = $this->isPlainCssValidElement($rhs, true);
1415 $this->inParens ?
'(' : '',
1417 ($whiteBefore ?
' ' : '') . $op . ($whiteAfter ?
' ' : ''),
1419 $this->inParens ?
')' : ''
1423 case Type
::T_CUSTOM_PROPERTY
:
1425 $parsed[2] = $this->isPlainCssValidElement($parsed[2]);
1431 case Type
::T_FUNCTION
:
1432 $argsList = $parsed[2];
1433 foreach ($argsList[2] as $argElement) {
1434 if (! $this->isPlainCssValidElement($argElement)) {
1440 case Type
::T_FUNCTION_CALL
:
1441 $parsed[0] = Type
::T_FUNCTION
;
1442 $argsList = [Type
::T_LIST
, ',', []];
1443 foreach ($parsed[2] as $arg) {
1444 if ($arg[0] ||
! empty($arg[2])) {
1445 // no named arguments possible in a css function call
1449 $arg = $this->isPlainCssValidElement($arg[1], $parsed[1] === 'calc');
1453 $argsList[2][] = $arg;
1455 $parsed[2] = $argsList;
1463 * Match string looking for either ending delim, escape, or string interpolation
1465 * {@internal This is a workaround for preg_match's 250K string match limit. }}
1467 * @param array $m Matches (passed by reference)
1468 * @param string $delim Delimiter
1470 * @return bool True if match; false otherwise
1472 protected function matchString(&$m, $delim)
1476 $end = \
strlen($this->buffer
);
1478 // look for either ending delim, escape, or string interpolation
1479 foreach (['#{', '\\', "\r", $delim] as $lookahead) {
1480 $pos = strpos($this->buffer
, $lookahead, $this->count
);
1482 if ($pos !== false && $pos < $end) {
1484 $token = $lookahead;
1488 if (! isset($token)) {
1492 $match = substr($this->buffer
, $this->count
, $end - $this->count
);
1498 $this->count
= $end + \
strlen($token);
1504 * Try to match something on head of buffer
1506 * @param string $regex
1508 * @param bool $eatWhitespace
1512 protected function match($regex, &$out, $eatWhitespace = null)
1514 $r = '/' . $regex . '/' . $this->patternModifiers
;
1516 if (! preg_match($r, $this->buffer
, $out, 0, $this->count
)) {
1520 $this->count +
= \
strlen($out[0]);
1522 if (! isset($eatWhitespace)) {
1523 $eatWhitespace = $this->eatWhiteDefault
;
1526 if ($eatWhitespace) {
1527 $this->whitespace();
1534 * Match a single string
1536 * @param string $char
1537 * @param bool $eatWhitespace
1541 protected function matchChar($char, $eatWhitespace = null)
1543 if (! isset($this->buffer
[$this->count
]) ||
$this->buffer
[$this->count
] !== $char) {
1549 if (! isset($eatWhitespace)) {
1550 $eatWhitespace = $this->eatWhiteDefault
;
1553 if ($eatWhitespace) {
1554 $this->whitespace();
1561 * Match literal string
1563 * @param string $what
1565 * @param bool $eatWhitespace
1569 protected function literal($what, $len, $eatWhitespace = null)
1571 if (strcasecmp(substr($this->buffer
, $this->count
, $len), $what) !== 0) {
1575 $this->count +
= $len;
1577 if (! isset($eatWhitespace)) {
1578 $eatWhitespace = $this->eatWhiteDefault
;
1581 if ($eatWhitespace) {
1582 $this->whitespace();
1589 * Match some whitespace
1593 protected function whitespace()
1597 while (preg_match(static::$whitePattern, $this->buffer
, $m, 0, $this->count
)) {
1598 if (isset($m[1]) && empty($this->commentsSeen
[$this->count
])) {
1599 // comment that are kept in the output CSS
1601 $startCommentCount = $this->count
;
1602 $endCommentCount = $this->count + \
strlen($m[1]);
1604 // find interpolations in comment
1605 $p = strpos($this->buffer
, '#{', $this->count
);
1607 while ($p !== false && $p < $endCommentCount) {
1608 $c = substr($this->buffer
, $this->count
, $p - $this->count
);
1613 if ($this->interpolation($out)) {
1614 // keep right spaces in the following string part
1616 while ($this->buffer
[$this->count
- 1] !== '}') {
1623 $comment[] = [Type
::T_COMMENT
, substr($this->buffer
, $p, $this->count
- $p), $out];
1625 list($line, $column) = $this->getSourcePosition($this->count
);
1626 $file = $this->sourceName
;
1627 if (!$this->discardComments
) {
1628 $this->logger
->warn("Unterminated interpolations in multiline comments are deprecated and will be removed in ScssPhp 2.0, in \"$file\", line $line, column $column.", true);
1630 $comment[] = substr($this->buffer
, $this->count
, 2);
1635 $p = strpos($this->buffer
, '#{', $this->count
);
1639 $c = substr($this->buffer
, $this->count
, $endCommentCount - $this->count
);
1642 // single part static comment
1643 $this->appendComment([Type
::T_COMMENT
, $c]);
1646 $staticComment = substr($this->buffer
, $startCommentCount, $endCommentCount - $startCommentCount);
1647 $commentStatement = [Type
::T_COMMENT
, $staticComment, [Type
::T_STRING
, '', $comment]];
1649 list($line, $column) = $this->getSourcePosition($startCommentCount);
1650 $commentStatement[self
::SOURCE_LINE
] = $line;
1651 $commentStatement[self
::SOURCE_COLUMN
] = $column;
1652 $commentStatement[self
::SOURCE_INDEX
] = $this->sourceIndex
;
1654 $this->appendComment($commentStatement);
1657 $this->commentsSeen
[$startCommentCount] = true;
1658 $this->count
= $endCommentCount;
1660 // comment that are ignored and not kept in the output css
1661 $this->count +
= \
strlen($m[0]);
1662 // silent comments are not allowed in plain CSS files
1664 ||
! \
strlen(trim($m[0]))
1665 ||
$this->assertPlainCssValid(false, $this->count
- \
strlen($m[0]));
1675 * Append comment to current block
1677 * @param array $comment
1679 protected function appendComment($comment)
1681 if (! $this->discardComments
) {
1682 $this->env
->comments
[] = $comment;
1687 * Append statement to current block
1689 * @param array|null $statement
1692 protected function append($statement, $pos = null)
1694 if (! \
is_null($statement)) {
1695 ! $this->cssOnly ||
($statement = $this->assertPlainCssValid($statement, $pos));
1697 if (! \
is_null($pos)) {
1698 list($line, $column) = $this->getSourcePosition($pos);
1700 $statement[static::SOURCE_LINE
] = $line;
1701 $statement[static::SOURCE_COLUMN
] = $column;
1702 $statement[static::SOURCE_INDEX
] = $this->sourceIndex
;
1705 $this->env
->children
[] = $statement;
1708 $comments = $this->env
->comments
;
1711 $this->env
->children
= array_merge($this->env
->children
, $comments);
1712 $this->env
->comments
= [];
1717 * Returns last child was appended
1719 * @return array|null
1721 protected function last()
1723 $i = \
count($this->env
->children
) - 1;
1725 if (isset($this->env
->children
[$i])) {
1726 return $this->env
->children
[$i];
1731 * Parse media query list
1737 protected function mediaQueryList(&$out)
1739 return $this->genericList($out, 'mediaQuery', ',', false);
1749 protected function mediaQuery(&$out)
1751 $expressions = null;
1755 ($this->literal('only', 4) && ($only = true) ||
1756 $this->literal('not', 3) && ($not = true) ||
true) &&
1757 $this->mixedKeyword($mediaType)
1759 $prop = [Type
::T_MEDIA_TYPE
];
1762 $prop[] = [Type
::T_KEYWORD
, 'only'];
1766 $prop[] = [Type
::T_KEYWORD
, 'not'];
1769 $media = [Type
::T_LIST
, '', []];
1771 foreach ((array) $mediaType as $type) {
1772 if (\
is_array($type)) {
1773 $media[2][] = $type;
1775 $media[2][] = [Type
::T_KEYWORD
, $type];
1783 if (empty($parts) ||
$this->literal('and', 3)) {
1784 $this->genericList($expressions, 'mediaExpression', 'and', false);
1786 if (\
is_array($expressions)) {
1787 $parts = array_merge($parts, $expressions[2]);
1797 * Parse supports query
1803 protected function supportsQuery(&$out)
1805 $expressions = null;
1813 ($this->literal('not', 3) && ($not = true) ||
true) &&
1814 $this->matchChar('(') &&
1815 ($this->expression($property)) &&
1816 $this->literal(': ', 2) &&
1817 $this->valueList($value) &&
1818 $this->matchChar(')')
1820 $support = [Type
::T_STRING
, '', [[Type
::T_KEYWORD
, ($not ?
'not ' : '') . '(']]];
1821 $support[2][] = $property;
1822 $support[2][] = [Type
::T_KEYWORD
, ': '];
1823 $support[2][] = $value;
1824 $support[2][] = [Type
::T_KEYWORD
, ')'];
1826 $parts[] = $support;
1833 $this->matchChar('(') &&
1834 $this->supportsQuery($subQuery) &&
1835 $this->matchChar(')')
1837 $parts[] = [Type
::T_STRING
, '', [[Type
::T_KEYWORD
, '('], $subQuery, [Type
::T_KEYWORD
, ')']]];
1844 $this->literal('not', 3) &&
1845 $this->supportsQuery($subQuery)
1847 $parts[] = [Type
::T_STRING
, '', [[Type
::T_KEYWORD
, 'not '], $subQuery]];
1854 $this->literal('selector(', 9) &&
1855 $this->selector($selector) &&
1856 $this->matchChar(')')
1858 $support = [Type
::T_STRING
, '', [[Type
::T_KEYWORD
, 'selector(']]];
1860 $selectorList = [Type
::T_LIST
, '', []];
1862 foreach ($selector as $sc) {
1863 $compound = [Type
::T_STRING
, '', []];
1865 foreach ($sc as $scp) {
1866 if (\
is_array($scp)) {
1867 $compound[2][] = $scp;
1869 $compound[2][] = [Type
::T_KEYWORD
, $scp];
1873 $selectorList[2][] = $compound;
1876 $support[2][] = $selectorList;
1877 $support[2][] = [Type
::T_KEYWORD
, ')'];
1878 $parts[] = $support;
1884 if ($this->variable($var) or $this->interpolation($var)) {
1892 $this->literal('and', 3) &&
1893 $this->genericList($expressions, 'supportsQuery', ' and', false)
1895 array_unshift($expressions[2], [Type
::T_STRING
, '', $parts]);
1897 $parts = [$expressions];
1904 $this->literal('or', 2) &&
1905 $this->genericList($expressions, 'supportsQuery', ' or', false)
1907 array_unshift($expressions[2], [Type
::T_STRING
, '', $parts]);
1909 $parts = [$expressions];
1915 if (\
count($parts)) {
1916 if ($this->eatWhiteDefault
) {
1917 $this->whitespace();
1920 $out = [Type
::T_STRING
, '', $parts];
1930 * Parse media expression
1936 protected function mediaExpression(&$out)
1942 $this->matchChar('(') &&
1943 $this->expression($feature) &&
1944 ($this->matchChar(':') &&
1945 $this->expression($value) ||
true) &&
1946 $this->matchChar(')')
1948 $out = [Type
::T_MEDIA_EXPRESSION
, $feature];
1963 * Parse argument values
1969 protected function argValues(&$out)
1971 $discardComments = $this->discardComments
;
1972 $this->discardComments
= true;
1974 if ($this->genericList($list, 'argValue', ',', false)) {
1977 $this->discardComments
= $discardComments;
1982 $this->discardComments
= $discardComments;
1988 * Parse argument value
1994 protected function argValue(&$out)
2000 if (! $this->variable($keyword) ||
! $this->matchChar(':')) {
2006 if ($this->genericList($value, 'expression', '', true)) {
2007 $out = [$keyword, $value, false];
2010 if ($this->literal('...', 3)) {
2023 * Check if a generic directive is known to be able to allow almost any syntax or not
2024 * @param mixed $directiveName
2027 protected function isKnownGenericDirective($directiveName)
2029 if (\
is_array($directiveName) && \
is_string(reset($directiveName))) {
2030 $directiveName = reset($directiveName);
2032 if (! \
is_string($directiveName)) {
2036 \
in_array($directiveName, [
2041 'scssphp-import-once',
2070 * Parse directive value list that considers $vars as keyword
2073 * @param bool|string $endChar
2077 protected function directiveValue(&$out, $endChar = false)
2081 if ($this->variable($out)) {
2082 if ($endChar && $this->matchChar($endChar, false)) {
2086 if (! $endChar && $this->end()) {
2093 if (\
is_string($endChar) && $this->openString($endChar ?
$endChar : ';', $out, null, null, true, ";}{")) {
2094 if ($endChar && $this->matchChar($endChar, false)) {
2098 if (!$endChar && $this->end()) {
2106 $allowVars = $this->allowVars
;
2107 $this->allowVars
= false;
2109 $res = $this->genericList($out, 'spaceList', ',');
2110 $this->allowVars
= $allowVars;
2113 if ($endChar && $this->matchChar($endChar, false)) {
2117 if (! $endChar && $this->end()) {
2124 if ($endChar && $this->matchChar($endChar, false)) {
2132 * Parse comma separated value list
2138 protected function valueList(&$out)
2140 $discardComments = $this->discardComments
;
2141 $this->discardComments
= true;
2142 $res = $this->genericList($out, 'spaceList', ',');
2143 $this->discardComments
= $discardComments;
2149 * Parse a function call, where externals () are part of the call
2150 * and not of the value list
2153 * @param bool $mandatoryEnclos
2154 * @param null|string $charAfter
2155 * @param null|bool $eatWhiteSp
2159 protected function functionCallArgumentsList(&$out, $mandatoryEnclos = true, $charAfter = null, $eatWhiteSp = null)
2164 $this->matchChar('(') &&
2165 $this->valueList($out) &&
2166 $this->matchChar(')') &&
2167 ($charAfter ?
$this->matchChar($charAfter, $eatWhiteSp) : $this->end())
2172 if (! $mandatoryEnclos) {
2176 $this->valueList($out) &&
2177 ($charAfter ?
$this->matchChar($charAfter, $eatWhiteSp) : $this->end())
2189 * Parse space separated value list
2195 protected function spaceList(&$out)
2197 return $this->genericList($out, 'expression');
2201 * Parse generic list
2204 * @param string $parseItem The name of the method used to parse items
2205 * @param string $delim
2206 * @param bool $flatten
2210 protected function genericList(&$out, $parseItem, $delim = '', $flatten = true)
2216 while ($this->$parseItem($value)) {
2217 $trailing_delim = false;
2221 if (! $this->literal($delim, \
strlen($delim))) {
2225 $trailing_delim = true;
2227 // if no delim watch that a keyword didn't eat the single/double quote
2228 // from the following starting string
2229 if ($value[0] === Type
::T_KEYWORD
) {
2232 $last_char = substr($word, -1);
2235 strlen($word) > 1 &&
2236 in_array($last_char, [ "'", '"']) &&
2237 substr($word, -2, 1) !== '\\'
2239 // if there is a non escaped opening quote in the keyword, this seems unlikely a mistake
2240 $word = str_replace('\\' . $last_char, '\\\\', $word);
2241 if (strpos($word, $last_char) < strlen($word) - 1) {
2245 $currentCount = $this->count
;
2247 // let's try to rewind to previous char and try a parse
2249 // in case the keyword also eat spaces
2250 while (substr($this->buffer
, $this->count
, 1) !== $last_char) {
2255 if ($this->$parseItem($nextValue)) {
2256 if ($nextValue[0] === Type
::T_KEYWORD
&& $nextValue[1] === $last_char) {
2257 // bad try, forget it
2258 $this->seek($currentCount);
2261 if ($nextValue[0] !== Type
::T_STRING
) {
2262 // bad try, forget it
2263 $this->seek($currentCount);
2267 // OK it was a good idea
2268 $value[1] = substr($value[1], 0, -1);
2271 $items[] = $nextValue;
2273 // bad try, forget it
2274 $this->seek($currentCount);
2288 if ($trailing_delim) {
2289 $items[] = [Type
::T_NULL
];
2292 if ($flatten && \
count($items) === 1) {
2295 $out = [Type
::T_LIST
, $delim, $items];
2305 * @param bool $listOnly
2306 * @param bool $lookForExp
2310 protected function expression(&$out, $listOnly = false, $lookForExp = true)
2313 $discard = $this->discardComments
;
2314 $this->discardComments
= true;
2315 $allowedTypes = ($listOnly ?
[Type
::T_LIST
] : [Type
::T_LIST
, Type
::T_MAP
]);
2317 if ($this->matchChar('(')) {
2318 if ($this->enclosedExpression($lhs, $s, ')', $allowedTypes)) {
2320 $out = $this->expHelper($lhs, 0);
2325 $this->discardComments
= $discard;
2333 if (\
in_array(Type
::T_LIST
, $allowedTypes) && $this->matchChar('[')) {
2334 if ($this->enclosedExpression($lhs, $s, ']', [Type
::T_LIST
])) {
2336 $out = $this->expHelper($lhs, 0);
2341 $this->discardComments
= $discard;
2349 if (! $listOnly && $this->value($lhs)) {
2351 $out = $this->expHelper($lhs, 0);
2356 $this->discardComments
= $discard;
2361 $this->discardComments
= $discard;
2367 * Parse expression specifically checking for lists in parenthesis or brackets
2371 * @param string $closingParen
2372 * @param array $allowedTypes
2376 protected function enclosedExpression(&$out, $s, $closingParen = ')', $allowedTypes = [Type
::T_LIST
, Type
::T_MAP
])
2378 if ($this->matchChar($closingParen) && \
in_array(Type
::T_LIST
, $allowedTypes)) {
2379 $out = [Type
::T_LIST
, '', []];
2381 switch ($closingParen) {
2383 $out['enclosing'] = 'parent'; // parenthesis list
2387 $out['enclosing'] = 'bracket'; // bracketed list
2395 $this->valueList($out) &&
2396 $this->matchChar($closingParen) && ! ($closingParen === ')' &&
2397 \
in_array($out[0], [Type
::T_EXPRESSION
, Type
::T_UNARY
])) &&
2398 \
in_array(Type
::T_LIST
, $allowedTypes)
2400 if ($out[0] !== Type
::T_LIST ||
! empty($out['enclosing'])) {
2401 $out = [Type
::T_LIST
, '', [$out]];
2404 switch ($closingParen) {
2406 $out['enclosing'] = 'parent'; // parenthesis list
2410 $out['enclosing'] = 'bracket'; // bracketed list
2419 if (\
in_array(Type
::T_MAP
, $allowedTypes) && $this->map($out)) {
2427 * Parse left-hand side of subexpression
2434 protected function expHelper($lhs, $minP)
2436 $operators = static::$operatorPattern;
2439 $whiteBefore = isset($this->buffer
[$this->count
- 1]) &&
2440 ctype_space($this->buffer
[$this->count
- 1]);
2442 while ($this->match($operators, $m, false) && static::$precedence[$m[1]] >= $minP) {
2443 $whiteAfter = isset($this->buffer
[$this->count
]) &&
2444 ctype_space($this->buffer
[$this->count
]);
2445 $varAfter = isset($this->buffer
[$this->count
]) &&
2446 $this->buffer
[$this->count
] === '$';
2448 $this->whitespace();
2452 // don't turn negative numbers into expressions
2453 if ($op === '-' && $whiteBefore && ! $whiteAfter && ! $varAfter) {
2457 if (! $this->value($rhs) && ! $this->expression($rhs, true, false)) {
2461 if ($op === '-' && ! $whiteAfter && $rhs[0] === Type
::T_KEYWORD
) {
2465 // consume higher-precedence operators on the right-hand side
2466 $rhs = $this->expHelper($rhs, static::$precedence[$op] +
1);
2468 $lhs = [Type
::T_EXPRESSION
, $op, $lhs, $rhs, $this->inParens
, $whiteBefore, $whiteAfter];
2471 $whiteBefore = isset($this->buffer
[$this->count
- 1]) &&
2472 ctype_space($this->buffer
[$this->count
- 1]);
2487 protected function value(&$out)
2489 if (! isset($this->buffer
[$this->count
])) {
2494 $char = $this->buffer
[$this->count
];
2497 $this->literal('url(', 4) &&
2498 $this->match('data:([a-z]+)\/([a-z0-9.+-]+);base64,', $m, false)
2502 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwyxz0123456789+/=',
2506 $this->count +
= $len;
2508 if ($this->matchChar(')')) {
2509 $content = substr($this->buffer
, $s, $this->count
- $s);
2510 $out = [Type
::T_KEYWORD
, $content];
2519 $this->literal('url(', 4, false) &&
2520 $this->match('\s*(\/\/[^\s\)]+)\s*', $m)
2522 $content = 'url(' . $m[1];
2524 if ($this->matchChar(')')) {
2526 $out = [Type
::T_KEYWORD
, $content];
2535 if ($char === 'n' && $this->literal('not', 3, false)) {
2537 $this->whitespace() &&
2538 $this->value($inner)
2540 $out = [Type
::T_UNARY
, 'not', $inner, $this->inParens
];
2547 if ($this->parenValue($inner)) {
2548 $out = [Type
::T_UNARY
, 'not', $inner, $this->inParens
];
2557 if ($char === '+') {
2560 $follow_white = $this->whitespace();
2562 if ($this->value($inner)) {
2563 $out = [Type
::T_UNARY
, '+', $inner, $this->inParens
];
2568 if ($follow_white) {
2569 $out = [Type
::T_KEYWORD
, $char];
2579 if ($char === '-') {
2580 if ($this->customProperty($out)) {
2586 $follow_white = $this->whitespace();
2588 if ($this->variable($inner) ||
$this->unit($inner) ||
$this->parenValue($inner)) {
2589 $out = [Type
::T_UNARY
, '-', $inner, $this->inParens
];
2595 $this->keyword($inner) &&
2596 ! $this->func($inner, $out)
2598 $out = [Type
::T_UNARY
, '-', $inner, $this->inParens
];
2603 if ($follow_white) {
2604 $out = [Type
::T_KEYWORD
, $char];
2613 if ($char === '(' && $this->parenValue($out)) {
2617 if ($char === '#') {
2618 if ($this->interpolation($out) ||
$this->color($out)) {
2624 if ($this->keyword($keyword)) {
2625 $out = [Type
::T_KEYWORD
, '#' . $keyword];
2633 if ($this->matchChar('&', true)) {
2634 $out = [Type
::T_SELF
];
2639 if ($char === '$' && $this->variable($out)) {
2643 if ($char === 'p' && $this->progid($out)) {
2647 if (($char === '"' ||
$char === "'") && $this->string($out)) {
2651 if ($this->unit($out)) {
2655 // unicode range with wildcards
2657 $this->literal('U+', 2) &&
2658 $this->match('\?+|([0-9A-F]+(\?+|(-[0-9A-F]+))?)', $m, false)
2660 $unicode = explode('-', $m[0]);
2661 if (strlen(reset($unicode)) <= 6 && strlen(end($unicode)) <= 6) {
2662 $out = [Type
::T_KEYWORD
, 'U+' . $m[0]];
2666 $this->count
-= strlen($m[0]) +
2;
2669 if ($this->keyword($keyword, false)) {
2670 if ($this->func($keyword, $out)) {
2674 $this->whitespace();
2676 if ($keyword === 'null') {
2677 $out = [Type
::T_NULL
];
2679 $out = [Type
::T_KEYWORD
, $keyword];
2689 * Parse parenthesized value
2695 protected function parenValue(&$out)
2699 $inParens = $this->inParens
;
2701 if ($this->matchChar('(')) {
2702 if ($this->matchChar(')')) {
2703 $out = [Type
::T_LIST
, '', []];
2708 $this->inParens
= true;
2711 $this->expression($exp) &&
2712 $this->matchChar(')')
2715 $this->inParens
= $inParens;
2721 $this->inParens
= $inParens;
2734 protected function progid(&$out)
2739 $this->literal('progid:', 7, false) &&
2740 $this->openString('(', $fn) &&
2741 $this->matchChar('(')
2743 $this->openString(')', $args, '(');
2745 if ($this->matchChar(')')) {
2746 $out = [Type
::T_STRING
, '', [
2747 'progid:', $fn, '(', $args, ')'
2760 * Parse function call
2762 * @param string $name
2763 * @param array $func
2767 protected function func($name, &$func)
2771 if ($this->matchChar('(')) {
2772 if ($name === 'alpha' && $this->argumentList($args)) {
2773 $func = [Type
::T_FUNCTION
, $name, [Type
::T_STRING
, '', $args]];
2778 if ($name !== 'expression' && ! preg_match('/^(-[a-z]+-)?calc$/', $name)) {
2782 $this->argValues($args) &&
2783 $this->matchChar(')')
2785 $func = [Type
::T_FUNCTION_CALL
, $name, $args];
2794 ($this->openString(')', $str, '(') ||
true) &&
2795 $this->matchChar(')')
2799 if (! empty($str)) {
2800 $args[] = [null, [Type
::T_STRING
, '', [$str]]];
2803 $func = [Type
::T_FUNCTION_CALL
, $name, $args];
2815 * Parse function call argument list
2821 protected function argumentList(&$out)
2824 $this->matchChar('(');
2828 while ($this->keyword($var)) {
2830 $this->matchChar('=') &&
2831 $this->expression($exp)
2833 $args[] = [Type
::T_STRING
, '', [$var . '=']];
2841 if (! $this->matchChar(',')) {
2845 $args[] = [Type
::T_STRING
, '', [', ']];
2848 if (! $this->matchChar(')') ||
! $args) {
2860 * Parse mixin/function definition argument list
2866 protected function argumentDef(&$out)
2869 $this->matchChar('(');
2873 while ($this->variable($var)) {
2874 $arg = [$var[1], null, false];
2879 $this->matchChar(':') &&
2880 $this->genericList($defaultVal, 'expression', '', true)
2882 $arg[1] = $defaultVal;
2889 if ($this->literal('...', 3)) {
2890 $sss = $this->count
;
2892 if (! $this->matchChar(')')) {
2893 throw $this->parseError('... has to be after the final argument');
2905 if (! $this->matchChar(',')) {
2910 if (! $this->matchChar(')')) {
2928 protected function map(&$out)
2932 if (! $this->matchChar('(')) {
2940 $this->genericList($key, 'expression', '', true) &&
2941 $this->matchChar(':') &&
2942 $this->genericList($value, 'expression', '', true)
2947 if (! $this->matchChar(',')) {
2952 if (! $keys ||
! $this->matchChar(')')) {
2958 $out = [Type
::T_MAP
, $keys, $values];
2970 protected function color(&$out)
2974 if ($this->match('(#([0-9a-f]+)\b)', $m)) {
2975 if (\
in_array(\
strlen($m[2]), [3,4,6,8])) {
2976 $out = [Type
::T_KEYWORD
, $m[0]];
2990 * Parse number with unit
2992 * @param array $unit
2996 protected function unit(&$unit)
3000 if ($this->match('([0-9]*(\.)?[0-9]+)([%a-zA-Z]+)?', $m, false)) {
3001 if (\
strlen($this->buffer
) === $this->count ||
! ctype_digit($this->buffer
[$this->count
])) {
3002 $this->whitespace();
3004 $unit = new Node\
Number($m[1], empty($m[3]) ?
'' : $m[3]);
3019 * @param bool $keepDelimWithInterpolation
3023 protected function string(&$out, $keepDelimWithInterpolation = false)
3027 if ($this->matchChar('"', false)) {
3029 } elseif ($this->matchChar("'", false)) {
3036 $oldWhite = $this->eatWhiteDefault
;
3037 $this->eatWhiteDefault
= false;
3038 $hasInterpolation = false;
3040 while ($this->matchString($m, $delim)) {
3045 if ($m[2] === '#{') {
3046 $this->count
-= \
strlen($m[2]);
3048 if ($this->interpolation($inter, false)) {
3049 $content[] = $inter;
3050 $hasInterpolation = true;
3052 $this->count +
= \
strlen($m[2]);
3053 $content[] = '#{'; // ignore it
3055 } elseif ($m[2] === "\r") {
3056 $content[] = chr(10);
3058 # DEPRECATION WARNING on line x, column y of zzz:
3059 # Unescaped multiline strings are deprecated and will be removed in a future version of Sass.
3060 # To include a newline in a string, use "\a" or "\a " as in CSS.
3061 if ($this->matchChar("\n", false)) {
3064 } elseif ($m[2] === '\\') {
3066 $this->literal("\r\n", 2, false) ||
3067 $this->matchChar("\r", false) ||
3068 $this->matchChar("\n", false) ||
3069 $this->matchChar("\f", false)
3071 // this is a continuation escaping, to be ignored
3072 } elseif ($this->matchEscapeCharacter($c)) {
3075 throw $this->parseError('Unterminated escape sequence');
3078 $this->count
-= \
strlen($delim);
3083 $this->eatWhiteDefault
= $oldWhite;
3085 if ($this->literal($delim, \
strlen($delim))) {
3086 if ($hasInterpolation && ! $keepDelimWithInterpolation) {
3090 $out = [Type
::T_STRING
, $delim, $content];
3101 * @param string $out
3102 * @param bool $inKeywords
3106 protected function matchEscapeCharacter(&$out, $inKeywords = false)
3109 if ($this->match('[a-f0-9]', $m, false)) {
3112 for ($i = 5; $i--;) {
3113 if ($this->match('[a-f0-9]', $m, false)) {
3120 // CSS allows Unicode escape sequences to be followed by a delimiter space
3121 // (necessary in some cases for shorter sequences to disambiguate their end)
3122 $this->matchChar(' ', false);
3124 $value = hexdec($hex);
3126 if (!$inKeywords && ($value == 0 ||
($value >= 0xD800 && $value <= 0xDFFF) ||
$value >= 0x10FFFF)) {
3127 $out = "\xEF\xBF\xBD"; // "\u{FFFD}" but with a syntax supported on PHP 5
3128 } elseif ($value < 0x20) {
3129 $out = Util
::mbChr($value);
3131 $out = Util
::mbChr($value);
3137 if ($this->match('.', $m, false)) {
3138 if ($inKeywords && in_array($m[0], ["'",'"','@','&',' ','\\',':','/','%'])) {
3151 * Parse keyword or interpolation
3154 * @param bool $restricted
3158 protected function mixedKeyword(&$out, $restricted = false)
3162 $oldWhite = $this->eatWhiteDefault
;
3163 $this->eatWhiteDefault
= false;
3166 if ($restricted ?
$this->restrictedKeyword($key) : $this->keyword($key)) {
3171 if ($this->interpolation($inter)) {
3179 $this->eatWhiteDefault
= $oldWhite;
3185 if ($this->eatWhiteDefault
) {
3186 $this->whitespace();
3195 * Parse an unbounded string stopped by $end
3197 * @param string $end
3199 * @param string $nestOpen
3200 * @param string $nestClose
3201 * @param bool $rtrim
3202 * @param string $disallow
3206 protected function openString($end, &$out, $nestOpen = null, $nestClose = null, $rtrim = true, $disallow = null)
3208 $oldWhite = $this->eatWhiteDefault
;
3209 $this->eatWhiteDefault
= false;
3211 if ($nestOpen && ! $nestClose) {
3215 $patt = ($disallow ?
'[^' . $this->pregQuote($disallow) . ']' : '.');
3216 $patt = '(' . $patt . '*?)([\'"]|#\{|'
3217 . $this->pregQuote($end) . '|'
3218 . (($nestClose && $nestClose !== $end) ?
$this->pregQuote($nestClose) . '|' : '')
3219 . static::$commentPattern . ')';
3225 while ($this->match($patt, $m, false)) {
3226 if (isset($m[1]) && $m[1] !== '') {
3230 $nestingLevel +
= substr_count($m[1], $nestOpen);
3236 $this->count
-= \
strlen($tok);
3238 if ($tok === $end && ! $nestingLevel) {
3242 if ($tok === $nestClose) {
3246 if (($tok === "'" ||
$tok === '"') && $this->string($str, true)) {
3251 if ($tok === '#{' && $this->interpolation($inter)) {
3252 $content[] = $inter;
3257 $this->count +
= \
strlen($tok);
3260 $this->eatWhiteDefault
= $oldWhite;
3262 if (! $content ||
$tok !== $end) {
3267 if ($rtrim && \
is_string(end($content))) {
3268 $content[\
count($content) - 1] = rtrim(end($content));
3271 $out = [Type
::T_STRING
, '', $content];
3277 * Parser interpolation
3279 * @param string|array $out
3280 * @param bool $lookWhite save information about whitespace before and after
3284 protected function interpolation(&$out, $lookWhite = true)
3286 $oldWhite = $this->eatWhiteDefault
;
3287 $allowVars = $this->allowVars
;
3288 $this->allowVars
= true;
3289 $this->eatWhiteDefault
= true;
3294 $this->literal('#{', 2) &&
3295 $this->valueList($value) &&
3296 $this->matchChar('}', false)
3298 if ($value === [Type
::T_SELF
]) {
3302 $left = ($s > 0 && preg_match('/\s/', $this->buffer
[$s - 1])) ?
' ' : '';
3304 ! empty($this->buffer
[$this->count
]) &&
3305 preg_match('/\s/', $this->buffer
[$this->count
])
3308 $left = $right = false;
3311 $out = [Type
::T_INTERPOLATE
, $value, $left, $right];
3314 $this->eatWhiteDefault
= $oldWhite;
3315 $this->allowVars
= $allowVars;
3317 if ($this->eatWhiteDefault
) {
3318 $this->whitespace();
3326 $this->eatWhiteDefault
= $oldWhite;
3327 $this->allowVars
= $allowVars;
3333 * Parse property name (as an array of parts or a string)
3339 protected function propertyName(&$out)
3343 $oldWhite = $this->eatWhiteDefault
;
3344 $this->eatWhiteDefault
= false;
3347 if ($this->interpolation($inter)) {
3352 if ($this->keyword($text)) {
3357 if (! $parts && $this->match('[:.#]', $m, false)) {
3366 $this->eatWhiteDefault
= $oldWhite;
3372 // match comment hack
3373 if (preg_match(static::$whitePattern, $this->buffer
, $m, 0, $this->count
)) {
3374 if (! empty($m[0])) {
3376 $this->count +
= \
strlen($m[0]);
3380 $this->whitespace(); // get any extra whitespace
3382 $out = [Type
::T_STRING
, '', $parts];
3388 * Parse custom property name (as an array of parts or a string)
3394 protected function customProperty(&$out)
3398 if (! $this->literal('--', 2, false)) {
3404 $oldWhite = $this->eatWhiteDefault
;
3405 $this->eatWhiteDefault
= false;
3408 if ($this->interpolation($inter)) {
3413 if ($this->matchChar('&', false)) {
3414 $parts[] = [Type
::T_SELF
];
3418 if ($this->variable($var)) {
3423 if ($this->keyword($text)) {
3431 $this->eatWhiteDefault
= $oldWhite;
3433 if (\
count($parts) == 1) {
3439 $this->whitespace(); // get any extra whitespace
3441 $out = [Type
::T_STRING
, '', $parts];
3447 * Parse comma separated selector list
3450 * @param string|bool $subSelector
3454 protected function selectors(&$out, $subSelector = false)
3459 while ($this->selector($sel, $subSelector)) {
3460 $selectors[] = $sel;
3462 if (! $this->matchChar(',', true)) {
3466 while ($this->matchChar(',', true)) {
3483 * Parse whitespace separated selector list
3486 * @param string|bool $subSelector
3490 protected function selector(&$out, $subSelector = false)
3494 $discardComments = $this->discardComments
;
3495 $this->discardComments
= true;
3500 if ($this->match('[>+~]+', $m, true)) {
3502 $subSelector && \
is_string($subSelector) && strpos($subSelector, 'nth-') === 0 &&
3503 $m[0] === '+' && $this->match("(\d+|n\b)", $counter)
3507 $selector[] = [$m[0]];
3512 if ($this->selectorSingle($part, $subSelector)) {
3513 $selector[] = $part;
3514 $this->whitespace();
3521 $this->discardComments
= $discardComments;
3533 * parsing escaped chars in selectors:
3534 * - escaped single chars are kept escaped in the selector but in a normalized form
3535 * (if not in 0-9a-f range as this would be ambigous)
3536 * - other escaped sequences (multibyte chars or 0-9a-f) are kept in their initial escaped form,
3537 * normalized to lowercase
3539 * TODO: this is a fallback solution. Ideally escaped chars in selectors should be encoded as the genuine chars,
3540 * and escaping added when printing in the Compiler, where/if it's mandatory
3541 * - but this require a better formal selector representation instead of the array we have now
3543 * @param string $out
3544 * @param bool $keepEscapedNumber
3548 protected function matchEscapeCharacterInSelector(&$out, $keepEscapedNumber = false)
3550 $s_escape = $this->count
;
3551 if ($this->match('\\\\', $m)) {
3552 $out = '\\' . $m[0];
3556 if ($this->matchEscapeCharacter($escapedout, true)) {
3557 if (strlen($escapedout) === 1) {
3558 if (!preg_match(",\w,", $escapedout)) {
3559 $out = '\\' . $escapedout;
3561 } elseif (! $keepEscapedNumber ||
! \
is_numeric($escapedout)) {
3566 $escape_sequence = rtrim(substr($this->buffer
, $s_escape, $this->count
- $s_escape));
3567 if (strlen($escape_sequence) < 6) {
3568 $escape_sequence .= ' ';
3570 $out = '\\' . strtolower($escape_sequence);
3573 if ($this->match('\\S', $m)) {
3574 $out = '\\' . $m[0];
3583 * Parse the parts that make up a selector
3586 * div[yes=no]#something.hello.world:nth-child(-2n+1)%placeholder
3590 * @param string|bool $subSelector
3594 protected function selectorSingle(&$out, $subSelector = false)
3596 $oldWhite = $this->eatWhiteDefault
;
3597 $this->eatWhiteDefault
= false;
3601 if ($this->matchChar('*', false)) {
3606 if (! isset($this->buffer
[$this->count
])) {
3611 $char = $this->buffer
[$this->count
];
3613 // see if we can stop early
3614 if ($char === '{' ||
$char === ',' ||
$char === ';' ||
$char === '}' ||
$char === '@') {
3618 // parsing a sub selector in () stop with the closing )
3619 if ($subSelector && $char === ')') {
3626 $parts[] = Compiler
::$selfSelector;
3628 ! $this->cssOnly ||
$this->assertPlainCssValid(false, $s);
3642 // handling of escaping in selectors : get the escaped char
3643 if ($char === '\\') {
3645 if ($this->matchEscapeCharacterInSelector($escaped, true)) {
3646 $parts[] = $escaped;
3652 if ($char === '%') {
3655 if ($this->placeholder($placeholder)) {
3657 $parts[] = $placeholder;
3658 ! $this->cssOnly ||
$this->assertPlainCssValid(false, $s);
3665 if ($char === '#') {
3666 if ($this->interpolation($inter)) {
3668 ! $this->cssOnly ||
$this->assertPlainCssValid(false, $s);
3677 // a pseudo selector
3678 if ($char === ':') {
3679 if ($this->buffer
[$this->count +
1] === ':') {
3687 if ($this->mixedKeyword($nameParts, true)) {
3690 foreach ($nameParts as $sub) {
3697 $nameParts === ['not'] ||
3698 $nameParts === ['is'] ||
3699 $nameParts === ['has'] ||
3700 $nameParts === ['where'] ||
3701 $nameParts === ['slotted'] ||
3702 $nameParts === ['nth-child'] ||
3703 $nameParts === ['nth-last-child'] ||
3704 $nameParts === ['nth-of-type'] ||
3705 $nameParts === ['nth-last-of-type']
3708 $this->matchChar('(', true) &&
3709 ($this->selectors($subs, reset($nameParts)) ||
true) &&
3710 $this->matchChar(')')
3714 while ($sub = array_shift($subs)) {
3715 while ($ps = array_shift($sub)) {
3716 foreach ($ps as &$p) {
3720 if (\
count($sub) && reset($sub)) {
3725 if (\
count($subs) && reset($subs)) {
3735 $this->matchChar('(', true) &&
3736 ($this->openString(')', $str, '(') ||
true) &&
3737 $this->matchChar(')')
3741 if (! empty($str)) {
3757 if ($subSelector && \
is_string($subSelector) && strpos($subSelector, 'nth-') === 0) {
3758 if ($this->match("(\s*(\+\s*|\-\s*)?(\d+|n|\d+n))+", $counter)) {
3759 $parts[] = $counter[0];
3760 //$parts[] = str_replace(' ', '', $counter[0]);
3767 // attribute selector
3770 $this->matchChar('[') &&
3771 ($this->openString(']', $str, '[') ||
true) &&
3772 $this->matchChar(']')
3776 if (! empty($str)) {
3787 if ($this->unit($unit)) {
3792 if ($this->restrictedKeyword($name, false, true)) {
3800 $this->eatWhiteDefault
= $oldWhite;
3818 protected function variable(&$out)
3823 $this->matchChar('$', false) &&
3824 $this->keyword($name)
3826 if ($this->allowVars
) {
3827 $out = [Type
::T_VARIABLE
, $name];
3829 $out = [Type
::T_KEYWORD
, '$' . $name];
3843 * @param string $word
3844 * @param bool $eatWhitespace
3845 * @param bool $inSelector
3849 protected function keyword(&$word, $eatWhitespace = null, $inSelector = false)
3852 $match = $this->match(
3854 ?
'(([\pL\w\x{00A0}-\x{10FFFF}_\-\*!"\']|\\\\[a-f0-9]{6} ?|\\\\[a-f0-9]{1,5}(?![a-f0-9]) ?|[\\\\].)([\pL\w\x{00A0}-\x{10FFFF}\-_"\']|\\\\[a-f0-9]{6} ?|\\\\[a-f0-9]{1,5}(?![a-f0-9]) ?|[\\\\].)*)'
3855 : '(([\w_\-\*!"\']|\\\\[a-f0-9]{6} ?|\\\\[a-f0-9]{1,5}(?![a-f0-9]) ?|[\\\\].)([\w\-_"\']|\\\\[a-f0-9]{6} ?|\\\\[a-f0-9]{1,5}(?![a-f0-9]) ?|[\\\\].)*)',
3863 // handling of escaping in keyword : get the escaped char
3864 if (strpos($word, '\\') !== false) {
3865 $send = $this->count
;
3868 $previousEscape = false;
3869 while ($this->count
< $send) {
3870 $char = $this->buffer
[$this->count
];
3873 $this->count
< $send
3878 $this->matchEscapeCharacterInSelector($out)
3880 $this->matchEscapeCharacter($out, true)
3883 $escapedWord[] = $out;
3885 if ($previousEscape) {
3886 $previousEscape = false;
3887 } elseif ($char === '\\') {
3888 $previousEscape = true;
3890 $escapedWord[] = $char;
3894 $word = implode('', $escapedWord);
3897 if (is_null($eatWhitespace) ?
$this->eatWhiteDefault
: $eatWhitespace) {
3898 $this->whitespace();
3908 * Parse a keyword that should not start with a number
3910 * @param string $word
3911 * @param bool $eatWhitespace
3912 * @param bool $inSelector
3916 protected function restrictedKeyword(&$word, $eatWhitespace = null, $inSelector = false)
3920 if ($this->keyword($word, $eatWhitespace, $inSelector) && (\
ord($word[0]) > 57 || \
ord($word[0]) < 48)) {
3930 * Parse a placeholder
3932 * @param string|array $placeholder
3936 protected function placeholder(&$placeholder)
3938 $match = $this->match(
3946 $placeholder = $m[1];
3951 if ($this->interpolation($placeholder)) {
3965 protected function url(&$out)
3967 if ($this->literal('url(', 4)) {
3971 ($this->string($out) ||
$this->spaceList($out)) &&
3972 $this->matchChar(')')
3974 $out = [Type
::T_STRING
, '', ['url(', $out, ')']];
3982 $this->openString(')', $out) &&
3983 $this->matchChar(')')
3985 $out = [Type
::T_STRING
, '', ['url(', $out, ')']];
3995 * Consume an end of statement delimiter
3996 * @param bool $eatWhitespace
4000 protected function end($eatWhitespace = null)
4002 if ($this->matchChar(';', $eatWhitespace)) {
4006 if ($this->count
=== \
strlen($this->buffer
) ||
$this->buffer
[$this->count
] === '}') {
4007 // if there is end of file or a closing block next then we don't need a ;
4015 * Strip assignment flag from the list
4017 * @param array $value
4021 protected function stripAssignmentFlags(&$value)
4025 for ($token = &$value; $token[0] === Type
::T_LIST
&& ($s = \
count($token[2])); $token = &$lastNode) {
4026 $lastNode = &$token[2][$s - 1];
4028 while ($lastNode[0] === Type
::T_KEYWORD
&& \
in_array($lastNode[1], ['!default', '!global'])) {
4029 array_pop($token[2]);
4031 $node = end($token[2]);
4032 $token = $this->flattenList($token);
4033 $flags[] = $lastNode[1];
4042 * Strip optional flag from selector list
4044 * @param array $selectors
4048 protected function stripOptionalFlag(&$selectors)
4051 $selector = end($selectors);
4052 $part = end($selector);
4054 if ($part === ['!optional']) {
4055 array_pop($selectors[\
count($selectors) - 1]);
4064 * Turn list of length 1 into value type
4066 * @param array $value
4070 protected function flattenList($value)
4072 if ($value[0] === Type
::T_LIST
&& \
count($value[2]) === 1) {
4073 return $this->flattenList($value[2][0]);
4080 * Quote regular expression
4082 * @param string $what
4086 private function pregQuote($what)
4088 return preg_quote($what, '/');
4092 * Extract line numbers from buffer
4094 * @param string $buffer
4096 private function extractLineNumbers($buffer)
4098 $this->sourcePositions
= [0 => 0];
4101 while (($pos = strpos($buffer, "\n", $prev)) !== false) {
4102 $this->sourcePositions
[] = $pos;
4106 $this->sourcePositions
[] = \
strlen($buffer);
4108 if (substr($buffer, -1) !== "\n") {
4109 $this->sourcePositions
[] = \
strlen($buffer) +
1;
4114 * Get source line number and column (given character position in the buffer)
4120 private function getSourcePosition($pos)
4123 $high = \
count($this->sourcePositions
);
4125 while ($low < $high) {
4126 $mid = (int) (($high +
$low) / 2);
4128 if ($pos < $this->sourcePositions
[$mid]) {
4133 if ($pos >= $this->sourcePositions
[$mid +
1]) {
4138 return [$mid +
1, $pos - $this->sourcePositions
[$mid]];
4141 return [$low +
1, $pos - $this->sourcePositions
[$low]];
4145 * Save internal encoding of mbstring
4147 * When mbstring.func_overload is used to replace the standard PHP string functions,
4148 * this method configures the internal encoding to a single-byte one so that the
4149 * behavior matches the normal behavior of PHP string functions while using the parser.
4150 * The existing internal encoding is saved and will be restored when calling {@see restoreEncoding}.
4152 * If mbstring.func_overload is not used (or does not override string functions), this method is a no-op.
4156 private function saveEncoding()
4158 if (\PHP_VERSION_ID
< 80000 && \
extension_loaded('mbstring') && (2 & (int) ini_get('mbstring.func_overload')) > 0) {
4159 $this->encoding
= mb_internal_encoding();
4161 mb_internal_encoding('iso-8859-1');
4166 * Restore internal encoding
4170 private function restoreEncoding()
4172 if (\
extension_loaded('mbstring') && $this->encoding
) {
4173 mb_internal_encoding($this->encoding
);