View source
<?php
class SassRuleNode extends SassNode {
const MATCH = '/^(.+?)(?:\\s*\\{)?$/';
const SELECTOR = 1;
const CONTINUED = ',';
const PARENT_REFERENCE = '&';
private $selectors = array();
private $parentSelectors = array();
private $resolvedSelectors = array();
private $isContinued;
public function __construct($token) {
parent::__construct($token);
preg_match(self::MATCH, $token->source, $matches);
$this
->addSelectors($matches[SassRuleNode::SELECTOR]);
}
public function addSelectors($selectors) {
$this->isContinued = substr($selectors, -1) === self::CONTINUED;
$this->selectors = array_merge($this->selectors, $this
->explode($selectors));
}
public function getIsContinued() {
return $this->isContinued;
}
public function parse($context) {
$node = clone $this;
$node->selectors = $this
->resolveSelectors($context);
$node->children = $this
->parseChildren($context);
return array(
$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();
}
}
return $this->renderer
->renderRule($this, $properties, $rules);
}
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);
}
}
}
}
}
private function isPsuedo($selector) {
return strpos($selector, ':') !== false;
}
private function isSequence($selector) {
return strpos($selector, ' ') !== false;
}
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,
);
}
public function getSelectors() {
return $this->selectors;
}
public function resolveSelectors($context) {
$resolvedSelectors = array();
$this->parentSelectors = $this
->getParentSelectors($context);
foreach ($this->selectors as $key => $selector) {
$selector = $this
->interpolate($selector, $context);
if ($this
->hasParentReference($selector)) {
$resolvedSelectors = array_merge($resolvedSelectors, $this
->resolveParentReferences($selector, $context));
}
elseif ($this->parentSelectors) {
foreach ($this->parentSelectors as $parentSelector) {
$resolvedSelectors[] = "{$parentSelector} {$selector}";
}
}
else {
$resolvedSelectors[] = $selector;
}
}
sort($resolvedSelectors);
return $resolvedSelectors;
}
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();
}
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;
}
private function hasParentReference($selector) {
return $this
->parentReferencePos($selector) !== false;
}
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;
}
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;
}
}