*/ class Parser { protected static $precedence = array( 'or' => 0, 'and' => 1, '==' => 2, '!=' => 2, '<=' => 2, '>=' => 2, '=' => 2, '<' => 3, '>' => 2, '+' => 3, '-' => 3, '*' => 4, '/' => 4, '%' => 4, ); protected static $operators = array('+', '-', '*', '/', '%', '==', '!=', '<=', '>=', '<', '>', 'and', 'or'); protected static $operatorStr; protected static $whitePattern; protected static $commentMulti; protected static $commentSingle = '//'; protected static $commentMultiLeft = '/*'; protected static $commentMultiRight = '*/'; /** * Constructor * * @param string $sourceName * @param boolean $rootParser */ public function __construct($sourceName = null, $rootParser = true) { $this->sourceName = $sourceName; $this->rootParser = $rootParser; 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'; } } 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 (false !== $this->parseChunk()) { ; } if ($this->count != strlen($this->buffer)) { $this->throwParseError(); } if (!empty($this->env->parent)) { $this->throwParseError('unclosed block'); } $this->env->isRoot = true; return $this->env; } /** * 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('@media') && $this->mediaQueryList($mediaQueryList) && $this->literal('{')) { $media = $this->pushSpecialBlock('media'); $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'); $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'); $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'); $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'); 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'); $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'); $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'); $if->cond = $cond; $if->cases = array(); return true; } $this->seek($s); if (($this->literal('@debug') || $this->literal('@warn')) && $this->valueList($value) && $this->end()) { $this->append(array('debug', $value, $s), $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'); } elseif ($this->literal('if') && $this->valueList($cond) && $this->literal('{')) { $else = $this->pushSpecialBlock('elseif'); $else->cond = $cond; } if (isset($else)) { $else->dontAppend = true; $if->cases[] = $else; return true; } } $this->seek($s); } if ($this->literal('@charset') && $this->valueList($charset) && $this->end() ) { $this->append(array('charset', $charset), $s); 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'); $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 !default $defaultVar = $value[0] == 'list' && $this->stripDefault($value); $this->append(array('assign', $name, $value, $defaultVar), $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); 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'); $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('