*/ class Parser { const SOURCE_INDEX = -1; const SOURCE_LINE = -2; const SOURCE_COLUMN = -3; /** * @var array */ protected static $precedence = [ '=' => 0, 'or' => 1, 'and' => 2, '==' => 3, '!=' => 3, '<=>' => 3, '<=' => 4, '>=' => 4, '<' => 4, '>' => 4, '+' => 5, '-' => 5, '*' => 6, '/' => 6, '%' => 6, ]; protected static $commentPattern; protected static $operatorPattern; protected static $whitePattern; private $sourceName; private $sourceIndex; private $sourcePositions; private $charset; private $count; private $env; private $inParens; private $eatWhiteDefault; private $buffer; private $utf8; private $encoding; private $patternModifiers; /** * Constructor * * @api * * @param string $sourceName * @param integer $sourceIndex * @param string $encoding */ public function __construct($sourceName, $sourceIndex = 0, $encoding = 'utf-8') { $this->sourceName = $sourceName ?: '(stdin)'; $this->sourceIndex = $sourceIndex; $this->charset = null; $this->utf8 = ! $encoding || strtolower($encoding) === 'utf-8'; $this->patternModifiers = $this->utf8 ? 'Aisu' : 'Ais'; if (empty(static::$operatorPattern)) { static::$operatorPattern = '([*\/%+-]|[!=]\=|\>\=?|\<\=\>|\<\=?|and|or)'; $commentSingle = '\/\/'; $commentMultiLeft = '\/\*'; $commentMultiRight = '\*\/'; static::$commentPattern = $commentMultiLeft . '.*?' . $commentMultiRight; static::$whitePattern = $this->utf8 ? '/' . $commentSingle . '[^\n]*\s*|(' . static::$commentPattern . ')\s*|\s+/AisuS' : '/' . $commentSingle . '[^\n]*\s*|(' . static::$commentPattern . ')\s*|\s+/AisS'; } } /** * Get source file name * * @api * * @return string */ public function getSourceName() { return $this->sourceName; } /** * Throw parser error * * @api * * @param string $msg * * @throws \Leafo\ScssPhp\Exception\ParserException */ public function throwParseError($msg = 'parse error') { list($line, /* $column */) = $this->getSourcePosition($this->count); $loc = empty($this->sourceName) ? "line: $line" : "$this->sourceName on line $line"; if ($this->peek("(.*?)(\n|$)", $m, $this->count)) { throw new ParserException("$msg: failed at `$m[1]` $loc"); } throw new ParserException("$msg: $loc"); } /** * Parser buffer * * @api * * @param string $buffer * * @return \Leafo\ScssPhp\Block */ public function parse($buffer) { // strip BOM (byte order marker) if (substr($buffer, 0, 3) === "\xef\xbb\xbf") { $buffer = substr($buffer, 3); } $this->buffer = rtrim($buffer, "\x00..\x1f"); $this->count = 0; $this->env = null; $this->inParens = false; $this->eatWhiteDefault = true; $this->saveEncoding(); $this->extractLineNumbers($buffer); $this->pushBlock(null); // root block $this->whitespace(); $this->pushBlock(null); $this->popBlock(); while ($this->parseChunk()) { ; } if ($this->count !== strlen($this->buffer)) { $this->throwParseError(); } if (! empty($this->env->parent)) { $this->throwParseError('unclosed block'); } if ($this->charset) { array_unshift($this->env->children, $this->charset); } $this->env->isRoot = true; $this->restoreEncoding(); return $this->env; } /** * Parse a value or value list * * @api * * @param string $buffer * @param string $out * * @return boolean */ public function parseValue($buffer, &$out) { $this->count = 0; $this->env = null; $this->inParens = false; $this->eatWhiteDefault = true; $this->buffer = (string) $buffer; $this->saveEncoding(); $list = $this->valueList($out); $this->restoreEncoding(); return $list; } /** * Parse a selector or selector list * * @api * * @param string $buffer * @param string $out * * @return boolean */ public function parseSelector($buffer, &$out) { $this->count = 0; $this->env = null; $this->inParens = false; $this->eatWhiteDefault = true; $this->buffer = (string) $buffer; $this->saveEncoding(); $selector = $this->selectors($out); $this->restoreEncoding(); return $selector; } /** * Parse a single chunk off the head of the buffer and append it to the * current parse environment. * * Returns false when the buffer is empty, or when there is an error. * * This function is called repeatedly until the entire document is * parsed. * * This parser is most similar to a recursive descent parser. Single * functions represent discrete grammatical rules for the language, and * they are able to capture the text that represents those rules. * * Consider the function Compiler::keyword(). (All parse functions are * structured the same.) * * The function takes a single reference argument. When calling the * function it will attempt to match a keyword on the head of the buffer. * If it is successful, it will place the keyword in the referenced * argument, advance the position in the buffer, and return true. If it * fails then it won't advance the buffer and it will return false. * * All of these parse functions are powered by Compiler::match(), which behaves * the same way, but takes a literal regular expression. Sometimes it is * more convenient to use match instead of creating a new function. * * Because of the format of the functions, to parse an entire string of * grammatical rules, you can chain them together using &&. * * But, if some of the rules in the chain succeed before one fails, then * the buffer position will be left at an invalid state. In order to * avoid this, Compiler::seek() is used to remember and set buffer positions. * * Before parsing a chain, use $s = $this->seek() to remember the current * position into $s. Then if a chain fails, use $this->seek($s) to * go back where we started. * * @return boolean */ protected function parseChunk() { $s = $this->seek(); // the directives if (isset($this->buffer[$this->count]) && $this->buffer[$this->count] === '@') { if ($this->literal('@at-root') && ($this->selectors($selector) || true) && ($this->map($with) || true) && $this->literal('{') ) { $atRoot = $this->pushSpecialBlock(Type::T_AT_ROOT, $s); $atRoot->selector = $selector; $atRoot->with = $with; return true; } $this->seek($s); if ($this->literal('@media') && $this->mediaQueryList($mediaQueryList) && $this->literal('{')) { $media = $this->pushSpecialBlock(Type::T_MEDIA, $s); $media->queryList = $mediaQueryList[2]; return true; } $this->seek($s); if ($this->literal('@mixin') && $this->keyword($mixinName) && ($this->argumentDef($args) || true) && $this->literal('{') ) { $mixin = $this->pushSpecialBlock(Type::T_MIXIN, $s); $mixin->name = $mixinName; $mixin->args = $args; return true; } $this->seek($s); if ($this->literal('@include') && $this->keyword($mixinName) && ($this->literal('(') && ($this->argValues($argValues) || true) && $this->literal(')') || true) && ($this->end() || $this->literal('{') && $hasBlock = true) ) { $child = [Type::T_INCLUDE, $mixinName, isset($argValues) ? $argValues : null, null]; if (! empty($hasBlock)) { $include = $this->pushSpecialBlock(Type::T_INCLUDE, $s); $include->child = $child; } else { $this->append($child, $s); } return true; } $this->seek($s); if ($this->literal('@scssphp-import-once') && $this->valueList($importPath) && $this->end() ) { $this->append([Type::T_SCSSPHP_IMPORT_ONCE, $importPath], $s); return true; } $this->seek($s); if ($this->literal('@import') && $this->valueList($importPath) && $this->end() ) { $this->append([Type::T_IMPORT, $importPath], $s); return true; } $this->seek($s); if ($this->literal('@import') && $this->url($importPath) && $this->end() ) { $this->append([Type::T_IMPORT, $importPath], $s); return true; } $this->seek($s); if ($this->literal('@extend') && $this->selectors($selectors) && $this->end() ) { // check for '!flag' $optional = $this->stripOptionalFlag($selectors); $this->append([Type::T_EXTEND, $selectors, $optional], $s); return true; } $this->seek($s); if ($this->literal('@function') && $this->keyword($fnName) && $this->argumentDef($args) && $this->literal('{') ) { $func = $this->pushSpecialBlock(Type::T_FUNCTION, $s); $func->name = $fnName; $func->args = $args; return true; } $this->seek($s); if ($this->literal('@break') && $this->end()) { $this->append([Type::T_BREAK], $s); return true; } $this->seek($s); if ($this->literal('@continue') && $this->end()) { $this->append([Type::T_CONTINUE], $s); return true; } $this->seek($s); if ($this->literal('@return') && ($this->valueList($retVal) || true) && $this->end()) { $this->append([Type::T_RETURN, isset($retVal) ? $retVal : [Type::T_NULL]], $s); return true; } $this->seek($s); if ($this->literal('@each') && $this->genericList($varNames, 'variable', ',', false) && $this->literal('in') && $this->valueList($list) && $this->literal('{') ) { $each = $this->pushSpecialBlock(Type::T_EACH, $s); foreach ($varNames[2] as $varName) { $each->vars[] = $varName[1]; } $each->list = $list; return true; } $this->seek($s); if ($this->literal('@while') && $this->expression($cond) && $this->literal('{') ) { $while = $this->pushSpecialBlock(Type::T_WHILE, $s); $while->cond = $cond; return true; } $this->seek($s); if ($this->literal('@for') && $this->variable($varName) && $this->literal('from') && $this->expression($start) && ($this->literal('through') || ($forUntil = true && $this->literal('to'))) && $this->expression($end) && $this->literal('{') ) { $for = $this->pushSpecialBlock(Type::T_FOR, $s); $for->var = $varName[1]; $for->start = $start; $for->end = $end; $for->until = isset($forUntil); return true; } $this->seek($s); if ($this->literal('@if') && $this->valueList($cond) && $this->literal('{')) { $if = $this->pushSpecialBlock(Type::T_IF, $s); $if->cond = $cond; $if->cases = []; return true; } $this->seek($s); if ($this->literal('@debug') && $this->valueList($value) && $this->end() ) { $this->append([Type::T_DEBUG, $value], $s); return true; } $this->seek($s); if ($this->literal('@warn') && $this->valueList($value) && $this->end() ) { $this->append([Type::T_WARN, $value], $s); return true; } $this->seek($s); if ($this->literal('@error') && $this->valueList($value) && $this->end() ) { $this->append([Type::T_ERROR, $value], $s); return true; } $this->seek($s); if ($this->literal('@content') && $this->end()) { $this->append([Type::T_MIXIN_CONTENT], $s); return true; } $this->seek($s); $last = $this->last(); if (isset($last) && $last[0] === Type::T_IF) { list(, $if) = $last; if ($this->literal('@else')) { if ($this->literal('{')) { $else = $this->pushSpecialBlock(Type::T_ELSE, $s); } elseif ($this->literal('if') && $this->valueList($cond) && $this->literal('{')) { $else = $this->pushSpecialBlock(Type::T_ELSEIF, $s); $else->cond = $cond; } if (isset($else)) { $else->dontAppend = true; $if->cases[] = $else; return true; } } $this->seek($s); } // only retain the first @charset directive encountered if ($this->literal('@charset') && $this->valueList($charset) && $this->end() ) { if (! isset($this->charset)) { $statement = [Type::T_CHARSET, $charset]; list($line, $column) = $this->getSourcePosition($s); $statement[static::SOURCE_LINE] = $line; $statement[static::SOURCE_COLUMN] = $column; $statement[static::SOURCE_INDEX] = $this->sourceIndex; $this->charset = $statement; } return true; } $this->seek($s); // doesn't match built in directive, do generic one if ($this->literal('@', false) && $this->keyword($dirName) && ($this->variable($dirValue) || $this->openString('{', $dirValue) || true) && $this->literal('{') ) { if ($dirName === 'media') { $directive = $this->pushSpecialBlock(Type::T_MEDIA, $s); } else { $directive = $this->pushSpecialBlock(Type::T_DIRECTIVE, $s); $directive->name = $dirName; } if (isset($dirValue)) { $directive->value = $dirValue; } return true; } $this->seek($s); return false; } // property shortcut // captures most properties before having to parse a selector if ($this->keyword($name, false) && $this->literal(': ') && $this->valueList($value) && $this->end() ) { $name = [Type::T_STRING, '', [$name]]; $this->append([Type::T_ASSIGN, $name, $value], $s); return true; } $this->seek($s); // variable assigns if ($this->variable($name) && $this->literal(':') && $this->valueList($value) && $this->end() ) { // check for '!flag' $assignmentFlags = $this->stripAssignmentFlags($value); $this->append([Type::T_ASSIGN, $name, $value, $assignmentFlags], $s); return true; } $this->seek($s); // misc if ($this->literal('-->')) { return true; } // opening css block if ($this->selectors($selectors) && $this->literal('{')) { $this->pushBlock($selectors, $s); return true; } $this->seek($s); // property assign, or nested assign if ($this->propertyName($name) && $this->literal(':')) { $foundSomething = false; if ($this->valueList($value)) { $this->append([Type::T_ASSIGN, $name, $value], $s); $foundSomething = true; } if ($this->literal('{')) { $propBlock = $this->pushSpecialBlock(Type::T_NESTED_PROPERTY, $s); $propBlock->prefix = $name; $foundSomething = true; } elseif ($foundSomething) { $foundSomething = $this->end(); } if ($foundSomething) { return true; } } $this->seek($s); // closing a block if ($this->literal('}')) { $block = $this->popBlock(); if (isset($block->type) && $block->type === Type::T_INCLUDE) { $include = $block->child; unset($block->child); $include[3] = $block; $this->append($include, $s); } elseif (empty($block->dontAppend)) { $type = isset($block->type) ? $block->type : Type::T_BLOCK; $this->append([$type, $block], $s); } return true; } // extra stuff if ($this->literal(';') || $this->literal('