*/ class Parser { const SOURCE_POSITION = -1; const SOURCE_PARSER = -2; /** * @var array */ protected static $precedence = array( '=' => 0, 'or' => 1, 'and' => 2, '==' => 3, '!=' => 3, '<=' => 4, '>=' => 4, '<' => 4, '>' => 4, '+' => 5, '-' => 5, '*' => 6, '/' => 6, '%' => 6, ); /** * @var array */ protected static $operators = array( '+', '-', '*', '/', '%', '==', '!=', '<=', '>=', '<', '>', 'and', 'or', ); protected static $operatorStr; protected static $whitePattern; protected static $commentMulti; protected static $commentSingle = '//'; protected static $commentMultiLeft = '/*'; protected static $commentMultiRight = '*/'; private $sourceName; private $rootParser; private $charset; private $count; private $env; private $inParens; private $eatWhiteDefault; private $buffer; /** * Constructor * * @param string $sourceName * @param boolean $rootParser */ public function __construct($sourceName = null, $rootParser = true) { $this->sourceName = $sourceName ?: '(stdin)'; $this->rootParser = $rootParser; $this->charset = null; if (empty(self::$operatorStr)) { self::$operatorStr = $this->makeOperatorStr(self::$operators); $commentSingle = $this->pregQuote(self::$commentSingle); $commentMultiLeft = $this->pregQuote(self::$commentMultiLeft); $commentMultiRight = $this->pregQuote(self::$commentMultiRight); self::$commentMulti = $commentMultiLeft . '.*?' . $commentMultiRight; self::$whitePattern = '/' . $commentSingle . '[^\n]*\s*|(' . self::$commentMulti . ')\s*|\s+/Ais'; } } /** * Make operator regex * * @param array $operators * * @return string */ protected static function makeOperatorStr($operators) { return '(' . implode('|', array_map(array('Leafo\ScssPhp\Parser', 'pregQuote'), $operators)) . ')'; } /** * Parser buffer * * @param string $buffer; * * @return \stdClass */ public function parse($buffer) { $this->count = 0; $this->env = null; $this->inParens = false; $this->eatWhiteDefault = true; $this->buffer = $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; return $this->env; } /** * Parse a value or value list * * @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; return $this->valueList($out); } /** * Parse a selector or selector list * * @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; return $this->selectors($out); } /** * 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('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('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('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('include', $mixinName, isset($argValues) ? $argValues : null, null); if (! empty($hasBlock)) { $include = $this->pushSpecialBlock('include', $s); $include->child = $child; } else { $this->append($child, $s); } return true; } $this->seek($s); if ($this->literal('@import') && $this->valueList($importPath) && $this->end() ) { $this->append(array('import', $importPath), $s); return true; } $this->seek($s); if ($this->literal('@import') && $this->url($importPath) && $this->end() ) { $this->append(array('import', $importPath), $s); return true; } $this->seek($s); if ($this->literal('@extend') && $this->selectors($selector) && $this->end() ) { $this->append(array('extend', $selector), $s); return true; } $this->seek($s); if ($this->literal('@function') && $this->keyword($fnName) && $this->argumentDef($args) && $this->literal('{') ) { $func = $this->pushSpecialBlock('function', $s); $func->name = $fnName; $func->args = $args; return true; } $this->seek($s); if ($this->literal('@return') && $this->valueList($retVal) && $this->end()) { $this->append(array('return', $retVal), $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('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('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('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('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('debug', $value), $s); return true; } $this->seek($s); if ($this->literal('@warn') && $this->valueList($value) && $this->end() ) { $this->append(array('warn', $value), $s); return true; } $this->seek($s); if ($this->literal('@error') && $this->valueList($value) && $this->end() ) { $this->append(array('error', $value), $s); return true; } $this->seek($s); if ($this->literal('@content') && $this->end()) { $this->append(array('mixin_content'), $s); return true; } $this->seek($s); $last = $this->last(); if (isset($last) && $last[0] === 'if') { list(, $if) = $last; if ($this->literal('@else')) { if ($this->literal('{')) { $else = $this->pushSpecialBlock('else', $s); } elseif ($this->literal('if') && $this->valueList($cond) && $this->literal('{')) { $else = $this->pushSpecialBlock('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('charset', $charset); $statement[self::SOURCE_POSITION] = $s; if (! $this->rootParser) { $statement[self::SOURCE_PARSER] = $this; } $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('{') ) { $directive = $this->pushSpecialBlock('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('string', '', array($name)); $this->append(array('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' $assignmentFlag = $this->stripAssignmentFlag($value); $this->append(array('assign', $name, $value, $assignmentFlag), $s); return true; } $this->seek($s); // misc if ($this->literal('-->')) { return true; } // opening css block if ($this->selectors($selectors) && $this->literal('{')) { $b = $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('assign', $name, $value), $s); $foundSomething = true; } if ($this->literal('{')) { $propBlock = $this->pushSpecialBlock('nestedprop', $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 === 'include') { $include = $block->child; unset($block->child); $include[3] = $block; $this->append($include, $s); } elseif (empty($block->dontAppend)) { $type = isset($block->type) ? $block->type : 'block'; $this->append(array($type, $block), $s); } return true; } // extra stuff if ($this->literal(';') || $this->literal('