SassScriptParser.php 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192
  1. <?php
  2. /* SVN FILE: $Id: SassScriptParser.php 118 2010-09-21 09:45:11Z chris.l.yates@gmail.com $ */
  3. /**
  4. * SassScriptParser class file.
  5. * @author Chris Yates <chris.l.yates@gmail.com>
  6. * @copyright Copyright (c) 2010 PBM Web Development
  7. * @license http://phamlp.googlecode.com/files/license.txt
  8. * @package PHamlP
  9. * @subpackage Sass.script
  10. */
  11. require_once('SassScriptLexer.php');
  12. require_once('SassScriptParserExceptions.php');
  13. /**
  14. * SassScriptParser class.
  15. * Parses SassScript. SassScript is lexed into {@link http://en.wikipedia.org/wiki/Reverse_Polish_notation Reverse Polish notation} by the SassScriptLexer and
  16. * the calculated result returned.
  17. * @package PHamlP
  18. * @subpackage Sass.script
  19. */
  20. class SassScriptParser {
  21. const MATCH_INTERPOLATION = '/(?<!\\\\)#\{(.*?)\}/';
  22. const DEFAULT_ENV = 0;
  23. const CSS_RULE = 1;
  24. const CSS_PROPERTY = 2;
  25. /**
  26. * @var SassContext Used for error reporting
  27. */
  28. public static $context;
  29. /**
  30. * @var SassScriptLexer the lexer object
  31. */
  32. private $lexer;
  33. /**
  34. * SassScriptParser constructor.
  35. * @return SassScriptParser
  36. */
  37. public function __construct() {
  38. $this->lexer = new SassScriptLexer($this);
  39. }
  40. /**
  41. * Replace interpolated SassScript contained in '#{}' with the parsed value.
  42. * @param string the text to interpolate
  43. * @param SassContext the context in which the string is interpolated
  44. * @return string the interpolated text
  45. */
  46. public function interpolate($string, $context) {
  47. for ($i = 0, $n = preg_match_all(self::MATCH_INTERPOLATION, $string, $matches);
  48. $i < $n; $i++) {
  49. $matches[1][$i] = $this->evaluate($matches[1][$i], $context)->toString();
  50. }
  51. return str_replace($matches[0], $matches[1], $string);
  52. }
  53. /**
  54. * Evaluate a SassScript.
  55. * @param string expression to parse
  56. * @param SassContext the context in which the expression is evaluated
  57. * @param integer the environment in which the expression is evaluated
  58. * @return SassLiteral parsed value
  59. */
  60. public function evaluate($expression, $context, $environment=self::DEFAULT_ENV) {
  61. self::$context = $context;
  62. $operands = array();
  63. $tokens = $this->parse($expression, $context, $environment);
  64. while (count($tokens)) {
  65. $token = array_shift($tokens);
  66. if ($token instanceof SassScriptFunction) {
  67. array_push($operands, $token->perform());
  68. }
  69. elseif ($token instanceof SassLiteral) {
  70. if ($token instanceof SassString) {
  71. $token = new SassString($this->interpolate($token->toString(), self::$context));
  72. }
  73. array_push($operands, $token);
  74. }
  75. else {
  76. $args = array();
  77. for ($i = 0, $c = $token->operandCount; $i < $c; $i++) {
  78. $args[] = array_pop($operands);
  79. }
  80. array_push($operands, $token->perform($args));
  81. }
  82. }
  83. return array_shift($operands);
  84. }
  85. /**
  86. * Parse SassScript to a set of tokens in RPN
  87. * using the Shunting Yard Algorithm.
  88. * @param string expression to parse
  89. * @param SassContext the context in which the expression is parsed
  90. * @param integer the environment in which the expression is parsed
  91. * @return array tokens in RPN
  92. */
  93. public function parse($expression, $context, $environment=self::DEFAULT_ENV) {
  94. $outputQueue = array();
  95. $operatorStack = array();
  96. $parenthesis = 0;
  97. $tokens = $this->lexer->lex($expression, $context);
  98. foreach($tokens as $i=>$token) {
  99. // If two literals/expessions are seperated by whitespace use the concat operator
  100. if (empty($token)) {
  101. if ($i > 0 && (!$tokens[$i-1] instanceof SassScriptOperation || $tokens[$i-1]->operator === SassScriptOperation::$operators[')'][0]) &&
  102. (!$tokens[$i+1] instanceof SassScriptOperation || $tokens[$i+1]->operator === SassScriptOperation::$operators['('][0])) {
  103. $token = new SassScriptOperation(SassScriptOperation::$defaultOperator, $context);
  104. }
  105. else {
  106. continue;
  107. }
  108. }
  109. elseif ($token instanceof SassScriptVariable) {
  110. $token = $token->evaluate($context);
  111. $environment = self::DEFAULT_ENV;
  112. }
  113. // If the token is a number or function add it to the output queue.
  114. if ($token instanceof SassLiteral || $token instanceof SassScriptFunction) {
  115. if ($environment === self::CSS_PROPERTY && $token instanceof SassNumber && !$parenthesis) {
  116. $token->inExpression = false;
  117. }
  118. array_push($outputQueue, $token);
  119. }
  120. // If the token is an operation
  121. elseif ($token instanceof SassScriptOperation) {
  122. // If the token is a left parenthesis push it onto the stack.
  123. if ($token->operator == SassScriptOperation::$operators['('][0]) {
  124. array_push($operatorStack, $token);
  125. $parenthesis++;
  126. }
  127. // If the token is a right parenthesis:
  128. elseif ($token->operator == SassScriptOperation::$operators[')'][0]) {
  129. $parenthesis--;
  130. while ($c = count($operatorStack)) {
  131. // If the token at the top of the stack is a left parenthesis
  132. if ($operatorStack[$c - 1]->operator == SassScriptOperation::$operators['('][0]) {
  133. // Pop the left parenthesis from the stack, but not onto the output queue.
  134. array_pop($operatorStack);
  135. break;
  136. }
  137. // else pop the operator off the stack onto the output queue.
  138. array_push($outputQueue, array_pop($operatorStack));
  139. }
  140. // If the stack runs out without finding a left parenthesis
  141. // there are mismatched parentheses.
  142. if ($c == 0) {
  143. throw new SassScriptParserException('Unmatched parentheses', array(), $context->node);
  144. }
  145. }
  146. // the token is an operator, o1, so:
  147. else {
  148. // while there is an operator, o2, at the top of the stack
  149. while ($c = count($operatorStack)) {
  150. $operation = $operatorStack[$c - 1];
  151. // if o2 is left parenthesis, or
  152. // the o1 has left associativty and greater precedence than o2, or
  153. // the o1 has right associativity and lower or equal precedence than o2
  154. if (($operation->operator == SassScriptOperation::$operators['('][0]) ||
  155. ($token->associativity == 'l' && $token->precedence > $operation->precedence) ||
  156. ($token->associativity == 'r' && $token->precedence <= $operation->precedence)) {
  157. break; // stop checking operators
  158. }
  159. //pop o2 off the stack and onto the output queue
  160. array_push($outputQueue, array_pop($operatorStack));
  161. }
  162. // push o1 onto the stack
  163. array_push($operatorStack, $token);
  164. }
  165. }
  166. }
  167. // When there are no more tokens
  168. while ($c = count($operatorStack)) { // While there are operators on the stack:
  169. if ($operatorStack[$c - 1]->operator !== SassScriptOperation::$operators['('][0]) {
  170. array_push($outputQueue, array_pop($operatorStack));
  171. }
  172. else {
  173. throw new SassScriptParserException('Unmatched parentheses', array(), $context->node);
  174. }
  175. }
  176. return $outputQueue;
  177. }
  178. }