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\Exception\ParserException
;
20 * @author Leaf Corcoran <leafot@gmail.com>
24 const SOURCE_INDEX
= -1;
25 const SOURCE_LINE
= -2;
26 const SOURCE_COLUMN
= -3;
29 * @var array<string, int>
31 protected static $precedence = [
51 protected static $commentPattern;
55 protected static $operatorPattern;
59 protected static $whitePattern;
69 * @var array<int, int>
71 private $sourcePositions;
77 * The current offset in the buffer
93 private $eatWhiteDefault;
97 private $discardComments;
108 private $patternModifiers;
109 private $commentsSeen;
118 * @param string $sourceName
119 * @param integer $sourceIndex
120 * @param string|null $encoding
121 * @param Cache|null $cache
122 * @param bool $cssOnly
124 public function __construct($sourceName, $sourceIndex = 0, $encoding = 'utf-8', Cache
$cache = null, $cssOnly = false)
126 $this->sourceName
= $sourceName ?
: '(stdin)';
127 $this->sourceIndex
= $sourceIndex;
128 $this->charset
= null;
129 $this->utf8
= ! $encoding ||
strtolower($encoding) === 'utf-8';
130 $this->patternModifiers
= $this->utf8 ?
'Aisu' : 'Ais';
131 $this->commentsSeen
= [];
132 $this->commentsSeen
= [];
133 $this->allowVars
= true;
134 $this->cssOnly
= $cssOnly;
136 if (empty(static::$operatorPattern)) {
137 static::$operatorPattern = '([*\/%+-]|[!=]\=|\>\=?|\<\=?|and|or)';
139 $commentSingle = '\/\/';
140 $commentMultiLeft = '\/\*';
141 $commentMultiRight = '\*\/';
143 static::$commentPattern = $commentMultiLeft . '.*?' . $commentMultiRight;
144 static::$whitePattern = $this->utf8
145 ?
'/' . $commentSingle . '[^\n]*\s*|(' . static::$commentPattern . ')\s*|\s+/AisuS'
146 : '/' . $commentSingle . '[^\n]*\s*|(' . static::$commentPattern . ')\s*|\s+/AisS';
149 $this->cache
= $cache;
153 * Get source file name
159 public function getSourceName()
161 return $this->sourceName
;
171 * @throws ParserException
173 * @deprecated use "parseError" and throw the exception in the caller instead.
175 public function throwParseError($msg = 'parse error')
178 'The method "throwParseError" is deprecated. Use "parseError" and throw the exception in the caller instead',
182 throw $this->parseError($msg);
186 * Creates a parser error
192 * @return ParserException
194 public function parseError($msg = 'parse error')
196 list($line, $column) = $this->getSourcePosition($this->count
);
198 $loc = empty($this->sourceName
)
199 ?
"line: $line, column: $column"
200 : "$this->sourceName on line $line, at column $column";
202 if ($this->peek('(.*?)(\n|$)', $m, $this->count
)) {
203 $this->restoreEncoding();
205 $e = new ParserException("$msg: failed at `$m[1]` $loc");
206 $e->setSourcePosition([$this->sourceName
, $line, $column]);
211 $this->restoreEncoding();
213 $e = new ParserException("$msg: $loc");
214 $e->setSourcePosition([$this->sourceName
, $line, $column]);
224 * @param string $buffer
228 public function parse($buffer)
231 $cacheKey = $this->sourceName
. ':' . md5($buffer);
233 'charset' => $this->charset
,
234 'utf8' => $this->utf8
,
236 $v = $this->cache
->getCache('parse', $cacheKey, $parseOptions);
238 if (! \
is_null($v)) {
243 // strip BOM (byte order marker)
244 if (substr($buffer, 0, 3) === "\xef\xbb\xbf") {
245 $buffer = substr($buffer, 3);
248 $this->buffer
= rtrim($buffer, "\x00..\x1f");
251 $this->inParens
= false;
252 $this->eatWhiteDefault
= true;
254 $this->saveEncoding();
255 $this->extractLineNumbers($buffer);
257 $this->pushBlock(null); // root block
259 $this->pushBlock(null);
262 while ($this->parseChunk()) {
266 if ($this->count
!== \
strlen($this->buffer
)) {
267 throw $this->parseError();
270 if (! empty($this->env
->parent
)) {
271 throw $this->parseError('unclosed block');
274 if ($this->charset
) {
275 array_unshift($this->env
->children
, $this->charset
);
278 $this->restoreEncoding();
281 $this->cache
->setCache('parse', $cacheKey, $this->env
, $parseOptions);
288 * Parse a value or value list
292 * @param string $buffer
293 * @param string|array $out
297 public function parseValue($buffer, &$out)
301 $this->inParens
= false;
302 $this->eatWhiteDefault
= true;
303 $this->buffer
= (string) $buffer;
305 $this->saveEncoding();
306 $this->extractLineNumbers($this->buffer
);
308 $list = $this->valueList($out);
310 $this->restoreEncoding();
316 * Parse a selector or selector list
320 * @param string $buffer
321 * @param string|array $out
322 * @param bool $shouldValidate
326 public function parseSelector($buffer, &$out, $shouldValidate = true)
330 $this->inParens
= false;
331 $this->eatWhiteDefault
= true;
332 $this->buffer
= (string) $buffer;
334 $this->saveEncoding();
335 $this->extractLineNumbers($this->buffer
);
337 // discard space/comments at the start
338 $this->discardComments
= true;
340 $this->discardComments
= false;
342 $selector = $this->selectors($out);
344 $this->restoreEncoding();
346 if ($shouldValidate && $this->count
!== strlen($buffer)) {
347 throw $this->parseError("`" . substr($buffer, $this->count
) . "` is not a valid Selector in `$buffer`");
354 * Parse a media Query
358 * @param string $buffer
359 * @param string|array $out
363 public function parseMediaQueryList($buffer, &$out)
367 $this->inParens
= false;
368 $this->eatWhiteDefault
= true;
369 $this->buffer
= (string) $buffer;
371 $this->saveEncoding();
372 $this->extractLineNumbers($this->buffer
);
374 $isMediaQuery = $this->mediaQueryList($out);
376 $this->restoreEncoding();
378 return $isMediaQuery;
382 * Parse a single chunk off the head of the buffer and append it to the
383 * current parse environment.
385 * Returns false when the buffer is empty, or when there is an error.
387 * This function is called repeatedly until the entire document is
390 * This parser is most similar to a recursive descent parser. Single
391 * functions represent discrete grammatical rules for the language, and
392 * they are able to capture the text that represents those rules.
394 * Consider the function Compiler::keyword(). (All parse functions are
395 * structured the same.)
397 * The function takes a single reference argument. When calling the
398 * function it will attempt to match a keyword on the head of the buffer.
399 * If it is successful, it will place the keyword in the referenced
400 * argument, advance the position in the buffer, and return true. If it
401 * fails then it won't advance the buffer and it will return false.
403 * All of these parse functions are powered by Compiler::match(), which behaves
404 * the same way, but takes a literal regular expression. Sometimes it is
405 * more convenient to use match instead of creating a new function.
407 * Because of the format of the functions, to parse an entire string of
408 * grammatical rules, you can chain them together using &&.
410 * But, if some of the rules in the chain succeed before one fails, then
411 * the buffer position will be left at an invalid state. In order to
412 * avoid this, Compiler::seek() is used to remember and set buffer positions.
414 * Before parsing a chain, use $s = $this->count to remember the current
415 * position into $s. Then if a chain fails, use $this->seek($s) to
416 * go back where we started.
420 protected function parseChunk()
425 if (isset($this->buffer
[$this->count
]) && $this->buffer
[$this->count
] === '@') {
427 $this->literal('@at-root', 8) &&
428 ($this->selectors($selector) ||
true) &&
429 ($this->map($with) ||
true) &&
430 (($this->matchChar('(') &&
431 $this->interpolation($with) &&
432 $this->matchChar(')')) ||
true) &&
433 $this->matchChar('{', false)
435 ! $this->cssOnly ||
$this->assertPlainCssValid(false, $s);
437 $atRoot = $this->pushSpecialBlock(Type
::T_AT_ROOT
, $s);
438 $atRoot->selector
= $selector;
439 $atRoot->with
= $with;
447 $this->literal('@media', 6) &&
448 $this->mediaQueryList($mediaQueryList) &&
449 $this->matchChar('{', false)
451 $media = $this->pushSpecialBlock(Type
::T_MEDIA
, $s);
452 $media->queryList
= $mediaQueryList[2];
460 $this->literal('@mixin', 6) &&
461 $this->keyword($mixinName) &&
462 ($this->argumentDef($args) ||
true) &&
463 $this->matchChar('{', false)
465 ! $this->cssOnly ||
$this->assertPlainCssValid(false, $s);
467 $mixin = $this->pushSpecialBlock(Type
::T_MIXIN
, $s);
468 $mixin->name
= $mixinName;
469 $mixin->args
= $args;
477 ($this->literal('@include', 8) &&
478 $this->keyword($mixinName) &&
479 ($this->matchChar('(') &&
480 ($this->argValues($argValues) ||
true) &&
481 $this->matchChar(')') ||
true) &&
483 ($this->literal('using', 5) &&
484 $this->argumentDef($argUsing) &&
485 ($this->end() ||
$this->matchChar('{') && $hasBlock = true)) ||
486 $this->matchChar('{') && $hasBlock = true)
488 ! $this->cssOnly ||
$this->assertPlainCssValid(false, $s);
493 isset($argValues) ?
$argValues : null,
495 isset($argUsing) ?
$argUsing : null
498 if (! empty($hasBlock)) {
499 $include = $this->pushSpecialBlock(Type
::T_INCLUDE
, $s);
500 $include->child
= $child;
502 $this->append($child, $s);
511 $this->literal('@scssphp-import-once', 20) &&
512 $this->valueList($importPath) &&
515 ! $this->cssOnly ||
$this->assertPlainCssValid(false, $s);
517 $this->append([Type
::T_SCSSPHP_IMPORT_ONCE
, $importPath], $s);
525 $this->literal('@import', 7) &&
526 $this->valueList($importPath) &&
527 $importPath[0] !== Type
::T_FUNCTION_CALL
&&
530 if ($this->cssOnly
) {
531 $this->assertPlainCssValid([Type
::T_IMPORT
, $importPath], $s);
532 $this->append([Type
::T_COMMENT
, rtrim(substr($this->buffer
, $s, $this->count
- $s))]);
536 $this->append([Type
::T_IMPORT
, $importPath], $s);
544 $this->literal('@import', 7) &&
545 $this->url($importPath) &&
548 if ($this->cssOnly
) {
549 $this->assertPlainCssValid([Type
::T_IMPORT
, $importPath], $s);
550 $this->append([Type
::T_COMMENT
, rtrim(substr($this->buffer
, $s, $this->count
- $s))]);
554 $this->append([Type
::T_IMPORT
, $importPath], $s);
562 $this->literal('@extend', 7) &&
563 $this->selectors($selectors) &&
566 ! $this->cssOnly ||
$this->assertPlainCssValid(false, $s);
569 $optional = $this->stripOptionalFlag($selectors);
570 $this->append([Type
::T_EXTEND
, $selectors, $optional], $s);
578 $this->literal('@function', 9) &&
579 $this->keyword($fnName) &&
580 $this->argumentDef($args) &&
581 $this->matchChar('{', false)
583 ! $this->cssOnly ||
$this->assertPlainCssValid(false, $s);
585 $func = $this->pushSpecialBlock(Type
::T_FUNCTION
, $s);
586 $func->name
= $fnName;
595 $this->literal('@return', 7) &&
596 ($this->valueList($retVal) ||
true) &&
599 ! $this->cssOnly ||
$this->assertPlainCssValid(false, $s);
601 $this->append([Type
::T_RETURN
, isset($retVal) ?
$retVal : [Type
::T_NULL
]], $s);
609 $this->literal('@each', 5) &&
610 $this->genericList($varNames, 'variable', ',', false) &&
611 $this->literal('in', 2) &&
612 $this->valueList($list) &&
613 $this->matchChar('{', false)
615 ! $this->cssOnly ||
$this->assertPlainCssValid(false, $s);
617 $each = $this->pushSpecialBlock(Type
::T_EACH
, $s);
619 foreach ($varNames[2] as $varName) {
620 $each->vars
[] = $varName[1];
631 $this->literal('@while', 6) &&
632 $this->expression($cond) &&
633 $this->matchChar('{', false)
635 ! $this->cssOnly ||
$this->assertPlainCssValid(false, $s);
638 $cond[0] === Type
::T_LIST
&&
639 ! empty($cond['enclosing']) &&
640 $cond['enclosing'] === 'parent' &&
641 \
count($cond[2]) == 1
643 $cond = reset($cond[2]);
646 $while = $this->pushSpecialBlock(Type
::T_WHILE
, $s);
647 $while->cond
= $cond;
655 $this->literal('@for', 4) &&
656 $this->variable($varName) &&
657 $this->literal('from', 4) &&
658 $this->expression($start) &&
659 ($this->literal('through', 7) ||
660 ($forUntil = true && $this->literal('to', 2))) &&
661 $this->expression($end) &&
662 $this->matchChar('{', false)
664 ! $this->cssOnly ||
$this->assertPlainCssValid(false, $s);
666 $for = $this->pushSpecialBlock(Type
::T_FOR
, $s);
667 $for->var = $varName[1];
668 $for->start
= $start;
670 $for->until
= isset($forUntil);
678 $this->literal('@if', 3) &&
679 $this->functionCallArgumentsList($cond, false, '{', false)
681 ! $this->cssOnly ||
$this->assertPlainCssValid(false, $s);
683 $if = $this->pushSpecialBlock(Type
::T_IF
, $s);
686 $cond[0] === Type
::T_LIST
&&
687 ! empty($cond['enclosing']) &&
688 $cond['enclosing'] === 'parent' &&
689 \
count($cond[2]) == 1
691 $cond = reset($cond[2]);
703 $this->literal('@debug', 6) &&
704 $this->functionCallArgumentsList($value, false)
706 ! $this->cssOnly ||
$this->assertPlainCssValid(false, $s);
708 $this->append([Type
::T_DEBUG
, $value], $s);
716 $this->literal('@warn', 5) &&
717 $this->functionCallArgumentsList($value, false)
719 ! $this->cssOnly ||
$this->assertPlainCssValid(false, $s);
721 $this->append([Type
::T_WARN
, $value], $s);
729 $this->literal('@error', 6) &&
730 $this->functionCallArgumentsList($value, false)
732 ! $this->cssOnly ||
$this->assertPlainCssValid(false, $s);
734 $this->append([Type
::T_ERROR
, $value], $s);
742 $this->literal('@content', 8) &&
744 $this->matchChar('(') &&
745 $this->argValues($argContent) &&
746 $this->matchChar(')') &&
749 ! $this->cssOnly ||
$this->assertPlainCssValid(false, $s);
751 $this->append([Type
::T_MIXIN_CONTENT
, isset($argContent) ?
$argContent : null], $s);
758 $last = $this->last();
760 if (isset($last) && $last[0] === Type
::T_IF
) {
763 if ($this->literal('@else', 5)) {
764 if ($this->matchChar('{', false)) {
765 $else = $this->pushSpecialBlock(Type
::T_ELSE
, $s);
767 $this->literal('if', 2) &&
768 $this->functionCallArgumentsList($cond, false, '{', false)
770 $else = $this->pushSpecialBlock(Type
::T_ELSEIF
, $s);
775 $else->dontAppend
= true;
776 $if->cases
[] = $else;
785 // only retain the first @charset directive encountered
787 $this->literal('@charset', 8) &&
788 $this->valueList($charset) &&
791 if (! isset($this->charset
)) {
792 $statement = [Type
::T_CHARSET
, $charset];
794 list($line, $column) = $this->getSourcePosition($s);
796 $statement[static::SOURCE_LINE
] = $line;
797 $statement[static::SOURCE_COLUMN
] = $column;
798 $statement[static::SOURCE_INDEX
] = $this->sourceIndex
;
800 $this->charset
= $statement;
809 $this->literal('@supports', 9) &&
810 ($t1 = $this->supportsQuery($supportQuery)) &&
811 ($t2 = $this->matchChar('{', false))
813 $directive = $this->pushSpecialBlock(Type
::T_DIRECTIVE
, $s);
814 $directive->name
= 'supports';
815 $directive->value
= $supportQuery;
822 // doesn't match built in directive, do generic one
824 $this->matchChar('@', false) &&
825 $this->mixedKeyword($dirName) &&
826 $this->directiveValue($dirValue, '{')
828 if (count($dirName) === 1 && is_string(reset($dirName))) {
829 $dirName = reset($dirName);
831 $dirName = [Type
::T_STRING
, '', $dirName];
833 if ($dirName === 'media') {
834 $directive = $this->pushSpecialBlock(Type
::T_MEDIA
, $s);
836 $directive = $this->pushSpecialBlock(Type
::T_DIRECTIVE
, $s);
837 $directive->name
= $dirName;
840 if (isset($dirValue)) {
841 ! $this->cssOnly ||
($dirValue = $this->assertPlainCssValid($dirValue));
842 $directive->value
= $dirValue;
850 // maybe it's a generic blockless directive
852 $this->matchChar('@', false) &&
853 $this->mixedKeyword($dirName) &&
854 ! $this->isKnownGenericDirective($dirName) &&
855 ($this->end(false) ||
($this->directiveValue($dirValue, '') && $this->end(false)))
857 if (\
count($dirName) === 1 && \
is_string(\reset
($dirName))) {
858 $dirName = \reset
($dirName);
860 $dirName = [Type
::T_STRING
, '', $dirName];
863 ! empty($this->env
->parent
) &&
865 ! \
in_array($this->env
->type
, [Type
::T_DIRECTIVE
, Type
::T_MEDIA
])
867 $plain = \trim
(\
substr($this->buffer
, $s, $this->count
- $s));
868 throw $this->parseError(
869 "Unknown directive `{$plain}` not allowed in `" . $this->env
->type
. "` block"
872 // blockless directives with a blank line after keeps their blank lines after
873 // sass-spec compliance purpose
875 $hasBlankLine = false;
876 if ($this->match('\s*?\n\s*\n', $out, false)) {
877 $hasBlankLine = true;
880 $isNotRoot = ! empty($this->env
->parent
);
881 $this->append([Type
::T_DIRECTIVE
, [$dirName, $dirValue, $hasBlankLine, $isNotRoot]], $s);
892 $inCssSelector = null;
893 if ($this->cssOnly
) {
894 $inCssSelector = (! empty($this->env
->parent
) &&
895 ! in_array($this->env
->type
, [Type
::T_DIRECTIVE
, Type
::T_MEDIA
]));
897 // custom properties : right part is static
898 if (($this->customProperty($name) ) && $this->matchChar(':', false)) {
899 $start = $this->count
;
901 // but can be complex and finish with ; or }
902 foreach ([';','}'] as $ending) {
904 $this->openString($ending, $stringValue, '(', ')', false) &&
908 $value = $stringValue;
910 // check if we have only a partial value due to nested [] or { } to take in account
911 $nestingPairs = [['[', ']'], ['{', '}']];
913 foreach ($nestingPairs as $nestingPair) {
914 $p = strpos($this->buffer
, $nestingPair[0], $start);
916 if ($p && $p < $end) {
920 $this->openString($ending, $stringValue, $nestingPair[0], $nestingPair[1], false) &&
925 $value = $stringValue;
931 $this->append([Type
::T_CUSTOM_PROPERTY
, $name, $value], $s);
937 // TODO: output an error here if nothing found according to sass spec
943 // captures most properties before having to parse a selector
945 $this->keyword($name, false) &&
946 $this->literal(': ', 2) &&
947 $this->valueList($value) &&
950 $name = [Type
::T_STRING
, '', [$name]];
951 $this->append([Type
::T_ASSIGN
, $name, $value], $s);
960 $this->variable($name) &&
961 $this->matchChar(':') &&
962 $this->valueList($value) &&
965 ! $this->cssOnly ||
$this->assertPlainCssValid(false, $s);
968 $assignmentFlags = $this->stripAssignmentFlags($value);
969 $this->append([Type
::T_ASSIGN
, $name, $value, $assignmentFlags], $s);
977 if ($this->literal('-->', 3)) {
983 $this->selectors($selectors) &&
984 $this->matchChar('{', false)
986 ! $this->cssOnly ||
! $inCssSelector ||
$this->assertPlainCssValid(false);
988 $this->pushBlock($selectors, $s);
990 if ($this->eatWhiteDefault
) {
992 $this->append(null); // collect comments at the beginning if needed
1000 // property assign, or nested assign
1002 $this->propertyName($name) &&
1003 $this->matchChar(':')
1005 $foundSomething = false;
1007 if ($this->valueList($value)) {
1008 if (empty($this->env
->parent
)) {
1009 throw $this->parseError('expected "{"');
1012 $this->append([Type
::T_ASSIGN
, $name, $value], $s);
1013 $foundSomething = true;
1016 if ($this->matchChar('{', false)) {
1017 ! $this->cssOnly ||
$this->assertPlainCssValid(false);
1019 $propBlock = $this->pushSpecialBlock(Type
::T_NESTED_PROPERTY
, $s);
1020 $propBlock->prefix
= $name;
1021 $propBlock->hasValue
= $foundSomething;
1023 $foundSomething = true;
1024 } elseif ($foundSomething) {
1025 $foundSomething = $this->end();
1028 if ($foundSomething) {
1036 if ($this->matchChar('}', false)) {
1037 $block = $this->popBlock();
1039 if (! isset($block->type
) ||
$block->type
!== Type
::T_IF
) {
1040 if ($this->env
->parent
) {
1041 $this->append(null); // collect comments before next statement if needed
1045 if (isset($block->type
) && $block->type
=== Type
::T_INCLUDE
) {
1046 $include = $block->child
;
1047 unset($block->child
);
1048 $include[3] = $block;
1049 $this->append($include, $s);
1050 } elseif (empty($block->dontAppend
)) {
1051 $type = isset($block->type
) ?
$block->type
: Type
::T_BLOCK
;
1052 $this->append([$type, $block], $s);
1055 // collect comments just after the block closing if needed
1056 if ($this->eatWhiteDefault
) {
1057 $this->whitespace();
1059 if ($this->env
->comments
) {
1060 $this->append(null);
1069 $this->matchChar(';') ||
1070 $this->literal('<!--', 4)
1079 * Push block onto parse tree
1081 * @param array|null $selectors
1082 * @param integer $pos
1086 protected function pushBlock($selectors, $pos = 0)
1088 list($line, $column) = $this->getSourcePosition($pos);
1091 $b->sourceName
= $this->sourceName
;
1092 $b->sourceLine
= $line;
1093 $b->sourceColumn
= $column;
1094 $b->sourceIndex
= $this->sourceIndex
;
1095 $b->selectors
= $selectors;
1097 $b->parent
= $this->env
;
1101 } elseif (empty($this->env
->children
)) {
1102 $this->env
->children
= $this->env
->comments
;
1104 $this->env
->comments
= [];
1106 $b->children
= $this->env
->comments
;
1107 $this->env
->comments
= [];
1112 // collect comments at the beginning of a block if needed
1113 if ($this->eatWhiteDefault
) {
1114 $this->whitespace();
1116 if ($this->env
->comments
) {
1117 $this->append(null);
1125 * Push special (named) block onto parse tree
1127 * @param string $type
1128 * @param integer $pos
1132 protected function pushSpecialBlock($type, $pos)
1134 $block = $this->pushBlock(null, $pos);
1135 $block->type
= $type;
1141 * Pop scope and return last block
1145 * @throws \Exception
1147 protected function popBlock()
1150 // collect comments ending just before of a block closing
1151 if ($this->env
->comments
) {
1152 $this->append(null);
1156 $block = $this->env
;
1158 if (empty($block->parent
)) {
1159 throw $this->parseError('unexpected }');
1162 if ($block->type
== Type
::T_AT_ROOT
) {
1163 // keeps the parent in case of self selector &
1164 $block->selfParent
= $block->parent
;
1167 $this->env
= $block->parent
;
1169 unset($block->parent
);
1177 * @param string $regex
1179 * @param integer $from
1183 protected function peek($regex, &$out, $from = null)
1185 if (! isset($from)) {
1186 $from = $this->count
;
1189 $r = '/' . $regex . '/' . $this->patternModifiers
;
1190 $result = preg_match($r, $this->buffer
, $out, null, $from);
1196 * Seek to position in input stream (or return current position in input stream)
1198 * @param integer $where
1200 protected function seek($where)
1202 $this->count
= $where;
1206 * Assert a parsed part is plain CSS Valid
1208 * @param array|false $parsed
1209 * @param int $startPos
1210 * @throws ParserException
1212 protected function assertPlainCssValid($parsed, $startPos = null)
1217 $parsed = $this->isPlainCssValidElement($parsed);
1220 if (! \
is_null($startPos)) {
1221 $plain = rtrim(substr($this->buffer
, $startPos, $this->count
- $startPos));
1222 $message = "Error : `{$plain}` isn't allowed in plain CSS";
1224 $message = 'Error: SCSS syntax not allowed in CSS file';
1227 $message .= " ($type)";
1229 throw $this->parseError($message);
1236 * Check a parsed element is plain CSS Valid
1237 * @param array $parsed
1238 * @return bool|array
1240 protected function isPlainCssValidElement($parsed, $allowExpression = false)
1242 // keep string as is
1243 if (is_string($parsed)) {
1248 \
in_array($parsed[0], [Type
::T_FUNCTION
, Type
::T_FUNCTION_CALL
]) &&
1249 !\
in_array($parsed[1], [
1263 'repeating-linear-gradient',
1264 'repeating-radial-gradient',
1271 Compiler
::isNativeFunction($parsed[1])
1276 switch ($parsed[0]) {
1278 case Type
::T_KEYWORD
:
1280 case Type
::T_NUMBER
:
1284 case Type
::T_COMMENT
:
1285 if (isset($parsed[2])) {
1290 case Type
::T_DIRECTIVE
:
1291 if (\
is_array($parsed[1])) {
1292 $parsed[1][1] = $this->isPlainCssValidElement($parsed[1][1]);
1293 if (! $parsed[1][1]) {
1300 case Type
::T_IMPORT
:
1301 if ($parsed[1][0] === Type
::T_LIST
) {
1304 $parsed[1] = $this->isPlainCssValidElement($parsed[1]);
1305 if ($parsed[1] === false) {
1310 case Type
::T_STRING
:
1311 foreach ($parsed[2] as $k => $substr) {
1312 if (\
is_array($substr)) {
1313 $parsed[2][$k] = $this->isPlainCssValidElement($substr);
1314 if (! $parsed[2][$k]) {
1322 if (!empty($parsed['enclosing'])) {
1325 foreach ($parsed[2] as $k => $listElement) {
1326 $parsed[2][$k] = $this->isPlainCssValidElement($listElement);
1327 if (! $parsed[2][$k]) {
1333 case Type
::T_ASSIGN
:
1334 foreach ([1, 2, 3] as $k) {
1335 if (! empty($parsed[$k])) {
1336 $parsed[$k] = $this->isPlainCssValidElement($parsed[$k]);
1337 if (! $parsed[$k]) {
1344 case Type
::T_EXPRESSION
:
1345 list( ,$op, $lhs, $rhs, $inParens, $whiteBefore, $whiteAfter) = $parsed;
1346 if (! $allowExpression && ! \
in_array($op, ['and', 'or', '/'])) {
1349 $lhs = $this->isPlainCssValidElement($lhs, true);
1353 $rhs = $this->isPlainCssValidElement($rhs, true);
1361 $this->inParens ?
'(' : '',
1363 ($whiteBefore ?
' ' : '') . $op . ($whiteAfter ?
' ' : ''),
1365 $this->inParens ?
')' : ''
1369 case Type
::T_CUSTOM_PROPERTY
:
1371 $parsed[2] = $this->isPlainCssValidElement($parsed[2]);
1377 case Type
::T_FUNCTION
:
1378 $argsList = $parsed[2];
1379 foreach ($argsList[2] as $argElement) {
1380 if (! $this->isPlainCssValidElement($argElement)) {
1386 case Type
::T_FUNCTION_CALL
:
1387 $parsed[0] = Type
::T_FUNCTION
;
1388 $argsList = [Type
::T_LIST
, ',', []];
1389 foreach ($parsed[2] as $arg) {
1390 if ($arg[0] ||
! empty($arg[2])) {
1391 // no named arguments possible in a css function call
1395 $arg = $this->isPlainCssValidElement($arg[1], $parsed[1] === 'calc');
1399 $argsList[2][] = $arg;
1401 $parsed[2] = $argsList;
1409 * Match string looking for either ending delim, escape, or string interpolation
1411 * {@internal This is a workaround for preg_match's 250K string match limit. }}
1413 * @param array $m Matches (passed by reference)
1414 * @param string $delim Delimiter
1416 * @return boolean True if match; false otherwise
1418 protected function matchString(&$m, $delim)
1422 $end = \
strlen($this->buffer
);
1424 // look for either ending delim, escape, or string interpolation
1425 foreach (['#{', '\\', "\r", $delim] as $lookahead) {
1426 $pos = strpos($this->buffer
, $lookahead, $this->count
);
1428 if ($pos !== false && $pos < $end) {
1430 $token = $lookahead;
1434 if (! isset($token)) {
1438 $match = substr($this->buffer
, $this->count
, $end - $this->count
);
1444 $this->count
= $end + \
strlen($token);
1450 * Try to match something on head of buffer
1452 * @param string $regex
1454 * @param boolean $eatWhitespace
1458 protected function match($regex, &$out, $eatWhitespace = null)
1460 $r = '/' . $regex . '/' . $this->patternModifiers
;
1462 if (! preg_match($r, $this->buffer
, $out, null, $this->count
)) {
1466 $this->count +
= \
strlen($out[0]);
1468 if (! isset($eatWhitespace)) {
1469 $eatWhitespace = $this->eatWhiteDefault
;
1472 if ($eatWhitespace) {
1473 $this->whitespace();
1480 * Match a single string
1482 * @param string $char
1483 * @param boolean $eatWhitespace
1487 protected function matchChar($char, $eatWhitespace = null)
1489 if (! isset($this->buffer
[$this->count
]) ||
$this->buffer
[$this->count
] !== $char) {
1495 if (! isset($eatWhitespace)) {
1496 $eatWhitespace = $this->eatWhiteDefault
;
1499 if ($eatWhitespace) {
1500 $this->whitespace();
1507 * Match literal string
1509 * @param string $what
1510 * @param integer $len
1511 * @param boolean $eatWhitespace
1515 protected function literal($what, $len, $eatWhitespace = null)
1517 if (strcasecmp(substr($this->buffer
, $this->count
, $len), $what) !== 0) {
1521 $this->count +
= $len;
1523 if (! isset($eatWhitespace)) {
1524 $eatWhitespace = $this->eatWhiteDefault
;
1527 if ($eatWhitespace) {
1528 $this->whitespace();
1535 * Match some whitespace
1539 protected function whitespace()
1543 while (preg_match(static::$whitePattern, $this->buffer
, $m, null, $this->count
)) {
1544 if (isset($m[1]) && empty($this->commentsSeen
[$this->count
])) {
1545 // comment that are kept in the output CSS
1547 $startCommentCount = $this->count
;
1548 $endCommentCount = $this->count + \
strlen($m[1]);
1550 // find interpolations in comment
1551 $p = strpos($this->buffer
, '#{', $this->count
);
1553 while ($p !== false && $p < $endCommentCount) {
1554 $c = substr($this->buffer
, $this->count
, $p - $this->count
);
1559 if ($this->interpolation($out)) {
1560 // keep right spaces in the following string part
1562 while ($this->buffer
[$this->count
- 1] !== '}') {
1569 $comment[] = [Type
::T_COMMENT
, substr($this->buffer
, $p, $this->count
- $p), $out];
1571 $comment[] = substr($this->buffer
, $this->count
, 2);
1576 $p = strpos($this->buffer
, '#{', $this->count
);
1580 $c = substr($this->buffer
, $this->count
, $endCommentCount - $this->count
);
1583 // single part static comment
1584 $this->appendComment([Type
::T_COMMENT
, $c]);
1587 $staticComment = substr($this->buffer
, $startCommentCount, $endCommentCount - $startCommentCount);
1588 $this->appendComment([Type
::T_COMMENT
, $staticComment, [Type
::T_STRING
, '', $comment]]);
1591 $this->commentsSeen
[$startCommentCount] = true;
1592 $this->count
= $endCommentCount;
1594 // comment that are ignored and not kept in the output css
1595 $this->count +
= \
strlen($m[0]);
1596 // silent comments are not allowed in plain CSS files
1598 ||
! \
strlen(trim($m[0]))
1599 ||
$this->assertPlainCssValid(false, $this->count
- \
strlen($m[0]));
1609 * Append comment to current block
1611 * @param array $comment
1613 protected function appendComment($comment)
1615 if (! $this->discardComments
) {
1616 $this->env
->comments
[] = $comment;
1621 * Append statement to current block
1623 * @param array|null $statement
1624 * @param integer $pos
1626 protected function append($statement, $pos = null)
1628 if (! \
is_null($statement)) {
1629 ! $this->cssOnly ||
($statement = $this->assertPlainCssValid($statement, $pos));
1631 if (! \
is_null($pos)) {
1632 list($line, $column) = $this->getSourcePosition($pos);
1634 $statement[static::SOURCE_LINE
] = $line;
1635 $statement[static::SOURCE_COLUMN
] = $column;
1636 $statement[static::SOURCE_INDEX
] = $this->sourceIndex
;
1639 $this->env
->children
[] = $statement;
1642 $comments = $this->env
->comments
;
1645 $this->env
->children
= array_merge($this->env
->children
, $comments);
1646 $this->env
->comments
= [];
1651 * Returns last child was appended
1653 * @return array|null
1655 protected function last()
1657 $i = \
count($this->env
->children
) - 1;
1659 if (isset($this->env
->children
[$i])) {
1660 return $this->env
->children
[$i];
1665 * Parse media query list
1671 protected function mediaQueryList(&$out)
1673 return $this->genericList($out, 'mediaQuery', ',', false);
1683 protected function mediaQuery(&$out)
1685 $expressions = null;
1689 ($this->literal('only', 4) && ($only = true) ||
1690 $this->literal('not', 3) && ($not = true) ||
true) &&
1691 $this->mixedKeyword($mediaType)
1693 $prop = [Type
::T_MEDIA_TYPE
];
1696 $prop[] = [Type
::T_KEYWORD
, 'only'];
1700 $prop[] = [Type
::T_KEYWORD
, 'not'];
1703 $media = [Type
::T_LIST
, '', []];
1705 foreach ((array) $mediaType as $type) {
1706 if (\
is_array($type)) {
1707 $media[2][] = $type;
1709 $media[2][] = [Type
::T_KEYWORD
, $type];
1717 if (empty($parts) ||
$this->literal('and', 3)) {
1718 $this->genericList($expressions, 'mediaExpression', 'and', false);
1720 if (\
is_array($expressions)) {
1721 $parts = array_merge($parts, $expressions[2]);
1731 * Parse supports query
1737 protected function supportsQuery(&$out)
1739 $expressions = null;
1747 ($this->literal('not', 3) && ($not = true) ||
true) &&
1748 $this->matchChar('(') &&
1749 ($this->expression($property)) &&
1750 $this->literal(': ', 2) &&
1751 $this->valueList($value) &&
1752 $this->matchChar(')')
1754 $support = [Type
::T_STRING
, '', [[Type
::T_KEYWORD
, ($not ?
'not ' : '') . '(']]];
1755 $support[2][] = $property;
1756 $support[2][] = [Type
::T_KEYWORD
, ': '];
1757 $support[2][] = $value;
1758 $support[2][] = [Type
::T_KEYWORD
, ')'];
1760 $parts[] = $support;
1767 $this->matchChar('(') &&
1768 $this->supportsQuery($subQuery) &&
1769 $this->matchChar(')')
1771 $parts[] = [Type
::T_STRING
, '', [[Type
::T_KEYWORD
, '('], $subQuery, [Type
::T_KEYWORD
, ')']]];
1778 $this->literal('not', 3) &&
1779 $this->supportsQuery($subQuery)
1781 $parts[] = [Type
::T_STRING
, '', [[Type
::T_KEYWORD
, 'not '], $subQuery]];
1788 $this->literal('selector(', 9) &&
1789 $this->selector($selector) &&
1790 $this->matchChar(')')
1792 $support = [Type
::T_STRING
, '', [[Type
::T_KEYWORD
, 'selector(']]];
1794 $selectorList = [Type
::T_LIST
, '', []];
1796 foreach ($selector as $sc) {
1797 $compound = [Type
::T_STRING
, '', []];
1799 foreach ($sc as $scp) {
1800 if (\
is_array($scp)) {
1801 $compound[2][] = $scp;
1803 $compound[2][] = [Type
::T_KEYWORD
, $scp];
1807 $selectorList[2][] = $compound;
1810 $support[2][] = $selectorList;
1811 $support[2][] = [Type
::T_KEYWORD
, ')'];
1812 $parts[] = $support;
1818 if ($this->variable($var) or $this->interpolation($var)) {
1826 $this->literal('and', 3) &&
1827 $this->genericList($expressions, 'supportsQuery', ' and', false)
1829 array_unshift($expressions[2], [Type
::T_STRING
, '', $parts]);
1831 $parts = [$expressions];
1838 $this->literal('or', 2) &&
1839 $this->genericList($expressions, 'supportsQuery', ' or', false)
1841 array_unshift($expressions[2], [Type
::T_STRING
, '', $parts]);
1843 $parts = [$expressions];
1849 if (\
count($parts)) {
1850 if ($this->eatWhiteDefault
) {
1851 $this->whitespace();
1854 $out = [Type
::T_STRING
, '', $parts];
1864 * Parse media expression
1870 protected function mediaExpression(&$out)
1876 $this->matchChar('(') &&
1877 $this->expression($feature) &&
1878 ($this->matchChar(':') &&
1879 $this->expression($value) ||
true) &&
1880 $this->matchChar(')')
1882 $out = [Type
::T_MEDIA_EXPRESSION
, $feature];
1897 * Parse argument values
1903 protected function argValues(&$out)
1905 $discardComments = $this->discardComments
;
1906 $this->discardComments
= true;
1908 if ($this->genericList($list, 'argValue', ',', false)) {
1911 $this->discardComments
= $discardComments;
1916 $this->discardComments
= $discardComments;
1922 * Parse argument value
1928 protected function argValue(&$out)
1934 if (! $this->variable($keyword) ||
! $this->matchChar(':')) {
1940 if ($this->genericList($value, 'expression', '', true)) {
1941 $out = [$keyword, $value, false];
1944 if ($this->literal('...', 3)) {
1957 * Check if a generic directive is known to be able to allow almost any syntax or not
1958 * @param mixed $directiveName
1961 protected function isKnownGenericDirective($directiveName)
1963 if (\
is_array($directiveName) && \
is_string(reset($directiveName))) {
1964 $directiveName = reset($directiveName);
1966 if (! \
is_string($directiveName)) {
1970 \
in_array($directiveName, [
1975 'scssphp-import-once',
2004 * Parse directive value list that considers $vars as keyword
2007 * @param boolean|string $endChar
2011 protected function directiveValue(&$out, $endChar = false)
2015 if ($this->variable($out)) {
2016 if ($endChar && $this->matchChar($endChar, false)) {
2020 if (! $endChar && $this->end()) {
2027 if (\
is_string($endChar) && $this->openString($endChar ?
$endChar : ';', $out, null, null, true, ";}{")) {
2028 if ($endChar && $this->matchChar($endChar, false)) {
2032 if (!$endChar && $this->end()) {
2040 $allowVars = $this->allowVars
;
2041 $this->allowVars
= false;
2043 $res = $this->genericList($out, 'spaceList', ',');
2044 $this->allowVars
= $allowVars;
2047 if ($endChar && $this->matchChar($endChar, false)) {
2051 if (! $endChar && $this->end()) {
2058 if ($endChar && $this->matchChar($endChar, false)) {
2066 * Parse comma separated value list
2072 protected function valueList(&$out)
2074 $discardComments = $this->discardComments
;
2075 $this->discardComments
= true;
2076 $res = $this->genericList($out, 'spaceList', ',');
2077 $this->discardComments
= $discardComments;
2083 * Parse a function call, where externals () are part of the call
2084 * and not of the value list
2087 * @param bool $mandatoryEnclos
2088 * @param null|string $charAfter
2089 * @param null|bool $eatWhiteSp
2092 protected function functionCallArgumentsList(&$out, $mandatoryEnclos = true, $charAfter = null, $eatWhiteSp = null)
2097 $this->matchChar('(') &&
2098 $this->valueList($out) &&
2099 $this->matchChar(')') &&
2100 ($charAfter ?
$this->matchChar($charAfter, $eatWhiteSp) : $this->end())
2105 if (! $mandatoryEnclos) {
2109 $this->valueList($out) &&
2110 ($charAfter ?
$this->matchChar($charAfter, $eatWhiteSp) : $this->end())
2122 * Parse space separated value list
2128 protected function spaceList(&$out)
2130 return $this->genericList($out, 'expression');
2134 * Parse generic list
2137 * @param string $parseItem The name of the method used to parse items
2138 * @param string $delim
2139 * @param boolean $flatten
2143 protected function genericList(&$out, $parseItem, $delim = '', $flatten = true)
2149 while ($this->$parseItem($value)) {
2150 $trailing_delim = false;
2154 if (! $this->literal($delim, \
strlen($delim))) {
2158 $trailing_delim = true;
2160 // if no delim watch that a keyword didn't eat the single/double quote
2161 // from the following starting string
2162 if ($value[0] === Type
::T_KEYWORD
) {
2165 $last_char = substr($word, -1);
2168 strlen($word) > 1 &&
2169 in_array($last_char, [ "'", '"']) &&
2170 substr($word, -2, 1) !== '\\'
2172 // if there is a non escaped opening quote in the keyword, this seems unlikely a mistake
2173 $word = str_replace('\\' . $last_char, '\\\\', $word);
2174 if (strpos($word, $last_char) < strlen($word) - 1) {
2178 $currentCount = $this->count
;
2180 // let's try to rewind to previous char and try a parse
2182 // in case the keyword also eat spaces
2183 while (substr($this->buffer
, $this->count
, 1) !== $last_char) {
2188 if ($this->$parseItem($nextValue)) {
2189 if ($nextValue[0] === Type
::T_KEYWORD
&& $nextValue[1] === $last_char) {
2190 // bad try, forget it
2191 $this->seek($currentCount);
2194 if ($nextValue[0] !== Type
::T_STRING
) {
2195 // bad try, forget it
2196 $this->seek($currentCount);
2200 // OK it was a good idea
2201 $value[1] = substr($value[1], 0, -1);
2204 $items[] = $nextValue;
2206 // bad try, forget it
2207 $this->seek($currentCount);
2221 if ($trailing_delim) {
2222 $items[] = [Type
::T_NULL
];
2225 if ($flatten && \
count($items) === 1) {
2228 $out = [Type
::T_LIST
, $delim, $items];
2238 * @param boolean $listOnly
2239 * @param boolean $lookForExp
2243 protected function expression(&$out, $listOnly = false, $lookForExp = true)
2246 $discard = $this->discardComments
;
2247 $this->discardComments
= true;
2248 $allowedTypes = ($listOnly ?
[Type
::T_LIST
] : [Type
::T_LIST
, Type
::T_MAP
]);
2250 if ($this->matchChar('(')) {
2251 if ($this->enclosedExpression($lhs, $s, ')', $allowedTypes)) {
2253 $out = $this->expHelper($lhs, 0);
2258 $this->discardComments
= $discard;
2266 if (\
in_array(Type
::T_LIST
, $allowedTypes) && $this->matchChar('[')) {
2267 if ($this->enclosedExpression($lhs, $s, ']', [Type
::T_LIST
])) {
2269 $out = $this->expHelper($lhs, 0);
2274 $this->discardComments
= $discard;
2282 if (! $listOnly && $this->value($lhs)) {
2284 $out = $this->expHelper($lhs, 0);
2289 $this->discardComments
= $discard;
2294 $this->discardComments
= $discard;
2300 * Parse expression specifically checking for lists in parenthesis or brackets
2304 * @param string $closingParen
2305 * @param array $allowedTypes
2309 protected function enclosedExpression(&$out, $s, $closingParen = ')', $allowedTypes = [Type
::T_LIST
, Type
::T_MAP
])
2311 if ($this->matchChar($closingParen) && \
in_array(Type
::T_LIST
, $allowedTypes)) {
2312 $out = [Type
::T_LIST
, '', []];
2314 switch ($closingParen) {
2316 $out['enclosing'] = 'parent'; // parenthesis list
2320 $out['enclosing'] = 'bracket'; // bracketed list
2328 $this->valueList($out) &&
2329 $this->matchChar($closingParen) && ! ($closingParen === ')' &&
2330 \
in_array($out[0], [Type
::T_EXPRESSION
, Type
::T_UNARY
])) &&
2331 \
in_array(Type
::T_LIST
, $allowedTypes)
2333 if ($out[0] !== Type
::T_LIST ||
! empty($out['enclosing'])) {
2334 $out = [Type
::T_LIST
, '', [$out]];
2337 switch ($closingParen) {
2339 $out['enclosing'] = 'parent'; // parenthesis list
2343 $out['enclosing'] = 'bracket'; // bracketed list
2352 if (\
in_array(Type
::T_MAP
, $allowedTypes) && $this->map($out)) {
2360 * Parse left-hand side of subexpression
2363 * @param integer $minP
2367 protected function expHelper($lhs, $minP)
2369 $operators = static::$operatorPattern;
2372 $whiteBefore = isset($this->buffer
[$this->count
- 1]) &&
2373 ctype_space($this->buffer
[$this->count
- 1]);
2375 while ($this->match($operators, $m, false) && static::$precedence[$m[1]] >= $minP) {
2376 $whiteAfter = isset($this->buffer
[$this->count
]) &&
2377 ctype_space($this->buffer
[$this->count
]);
2378 $varAfter = isset($this->buffer
[$this->count
]) &&
2379 $this->buffer
[$this->count
] === '$';
2381 $this->whitespace();
2385 // don't turn negative numbers into expressions
2386 if ($op === '-' && $whiteBefore && ! $whiteAfter && ! $varAfter) {
2390 if (! $this->value($rhs) && ! $this->expression($rhs, true, false)) {
2394 if ($op === '-' && ! $whiteAfter && $rhs[0] === Type
::T_KEYWORD
) {
2398 // peek and see if rhs belongs to next operator
2399 if ($this->peek($operators, $next) && static::$precedence[$next[1]] > static::$precedence[$op]) {
2400 $rhs = $this->expHelper($rhs, static::$precedence[$next[1]]);
2403 $lhs = [Type
::T_EXPRESSION
, $op, $lhs, $rhs, $this->inParens
, $whiteBefore, $whiteAfter];
2406 $whiteBefore = isset($this->buffer
[$this->count
- 1]) &&
2407 ctype_space($this->buffer
[$this->count
- 1]);
2422 protected function value(&$out)
2424 if (! isset($this->buffer
[$this->count
])) {
2429 $char = $this->buffer
[$this->count
];
2432 $this->literal('url(', 4) &&
2433 $this->match('data:([a-z]+)\/([a-z0-9.+-]+);base64,', $m, false)
2437 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwyxz0123456789+/=',
2441 $this->count +
= $len;
2443 if ($this->matchChar(')')) {
2444 $content = substr($this->buffer
, $s, $this->count
- $s);
2445 $out = [Type
::T_KEYWORD
, $content];
2454 $this->literal('url(', 4, false) &&
2455 $this->match('\s*(\/\/[^\s\)]+)\s*', $m)
2457 $content = 'url(' . $m[1];
2459 if ($this->matchChar(')')) {
2461 $out = [Type
::T_KEYWORD
, $content];
2470 if ($char === 'n' && $this->literal('not', 3, false)) {
2472 $this->whitespace() &&
2473 $this->value($inner)
2475 $out = [Type
::T_UNARY
, 'not', $inner, $this->inParens
];
2482 if ($this->parenValue($inner)) {
2483 $out = [Type
::T_UNARY
, 'not', $inner, $this->inParens
];
2492 if ($char === '+') {
2495 $follow_white = $this->whitespace();
2497 if ($this->value($inner)) {
2498 $out = [Type
::T_UNARY
, '+', $inner, $this->inParens
];
2503 if ($follow_white) {
2504 $out = [Type
::T_KEYWORD
, $char];
2514 if ($char === '-') {
2515 if ($this->customProperty($out)) {
2521 $follow_white = $this->whitespace();
2523 if ($this->variable($inner) ||
$this->unit($inner) ||
$this->parenValue($inner)) {
2524 $out = [Type
::T_UNARY
, '-', $inner, $this->inParens
];
2530 $this->keyword($inner) &&
2531 ! $this->func($inner, $out)
2533 $out = [Type
::T_UNARY
, '-', $inner, $this->inParens
];
2538 if ($follow_white) {
2539 $out = [Type
::T_KEYWORD
, $char];
2548 if ($char === '(' && $this->parenValue($out)) {
2552 if ($char === '#') {
2553 if ($this->interpolation($out) ||
$this->color($out)) {
2559 if ($this->keyword($keyword)) {
2560 $out = [Type
::T_KEYWORD
, '#' . $keyword];
2568 if ($this->matchChar('&', true)) {
2569 $out = [Type
::T_SELF
];
2574 if ($char === '$' && $this->variable($out)) {
2578 if ($char === 'p' && $this->progid($out)) {
2582 if (($char === '"' ||
$char === "'") && $this->string($out)) {
2586 if ($this->unit($out)) {
2590 // unicode range with wildcards
2592 $this->literal('U+', 2) &&
2593 $this->match('\?+|([0-9A-F]+(\?+|(-[0-9A-F]+))?)', $m, false)
2595 $unicode = explode('-', $m[0]);
2596 if (strlen(reset($unicode)) <= 6 && strlen(end($unicode)) <= 6) {
2597 $out = [Type
::T_KEYWORD
, 'U+' . $m[0]];
2601 $this->count
-= strlen($m[0]) +
2;
2604 if ($this->keyword($keyword, false)) {
2605 if ($this->func($keyword, $out)) {
2609 $this->whitespace();
2611 if ($keyword === 'null') {
2612 $out = [Type
::T_NULL
];
2614 $out = [Type
::T_KEYWORD
, $keyword];
2624 * Parse parenthesized value
2630 protected function parenValue(&$out)
2634 $inParens = $this->inParens
;
2636 if ($this->matchChar('(')) {
2637 if ($this->matchChar(')')) {
2638 $out = [Type
::T_LIST
, '', []];
2643 $this->inParens
= true;
2646 $this->expression($exp) &&
2647 $this->matchChar(')')
2650 $this->inParens
= $inParens;
2656 $this->inParens
= $inParens;
2669 protected function progid(&$out)
2674 $this->literal('progid:', 7, false) &&
2675 $this->openString('(', $fn) &&
2676 $this->matchChar('(')
2678 $this->openString(')', $args, '(');
2680 if ($this->matchChar(')')) {
2681 $out = [Type
::T_STRING
, '', [
2682 'progid:', $fn, '(', $args, ')'
2695 * Parse function call
2697 * @param string $name
2698 * @param array $func
2702 protected function func($name, &$func)
2706 if ($this->matchChar('(')) {
2707 if ($name === 'alpha' && $this->argumentList($args)) {
2708 $func = [Type
::T_FUNCTION
, $name, [Type
::T_STRING
, '', $args]];
2713 if ($name !== 'expression' && ! preg_match('/^(-[a-z]+-)?calc$/', $name)) {
2717 $this->argValues($args) &&
2718 $this->matchChar(')')
2720 $func = [Type
::T_FUNCTION_CALL
, $name, $args];
2729 ($this->openString(')', $str, '(') ||
true) &&
2730 $this->matchChar(')')
2734 if (! empty($str)) {
2735 $args[] = [null, [Type
::T_STRING
, '', [$str]]];
2738 $func = [Type
::T_FUNCTION_CALL
, $name, $args];
2750 * Parse function call argument list
2756 protected function argumentList(&$out)
2759 $this->matchChar('(');
2763 while ($this->keyword($var)) {
2765 $this->matchChar('=') &&
2766 $this->expression($exp)
2768 $args[] = [Type
::T_STRING
, '', [$var . '=']];
2776 if (! $this->matchChar(',')) {
2780 $args[] = [Type
::T_STRING
, '', [', ']];
2783 if (! $this->matchChar(')') ||
! $args) {
2795 * Parse mixin/function definition argument list
2801 protected function argumentDef(&$out)
2804 $this->matchChar('(');
2808 while ($this->variable($var)) {
2809 $arg = [$var[1], null, false];
2814 $this->matchChar(':') &&
2815 $this->genericList($defaultVal, 'expression', '', true)
2817 $arg[1] = $defaultVal;
2824 if ($this->literal('...', 3)) {
2825 $sss = $this->count
;
2827 if (! $this->matchChar(')')) {
2828 throw $this->parseError('... has to be after the final argument');
2840 if (! $this->matchChar(',')) {
2845 if (! $this->matchChar(')')) {
2863 protected function map(&$out)
2867 if (! $this->matchChar('(')) {
2875 $this->genericList($key, 'expression', '', true) &&
2876 $this->matchChar(':') &&
2877 $this->genericList($value, 'expression', '', true)
2882 if (! $this->matchChar(',')) {
2887 if (! $keys ||
! $this->matchChar(')')) {
2893 $out = [Type
::T_MAP
, $keys, $values];
2905 protected function color(&$out)
2909 if ($this->match('(#([0-9a-f]+)\b)', $m)) {
2910 if (\
in_array(\
strlen($m[2]), [3,4,6,8])) {
2911 $out = [Type
::T_KEYWORD
, $m[0]];
2925 * Parse number with unit
2927 * @param array $unit
2931 protected function unit(&$unit)
2935 if ($this->match('([0-9]*(\.)?[0-9]+)([%a-zA-Z]+)?', $m, false)) {
2936 if (\
strlen($this->buffer
) === $this->count ||
! ctype_digit($this->buffer
[$this->count
])) {
2937 $this->whitespace();
2939 $unit = new Node\
Number($m[1], empty($m[3]) ?
'' : $m[3]);
2957 protected function string(&$out, $keepDelimWithInterpolation = false)
2961 if ($this->matchChar('"', false)) {
2963 } elseif ($this->matchChar("'", false)) {
2970 $oldWhite = $this->eatWhiteDefault
;
2971 $this->eatWhiteDefault
= false;
2972 $hasInterpolation = false;
2974 while ($this->matchString($m, $delim)) {
2979 if ($m[2] === '#{') {
2980 $this->count
-= \
strlen($m[2]);
2982 if ($this->interpolation($inter, false)) {
2983 $content[] = $inter;
2984 $hasInterpolation = true;
2986 $this->count +
= \
strlen($m[2]);
2987 $content[] = '#{'; // ignore it
2989 } elseif ($m[2] === "\r") {
2990 $content[] = chr(10);
2992 # DEPRECATION WARNING on line x, column y of zzz:
2993 # Unescaped multiline strings are deprecated and will be removed in a future version of Sass.
2994 # To include a newline in a string, use "\a" or "\a " as in CSS.
2995 if ($this->matchChar("\n", false)) {
2998 } elseif ($m[2] === '\\') {
3000 $this->literal("\r\n", 2, false) ||
3001 $this->matchChar("\r", false) ||
3002 $this->matchChar("\n", false) ||
3003 $this->matchChar("\f", false)
3005 // this is a continuation escaping, to be ignored
3006 } elseif ($this->matchEscapeCharacter($c)) {
3009 throw $this->parseError('Unterminated escape sequence');
3012 $this->count
-= \
strlen($delim);
3017 $this->eatWhiteDefault
= $oldWhite;
3019 if ($this->literal($delim, \
strlen($delim))) {
3020 if ($hasInterpolation && ! $keepDelimWithInterpolation) {
3024 $out = [Type
::T_STRING
, $delim, $content];
3035 * @param string $out
3036 * @param bool $inKeywords
3039 protected function matchEscapeCharacter(&$out, $inKeywords = false)
3042 if ($this->match('[a-f0-9]', $m, false)) {
3045 for ($i = 5; $i--;) {
3046 if ($this->match('[a-f0-9]', $m, false)) {
3053 // CSS allows Unicode escape sequences to be followed by a delimiter space
3054 // (necessary in some cases for shorter sequences to disambiguate their end)
3055 $this->matchChar(' ', false);
3057 $value = hexdec($hex);
3059 if (!$inKeywords && ($value == 0 ||
($value >= 0xD800 && $value <= 0xDFFF) ||
$value >= 0x10FFFF)) {
3060 $out = "\xEF\xBF\xBD"; // "\u{FFFD}" but with a syntax supported on PHP 5
3061 } elseif ($value < 0x20) {
3062 $out = Util
::mbChr($value);
3064 $out = Util
::mbChr($value);
3070 if ($this->match('.', $m, false)) {
3071 if ($inKeywords && in_array($m[0], ["'",'"','@','&',' ','\\',':','/','%'])) {
3084 * Parse keyword or interpolation
3087 * @param boolean $restricted
3091 protected function mixedKeyword(&$out, $restricted = false)
3095 $oldWhite = $this->eatWhiteDefault
;
3096 $this->eatWhiteDefault
= false;
3099 if ($restricted ?
$this->restrictedKeyword($key) : $this->keyword($key)) {
3104 if ($this->interpolation($inter)) {
3112 $this->eatWhiteDefault
= $oldWhite;
3118 if ($this->eatWhiteDefault
) {
3119 $this->whitespace();
3128 * Parse an unbounded string stopped by $end
3130 * @param string $end
3132 * @param string $nestOpen
3133 * @param string $nestClose
3134 * @param boolean $rtrim
3135 * @param string $disallow
3139 protected function openString($end, &$out, $nestOpen = null, $nestClose = null, $rtrim = true, $disallow = null)
3141 $oldWhite = $this->eatWhiteDefault
;
3142 $this->eatWhiteDefault
= false;
3144 if ($nestOpen && ! $nestClose) {
3148 $patt = ($disallow ?
'[^' . $this->pregQuote($disallow) . ']' : '.');
3149 $patt = '(' . $patt . '*?)([\'"]|#\{|'
3150 . $this->pregQuote($end) . '|'
3151 . (($nestClose && $nestClose !== $end) ?
$this->pregQuote($nestClose) . '|' : '')
3152 . static::$commentPattern . ')';
3158 while ($this->match($patt, $m, false)) {
3159 if (isset($m[1]) && $m[1] !== '') {
3163 $nestingLevel +
= substr_count($m[1], $nestOpen);
3169 $this->count
-= \
strlen($tok);
3171 if ($tok === $end && ! $nestingLevel) {
3175 if ($tok === $nestClose) {
3179 if (($tok === "'" ||
$tok === '"') && $this->string($str, true)) {
3184 if ($tok === '#{' && $this->interpolation($inter)) {
3185 $content[] = $inter;
3190 $this->count +
= \
strlen($tok);
3193 $this->eatWhiteDefault
= $oldWhite;
3195 if (! $content ||
$tok !== $end) {
3200 if ($rtrim && \
is_string(end($content))) {
3201 $content[\
count($content) - 1] = rtrim(end($content));
3204 $out = [Type
::T_STRING
, '', $content];
3210 * Parser interpolation
3212 * @param string|array $out
3213 * @param boolean $lookWhite save information about whitespace before and after
3217 protected function interpolation(&$out, $lookWhite = true)
3219 $oldWhite = $this->eatWhiteDefault
;
3220 $allowVars = $this->allowVars
;
3221 $this->allowVars
= true;
3222 $this->eatWhiteDefault
= true;
3227 $this->literal('#{', 2) &&
3228 $this->valueList($value) &&
3229 $this->matchChar('}', false)
3231 if ($value === [Type
::T_SELF
]) {
3235 $left = ($s > 0 && preg_match('/\s/', $this->buffer
[$s - 1])) ?
' ' : '';
3237 ! empty($this->buffer
[$this->count
]) &&
3238 preg_match('/\s/', $this->buffer
[$this->count
])
3241 $left = $right = false;
3244 $out = [Type
::T_INTERPOLATE
, $value, $left, $right];
3247 $this->eatWhiteDefault
= $oldWhite;
3248 $this->allowVars
= $allowVars;
3250 if ($this->eatWhiteDefault
) {
3251 $this->whitespace();
3259 $this->eatWhiteDefault
= $oldWhite;
3260 $this->allowVars
= $allowVars;
3266 * Parse property name (as an array of parts or a string)
3272 protected function propertyName(&$out)
3276 $oldWhite = $this->eatWhiteDefault
;
3277 $this->eatWhiteDefault
= false;
3280 if ($this->interpolation($inter)) {
3285 if ($this->keyword($text)) {
3290 if (! $parts && $this->match('[:.#]', $m, false)) {
3299 $this->eatWhiteDefault
= $oldWhite;
3305 // match comment hack
3306 if (preg_match(static::$whitePattern, $this->buffer
, $m, null, $this->count
)) {
3307 if (! empty($m[0])) {
3309 $this->count +
= \
strlen($m[0]);
3313 $this->whitespace(); // get any extra whitespace
3315 $out = [Type
::T_STRING
, '', $parts];
3321 * Parse custom property name (as an array of parts or a string)
3327 protected function customProperty(&$out)
3331 if (! $this->literal('--', 2, false)) {
3337 $oldWhite = $this->eatWhiteDefault
;
3338 $this->eatWhiteDefault
= false;
3341 if ($this->interpolation($inter)) {
3346 if ($this->matchChar('&', false)) {
3347 $parts[] = [Type
::T_SELF
];
3351 if ($this->variable($var)) {
3356 if ($this->keyword($text)) {
3364 $this->eatWhiteDefault
= $oldWhite;
3366 if (\
count($parts) == 1) {
3372 $this->whitespace(); // get any extra whitespace
3374 $out = [Type
::T_STRING
, '', $parts];
3380 * Parse comma separated selector list
3383 * @param string|boolean $subSelector
3387 protected function selectors(&$out, $subSelector = false)
3392 while ($this->selector($sel, $subSelector)) {
3393 $selectors[] = $sel;
3395 if (! $this->matchChar(',', true)) {
3399 while ($this->matchChar(',', true)) {
3416 * Parse whitespace separated selector list
3419 * @param string|boolean $subSelector
3423 protected function selector(&$out, $subSelector = false)
3427 $discardComments = $this->discardComments
;
3428 $this->discardComments
= true;
3433 if ($this->match('[>+~]+', $m, true)) {
3435 $subSelector && \
is_string($subSelector) && strpos($subSelector, 'nth-') === 0 &&
3436 $m[0] === '+' && $this->match("(\d+|n\b)", $counter)
3440 $selector[] = [$m[0]];
3445 if ($this->selectorSingle($part, $subSelector)) {
3446 $selector[] = $part;
3447 $this->whitespace();
3454 $this->discardComments
= $discardComments;
3466 * parsing escaped chars in selectors:
3467 * - escaped single chars are kept escaped in the selector but in a normalized form
3468 * (if not in 0-9a-f range as this would be ambigous)
3469 * - other escaped sequences (multibyte chars or 0-9a-f) are kept in their initial escaped form,
3470 * normalized to lowercase
3472 * TODO: this is a fallback solution. Ideally escaped chars in selectors should be encoded as the genuine chars,
3473 * and escaping added when printing in the Compiler, where/if it's mandatory
3474 * - but this require a better formal selector representation instead of the array we have now
3476 * @param string $out
3477 * @param bool $keepEscapedNumber
3480 protected function matchEscapeCharacterInSelector(&$out, $keepEscapedNumber = false)
3482 $s_escape = $this->count
;
3483 if ($this->match('\\\\', $m)) {
3484 $out = '\\' . $m[0];
3488 if ($this->matchEscapeCharacter($escapedout, true)) {
3489 if (strlen($escapedout) === 1) {
3490 if (!preg_match(",\w,", $escapedout)) {
3491 $out = '\\' . $escapedout;
3493 } elseif (! $keepEscapedNumber ||
! \
is_numeric($escapedout)) {
3498 $escape_sequence = rtrim(substr($this->buffer
, $s_escape, $this->count
- $s_escape));
3499 if (strlen($escape_sequence) < 6) {
3500 $escape_sequence .= ' ';
3502 $out = '\\' . strtolower($escape_sequence);
3505 if ($this->match('\\S', $m)) {
3506 $out = '\\' . $m[0];
3515 * Parse the parts that make up a selector
3518 * div[yes=no]#something.hello.world:nth-child(-2n+1)%placeholder
3522 * @param string|boolean $subSelector
3526 protected function selectorSingle(&$out, $subSelector = false)
3528 $oldWhite = $this->eatWhiteDefault
;
3529 $this->eatWhiteDefault
= false;
3533 if ($this->matchChar('*', false)) {
3538 if (! isset($this->buffer
[$this->count
])) {
3543 $char = $this->buffer
[$this->count
];
3545 // see if we can stop early
3546 if ($char === '{' ||
$char === ',' ||
$char === ';' ||
$char === '}' ||
$char === '@') {
3550 // parsing a sub selector in () stop with the closing )
3551 if ($subSelector && $char === ')') {
3558 $parts[] = Compiler
::$selfSelector;
3560 ! $this->cssOnly ||
$this->assertPlainCssValid(false, $s);
3574 // handling of escaping in selectors : get the escaped char
3575 if ($char === '\\') {
3577 if ($this->matchEscapeCharacterInSelector($escaped, true)) {
3578 $parts[] = $escaped;
3584 if ($char === '%') {
3587 if ($this->placeholder($placeholder)) {
3589 $parts[] = $placeholder;
3590 ! $this->cssOnly ||
$this->assertPlainCssValid(false, $s);
3597 if ($char === '#') {
3598 if ($this->interpolation($inter)) {
3600 ! $this->cssOnly ||
$this->assertPlainCssValid(false, $s);
3609 // a pseudo selector
3610 if ($char === ':') {
3611 if ($this->buffer
[$this->count +
1] === ':') {
3619 if ($this->mixedKeyword($nameParts, true)) {
3622 foreach ($nameParts as $sub) {
3629 $nameParts === ['not'] ||
3630 $nameParts === ['is'] ||
3631 $nameParts === ['has'] ||
3632 $nameParts === ['where'] ||
3633 $nameParts === ['slotted'] ||
3634 $nameParts === ['nth-child'] ||
3635 $nameParts === ['nth-last-child'] ||
3636 $nameParts === ['nth-of-type'] ||
3637 $nameParts === ['nth-last-of-type']
3640 $this->matchChar('(', true) &&
3641 ($this->selectors($subs, reset($nameParts)) ||
true) &&
3642 $this->matchChar(')')
3646 while ($sub = array_shift($subs)) {
3647 while ($ps = array_shift($sub)) {
3648 foreach ($ps as &$p) {
3652 if (\
count($sub) && reset($sub)) {
3657 if (\
count($subs) && reset($subs)) {
3667 $this->matchChar('(', true) &&
3668 ($this->openString(')', $str, '(') ||
true) &&
3669 $this->matchChar(')')
3673 if (! empty($str)) {
3689 if ($subSelector && \
is_string($subSelector) && strpos($subSelector, 'nth-') === 0) {
3690 if ($this->match("(\s*(\+\s*|\-\s*)?(\d+|n|\d+n))+", $counter)) {
3691 $parts[] = $counter[0];
3692 //$parts[] = str_replace(' ', '', $counter[0]);
3699 // attribute selector
3702 $this->matchChar('[') &&
3703 ($this->openString(']', $str, '[') ||
true) &&
3704 $this->matchChar(']')
3708 if (! empty($str)) {
3719 if ($this->unit($unit)) {
3724 if ($this->restrictedKeyword($name, false, true)) {
3732 $this->eatWhiteDefault
= $oldWhite;
3750 protected function variable(&$out)
3755 $this->matchChar('$', false) &&
3756 $this->keyword($name)
3758 if ($this->allowVars
) {
3759 $out = [Type
::T_VARIABLE
, $name];
3761 $out = [Type
::T_KEYWORD
, '$' . $name];
3775 * @param string $word
3776 * @param boolean $eatWhitespace
3777 * @param boolean $inSelector
3781 protected function keyword(&$word, $eatWhitespace = null, $inSelector = false)
3784 $match = $this->match(
3786 ?
'(([\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]) ?|[\\\\].)*)'
3787 : '(([\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]) ?|[\\\\].)*)',
3795 // handling of escaping in keyword : get the escaped char
3796 if (strpos($word, '\\') !== false) {
3797 $send = $this->count
;
3800 $previousEscape = false;
3801 while ($this->count
< $send) {
3802 $char = $this->buffer
[$this->count
];
3805 $this->count
< $send
3810 $this->matchEscapeCharacterInSelector($out)
3812 $this->matchEscapeCharacter($out, true)
3815 $escapedWord[] = $out;
3817 if ($previousEscape) {
3818 $previousEscape = false;
3819 } elseif ($char === '\\') {
3820 $previousEscape = true;
3822 $escapedWord[] = $char;
3826 $word = implode('', $escapedWord);
3829 if (is_null($eatWhitespace) ?
$this->eatWhiteDefault
: $eatWhitespace) {
3830 $this->whitespace();
3840 * Parse a keyword that should not start with a number
3842 * @param string $word
3843 * @param boolean $eatWhitespace
3844 * @param boolean $inSelector
3848 protected function restrictedKeyword(&$word, $eatWhitespace = null, $inSelector = false)
3852 if ($this->keyword($word, $eatWhitespace, $inSelector) && (\
ord($word[0]) > 57 || \
ord($word[0]) < 48)) {
3862 * Parse a placeholder
3864 * @param string|array $placeholder
3868 protected function placeholder(&$placeholder)
3870 $match = $this->match(
3878 $placeholder = $m[1];
3883 if ($this->interpolation($placeholder)) {
3897 protected function url(&$out)
3899 if ($this->literal('url(', 4)) {
3903 ($this->string($out) ||
$this->spaceList($out)) &&
3904 $this->matchChar(')')
3906 $out = [Type
::T_STRING
, '', ['url(', $out, ')']];
3914 $this->openString(')', $out) &&
3915 $this->matchChar(')')
3917 $out = [Type
::T_STRING
, '', ['url(', $out, ')']];
3927 * Consume an end of statement delimiter
3928 * @param bool $eatWhitespace
3932 protected function end($eatWhitespace = null)
3934 if ($this->matchChar(';', $eatWhitespace)) {
3938 if ($this->count
=== \
strlen($this->buffer
) ||
$this->buffer
[$this->count
] === '}') {
3939 // if there is end of file or a closing block next then we don't need a ;
3947 * Strip assignment flag from the list
3949 * @param array $value
3953 protected function stripAssignmentFlags(&$value)
3957 for ($token = &$value; $token[0] === Type
::T_LIST
&& ($s = \
count($token[2])); $token = &$lastNode) {
3958 $lastNode = &$token[2][$s - 1];
3960 while ($lastNode[0] === Type
::T_KEYWORD
&& \
in_array($lastNode[1], ['!default', '!global'])) {
3961 array_pop($token[2]);
3963 $node = end($token[2]);
3964 $token = $this->flattenList($token);
3965 $flags[] = $lastNode[1];
3974 * Strip optional flag from selector list
3976 * @param array $selectors
3980 protected function stripOptionalFlag(&$selectors)
3983 $selector = end($selectors);
3984 $part = end($selector);
3986 if ($part === ['!optional']) {
3987 array_pop($selectors[\
count($selectors) - 1]);
3996 * Turn list of length 1 into value type
3998 * @param array $value
4002 protected function flattenList($value)
4004 if ($value[0] === Type
::T_LIST
&& \
count($value[2]) === 1) {
4005 return $this->flattenList($value[2][0]);
4012 * Quote regular expression
4014 * @param string $what
4018 private function pregQuote($what)
4020 return preg_quote($what, '/');
4024 * Extract line numbers from buffer
4026 * @param string $buffer
4028 private function extractLineNumbers($buffer)
4030 $this->sourcePositions
= [0 => 0];
4033 while (($pos = strpos($buffer, "\n", $prev)) !== false) {
4034 $this->sourcePositions
[] = $pos;
4038 $this->sourcePositions
[] = \
strlen($buffer);
4040 if (substr($buffer, -1) !== "\n") {
4041 $this->sourcePositions
[] = \
strlen($buffer) +
1;
4046 * Get source line number and column (given character position in the buffer)
4048 * @param integer $pos
4052 private function getSourcePosition($pos)
4055 $high = \
count($this->sourcePositions
);
4057 while ($low < $high) {
4058 $mid = (int) (($high +
$low) / 2);
4060 if ($pos < $this->sourcePositions
[$mid]) {
4065 if ($pos >= $this->sourcePositions
[$mid +
1]) {
4070 return [$mid +
1, $pos - $this->sourcePositions
[$mid]];
4073 return [$low +
1, $pos - $this->sourcePositions
[$low]];
4077 * Save internal encoding
4079 private function saveEncoding()
4081 if (\
extension_loaded('mbstring')) {
4082 $this->encoding
= mb_internal_encoding();
4084 mb_internal_encoding('iso-8859-1');
4089 * Restore internal encoding
4091 private function restoreEncoding()
4093 if (\
extension_loaded('mbstring') && $this->encoding
) {
4094 mb_internal_encoding($this->encoding
);