You are here

SassRuleNode.php in Sassy 7.3

File

phpsass/tree/SassRuleNode.php
View source
<?php

/* SVN FILE: $Id$ */

/**
 * SassRuleNode 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.tree
 */

/**
 * SassRuleNode class.
 * Represents a CSS rule.
 * @package      PHamlP
 * @subpackage  Sass.tree
 */
class SassRuleNode extends SassNode {
  const MATCH = '/^(.+?)(?:\\s*\\{)?$/';
  const SELECTOR = 1;
  const CONTINUED = ',';

  /**
   * @const string that is replaced with the parent node selector
   */
  const PARENT_REFERENCE = '&';

  /**
   * @var array selector(s)
   */
  private $selectors = array();

  /**
   * @var array parent selectors
   */
  private $parentSelectors = array();

  /**
   * @var array resolved selectors
   */
  private $resolvedSelectors = array();

  /**
   * @var boolean whether the node expects more selectors
   */
  private $isContinued;

  /**
   * SassRuleNode constructor.
   * @param object source token
   * @param string rule selector
   * @return SassRuleNode
   */
  public function __construct($token) {
    parent::__construct($token);
    preg_match(self::MATCH, $token->source, $matches);
    $this
      ->addSelectors($matches[SassRuleNode::SELECTOR]);
  }

  /**
   * Adds selector(s) to the rule.
   * If the selectors are to continue for the rule the selector must end in a comma
   * @param string selector
   */
  public function addSelectors($selectors) {
    $this->isContinued = substr($selectors, -1) === self::CONTINUED;
    $this->selectors = array_merge($this->selectors, $this
      ->explode($selectors));
  }

  /**
   * Returns a value indicating if the selectors for this rule are to be continued.
   * @param boolean true if the selectors for this rule are to be continued,
   * false if not
   */
  public function getIsContinued() {
    return $this->isContinued;
  }

  /**
   * Parse this node and its children into static nodes.
   * @param SassContext the context in which this node is parsed
   * @return array the parsed node and its children
   */
  public function parse($context) {
    $node = clone $this;
    $node->selectors = $this
      ->resolveSelectors($context);
    $node->children = $this
      ->parseChildren($context);
    return array(
      $node,
    );
  }

  /**
   * Render this node and its children to CSS.
   * @return string the rendered node
   */
  public function render() {
    $this
      ->extend();
    $rules = '';
    $properties = array();
    foreach ($this->children as $child) {
      $child->parent = $this;
      if ($child instanceof SassRuleNode) {
        $rules .= $child
          ->render();
      }
      else {
        $properties[] = $child
          ->render();
      }
    }

    // foreach
    return $this->renderer
      ->renderRule($this, $properties, $rules);
  }

  /**
   * Extend this nodes selectors
   * $extendee is the subject of the @extend directive
   * $extender is the selector that contains the @extend directive
   * $selector a selector or selector sequence that is to be extended
   */
  public function extend() {
    foreach ($this->root->extenders as $extendee => $extenders) {
      if ($this
        ->isPsuedo($extendee)) {
        $extendee = explode(':', $extendee);
        $pattern = preg_quote($extendee[0]) . '((\\.[-\\w]+)*):' . preg_quote($extendee[1]);
      }
      else {
        $pattern = preg_quote($extendee);
      }
      foreach (preg_grep('/' . $pattern . '/', $this->selectors) as $selector) {
        foreach ($extenders as $extender) {
          if (is_array($extendee)) {
            $this->selectors[] = preg_replace('/(.*?)' . $pattern . '$/', "\\1{$extender}\\2", $selector);
          }
          elseif ($this
            ->isSequence($extender) || $this
            ->isSequence($selector)) {
            $this->selectors = array_merge($this->selectors, $this
              ->mergeSequence($extender, $selector));
          }
          else {
            $this->selectors[] = str_replace($extendee, $extender, $selector);
          }
        }
      }
      $this->selectors = array_unique($this->selectors);
    }
  }

  /**
   * Tests whether the selector is a psuedo selector
   * @param string selector to test
   * @return boolean true if the selector is a psuedo selector, false if not
   */
  private function isPsuedo($selector) {
    return strpos($selector, ':') !== false;
  }

  /**
   * Tests whether the selector is a sequence selector
   * @param string selector to test
   * @return boolean true if the selector is a sequence selector, false if not
   */
  private function isSequence($selector) {
    return strpos($selector, ' ') !== false;
  }

  /**
   * Merges selector sequences
   * @param string the extender selector
   * @param string selector to extend
   * @return array the merged sequences
   */
  private function mergeSequence($extender, $selector) {
    $extender = explode(' ', $extender);
    $end = array_pop($extender);
    $selector = explode(' ', $selector);
    array_pop($selector);
    $common = array();
    if (count($extender) && count($selector)) {
      while (trim($extender[0]) === trim($selector[0])) {
        $common[] = array_shift($selector);
        array_shift($extender);
        if (!count($extender)) {
          break;
        }
      }
    }
    $begining = !empty($common) ? join(' ', $common) . ' ' : '';

    # Richard Lyon - 2011-10-25 - removes duplicates by uniquing and trimming.

    # regex removes whitespace from start and and end of string as well as removing

    # whitespace following whitespace. slightly quicker than a trim and simpler replace
    return array_unique(array(
      preg_replace('/(^\\s+|(\\s)\\s+|\\s+$)/', '$2', $begining . join(' ', $selector) . ' ' . join(' ', $extender) . ' ' . $end),
      preg_replace('/(^\\s+|(\\s)\\s+|\\s+$)/', '$2', $begining . join(' ', $extender) . ' ' . join(' ', $selector) . ' ' . $end),
    ));
  }

