<?php /* SVN FILE: $Id: SassScriptParser.php 118 2010-09-21 09:45:11Z chris.l.yates@gmail.com $ */ /**
* SassScriptParser class file. * @author Chris Yates <chris.l.yates@gmail.com> * @copyright Copyright (c) 2010 PBM Web Development * @license http://phamlp.googlecode.com/files/license.txt * @package PHamlP * @subpackage Sass.script */
require_once('SassScriptLexer.php'); require_once('SassScriptParserExceptions.php');
/**
* SassScriptParser class. * Parses SassScript. SassScript is lexed into {@link http://en.wikipedia.org/wiki/Reverse_Polish_notation Reverse Polish notation} by the SassScriptLexer and * the calculated result returned. * @package PHamlP * @subpackage Sass.script */
class SassScriptParser {
const MATCH_INTERPOLATION = '/(?<!\\\\)#\{(.*?)\}/'; const DEFAULT_ENV = 0; const CSS_RULE = 1; const CSS_PROPERTY = 2; /** * @var SassContext Used for error reporting */ public static $context; /** * @var SassScriptLexer the lexer object */ private $lexer; /** * SassScriptParser constructor. * @return SassScriptParser */ public function __construct() { $this->lexer = new SassScriptLexer($this); } /** * Replace interpolated SassScript contained in '#{}' with the parsed value. * @param string the text to interpolate * @param SassContext the context in which the string is interpolated * @return string the interpolated text */ public function interpolate($string, $context) { for ($i = 0, $n = preg_match_all(self::MATCH_INTERPOLATION, $string, $matches); $i < $n; $i++) { $matches[1][$i] = $this->evaluate($matches[1][$i], $context)->toString(); } return str_replace($matches[0], $matches[1], $string); } /** * Evaluate a SassScript. * @param string expression to parse * @param SassContext the context in which the expression is evaluated * @param integer the environment in which the expression is evaluated * @return SassLiteral parsed value */ public function evaluate($expression, $context, $environment=self::DEFAULT_ENV) { self::$context = $context; $operands = array(); $tokens = $this->parse($expression, $context, $environment); while (count($tokens)) { $token = array_shift($tokens); if ($token instanceof SassScriptFunction) { array_push($operands, $token->perform()); } elseif ($token instanceof SassLiteral) { if ($token instanceof SassString) { $token = new SassString($this->interpolate($token->toString(), self::$context)); } array_push($operands, $token); } else { $args = array(); for ($i = 0, $c = $token->operandCount; $i < $c; $i++) { $args[] = array_pop($operands); } array_push($operands, $token->perform($args)); } } return array_shift($operands); } /** * Parse SassScript to a set of tokens in RPN * using the Shunting Yard Algorithm. * @param string expression to parse * @param SassContext the context in which the expression is parsed * @param integer the environment in which the expression is parsed * @return array tokens in RPN */ public function parse($expression, $context, $environment=self::DEFAULT_ENV) { $outputQueue = array(); $operatorStack = array(); $parenthesis = 0; $tokens = $this->lexer->lex($expression, $context); foreach($tokens as $i=>$token) { // If two literals/expessions are seperated by whitespace use the concat operator if (empty($token)) { if ($i > 0 && (!$tokens[$i-1] instanceof SassScriptOperation || $tokens[$i-1]->operator === SassScriptOperation::$operators[')'][0]) && (!$tokens[$i+1] instanceof SassScriptOperation || $tokens[$i+1]->operator === SassScriptOperation::$operators['('][0])) { $token = new SassScriptOperation(SassScriptOperation::$defaultOperator, $context); } else { continue; } } elseif ($token instanceof SassScriptVariable) { $token = $token->evaluate($context); $environment = self::DEFAULT_ENV; } // If the token is a number or function add it to the output queue. if ($token instanceof SassLiteral || $token instanceof SassScriptFunction) { if ($environment === self::CSS_PROPERTY && $token instanceof SassNumber && !$parenthesis) { $token->inExpression = false; } array_push($outputQueue, $token); } // If the token is an operation elseif ($token instanceof SassScriptOperation) { // If the token is a left parenthesis push it onto the stack. if ($token->operator == SassScriptOperation::$operators['('][0]) { array_push($operatorStack, $token); $parenthesis++; } // If the token is a right parenthesis: elseif ($token->operator == SassScriptOperation::$operators[')'][0]) { $parenthesis--; while ($c = count($operatorStack)) { // If the token at the top of the stack is a left parenthesis if ($operatorStack[$c - 1]->operator == SassScriptOperation::$operators['('][0]) { // Pop the left parenthesis from the stack, but not onto the output queue. array_pop($operatorStack); break; } // else pop the operator off the stack onto the output queue. array_push($outputQueue, array_pop($operatorStack)); } // If the stack runs out without finding a left parenthesis // there are mismatched parentheses. if ($c == 0) { throw new SassScriptParserException('Unmatched parentheses', array(), $context->node); } } // the token is an operator, o1, so: else { // while there is an operator, o2, at the top of the stack while ($c = count($operatorStack)) { $operation = $operatorStack[$c - 1]; // if o2 is left parenthesis, or // the o1 has left associativty and greater precedence than o2, or // the o1 has right associativity and lower or equal precedence than o2 if (($operation->operator == SassScriptOperation::$operators['('][0]) || ($token->associativity == 'l' && $token->precedence > $operation->precedence) || ($token->associativity == 'r' && $token->precedence <= $operation->precedence)) { break; // stop checking operators } //pop o2 off the stack and onto the output queue array_push($outputQueue, array_pop($operatorStack)); } // push o1 onto the stack array_push($operatorStack, $token); } } } // When there are no more tokens while ($c = count($operatorStack)) { // While there are operators on the stack: if ($operatorStack[$c - 1]->operator !== SassScriptOperation::$operators['('][0]) { array_push($outputQueue, array_pop($operatorStack)); } else { throw new SassScriptParserException('Unmatched parentheses', array(), $context->node); } } return $outputQueue; }
}