123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192 |
- <?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;
- }
- }
|