  /**
   * Returns the selectors
   * @return array selectors
   */
  public function getSelectors() {
    return $this->selectors;
  }

  /**
   * Resolves selectors.
   * Interpolates SassScript in selectors and resolves any parent references or
   * appends the parent selectors.
   * @param SassContext the context in which this node is parsed
   */
  public function resolveSelectors($context) {
    $resolvedSelectors = array();
    $this->parentSelectors = $this
      ->getParentSelectors($context);
    foreach ($this->selectors as $key => $selector) {
      $selector = $this
        ->interpolate($selector, $context);

      //$selector = $this->evaluate($this->interpolate($selector, $context), $context)->toString();
      if ($this
        ->hasParentReference($selector)) {
        $resolvedSelectors = array_merge($resolvedSelectors, $this
          ->resolveParentReferences($selector, $context));
      }
      elseif ($this->parentSelectors) {
        foreach ($this->parentSelectors as $parentSelector) {
          $resolvedSelectors[] = "{$parentSelector} {$selector}";
        }

        // foreach
      }
      else {
        $resolvedSelectors[] = $selector;
      }
    }

    // foreach
    sort($resolvedSelectors);
    return $resolvedSelectors;
  }

  /**
   * Returns the parent selector(s) for this node.
   * This in an empty array if there is no parent selector.
   * @return array the parent selector for this node
   */
  protected function getParentSelectors($context) {
    $ancestor = $this->parent;
    while (!$ancestor instanceof SassRuleNode && $ancestor
      ->hasParent()) {
      $ancestor = $ancestor->parent;
    }
    if ($ancestor instanceof SassRuleNode) {
      return $ancestor
        ->resolveSelectors($context);
    }
    return array();
  }

  /**
   * Returns the position of the first parent reference in the selector.
   * If there is no parent reference in the selector this function returns
   * boolean FALSE.
   * Note that the return value may be non-Boolean that evaluates to FALSE,
   * i.e. 0. The return value should be tested using the === operator.
   * @param string selector to test
   * @return mixed integer: position of the the first parent reference,
   * boolean: false if there is no parent reference.
   */
  private function parentReferencePos($selector) {
    $inString = '';
    for ($i = 0, $l = strlen($selector); $i < $l; $i++) {
      $c = $selector[$i];
      if ($c === self::PARENT_REFERENCE && empty($inString)) {
        return $i;
      }
      elseif (empty($inString) && ($c === '"' || $c === "'")) {
        $inString = $c;
      }
      elseif ($c === $inString) {
        $inString = '';
      }
    }
    return false;
  }

  /**
   * Determines if there is a parent reference in the selector
   * @param string selector
   * @return boolean true if there is a parent reference in the selector
   */
  private function hasParentReference($selector) {
    return $this
      ->parentReferencePos($selector) !== false;
  }

  /**
   * Resolves parent references in the selector
   * @param string selector
   * @return string selector with parent references resolved
   */
  private function resolveParentReferences($selector, $context) {
    $resolvedReferences = array();
    if (!count($this->parentSelectors)) {
      throw new SassRuleNodeException('Can not use parent selector (' . self::PARENT_REFERENCE . ') when no parent selectors', $this);
    }
    foreach ($this
      ->getParentSelectors($context) as $parentSelector) {
      $resolvedReferences[] = str_replace(self::PARENT_REFERENCE, $parentSelector, $selector);
    }
    return $resolvedReferences;
  }

  /**
   * Explodes a string of selectors into an array.
   * We can't use PHP::explode as this will potentially explode attribute
   * matches in the selector, e.g. div[title="some,value"] and interpolations.
   * @param string selectors
   * @return array selectors
   */
  private function explode($string) {
    $selectors = array();
    $inString = false;
    $interpolate = false;
    $selector = '';
    for ($i = 0, $l = strlen($string); $i < $l; $i++) {
      $c = $string[$i];
      if ($c === self::CONTINUED && !$inString && !$interpolate) {
        $selectors[] = trim($selector);
        $selector = '';
      }
      else {
        $selector .= $c;
        if ($c === '"' || $c === "'") {
          do {
            $_c = $string[++$i];
            $selector .= $_c;
          } while ($_c !== $c);
        }
        elseif ($c === '#' && $string[$i + 1] === '{') {
          do {
            $c = $string[++$i];
            $selector .= $c;
          } while ($c !== '}');
        }
      }
    }
    if (!empty($selector)) {
      $selectors[] = trim($selector);
    }
    return $selectors;
  }

}

Classes

Namesort descending Description
SassRuleNode SassRuleNode class. Represents a CSS rule. @package PHamlP @subpackage Sass.tree