<?php /* SVN FILE: $Id: SassRuleNode.php 118 2010-09-21 09:45:11Z chris.l.yates@gmail.com $ */ /**

* 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);
                                }
                        }
                }
        }
}

/**
 * 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();
        while($extender[0] === $selector[0]) {
                $common[] = array_shift($selector);
                array_shift($extender);
        }

        $begining = (!empty($common) ? join(' ', $common) . ' ' : '');

        return array(
                $begining.join(' ', $selector).' '.join(' ', $extender).$end,
                $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', array(), $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;
}

}