*/ class Parser { const SOURCE_INDEX = -1; const SOURCE_LINE = -2; const SOURCE_COLUMN = -3; /** * @var array */ protected static $precedence = array('=' => 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(self::$operatorPattern)) { self::$operatorPattern = '([*\\/%+-]|[!=]\\=|\\>\\=?|\\<\\=\\>|\\<\\=?|and|or)'; $commentSingle = '\\/\\/'; $commentMultiLeft = '\\/\\*'; $commentMultiRight = '\\*\\/'; self::$commentPattern = $commentMultiLeft . '.*?' . $commentMultiRight; self::$whitePattern = $this->utf8 ? '/' . $commentSingle . '[^\\n]*\\s*|(' . self::$commentPattern . ')\\s*|\\s+/AisuS' : '/' . $commentSingle . '[^\\n]*\\s*|(' . self::$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, ) = $this->getSourcePosition($this->count); $loc = empty($this->sourceName) ? "line: {$line}" : "{$this->sourceName} on line {$line}"; if ($this->peek('(.*?)( |$)', $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) { $this->count = 0; $this->env = null; $this->inParens = false; $this->eatWhiteDefault = true; $this->buffer = rtrim($buffer, '..'); $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 = array(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(array(Type::T_SCSSPHP_IMPORT_ONCE, $importPath), $s); return true; } $this->seek($s); if ($this->literal('@import') && $this->valueList($importPath) && $this->end()) { $this->append(array(Type::T_IMPORT, $importPath), $s); return true; } $this->seek($s); if ($this->literal('@import') && $this->url($importPath) && $this->end()) { $this->append(array(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(array(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(array(Type::T_BREAK), $s); return true; } $this->seek($s); if ($this->literal('@continue') && $this->end()) { $this->append(array(Type::T_CONTINUE), $s); return true; } $this->seek($s); if ($this->literal('@return') && ($this->valueList($retVal) || true) && $this->end()) { $this->append(array(Type::T_RETURN, isset($retVal) ? $retVal : array(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 = array(); return true; } $this->seek($s); if ($this->literal('@debug') && $this->valueList($value) && $this->end()) { $this->append(array(Type::T_DEBUG, $value), $s); return true; } $this->seek($s); if ($this->literal('@warn') && $this->valueList($value) && $this->end()) { $this->append(array(Type::T_WARN, $value), $s); return true; } $this->seek($s); if ($this->literal('@error') && $this->valueList($value) && $this->end()) { $this->append(array(Type::T_ERROR, $value), $s); return true; } $this->seek($s); if ($this->literal('@content') && $this->end()) { $this->append(array(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 = array(Type::T_CHARSET, $charset); list($line, $column) = $this->getSourcePosition($s); $statement[self::SOURCE_LINE] = $line; $statement[self::SOURCE_COLUMN] = $column; $statement[self::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 = array(Type::T_STRING, '', array($name)); $this->append(array(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(array(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(array(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(array($type, $block), $s); } return true; } // extra stuff if ($this->literal(';') || $this->literal('