diff --git a/src/Compiler.php b/src/Compiler.php index 637f1c1c..4d37bbf7 100644 --- a/src/Compiler.php +++ b/src/Compiler.php @@ -775,6 +775,11 @@ protected function compileDirective(Block $block) */ protected function compileAtRoot(Block $block) { + + if (!$block->selector) { + $storeEnv = $this->getStoreEnv(); + } + $env = $this->pushEnv($block); $envs = $this->compactEnv($env); $without = isset($block->with) ? $this->compileWith($block->with) : static::WITH_RULE; @@ -786,12 +791,19 @@ protected function compileAtRoot(Block $block) $wrapped->sourceIndex = $block->sourceIndex; $wrapped->sourceLine = $block->sourceLine; $wrapped->sourceColumn = $block->sourceColumn; - $wrapped->selectors = $block->selector; + $wrapped->selectors = $this->preParseSelectors($this->evalSelectors($block->selector), $env); $wrapped->comments = []; $wrapped->parent = $block; $wrapped->children = $block->children; + $block->children = [[Type::T_BLOCK, $wrapped]]; + } else { + + foreach($block->children as $childIndex => $child) { + if ($child[0] === Type::T_BLOCK) { + $block->children[$childIndex][1]->selectors = $this->preParseSelectors($child[1]->selectors, $env); + } + } - $block->children = [[Type::T_BLOCK, $wrapped]]; } $this->env = $this->filterWithout($envs, $without); @@ -808,6 +820,61 @@ protected function compileAtRoot(Block $block) $this->popEnv(); } + /** + * Reconstitute selectors + * + * @param array $selectors + * + * @return array + */ + private function preParseSelectors($selectors, $env) + { + $newSelectors = []; + $injectIteration = 0; + + foreach($selectors as $key1 => $branch) { + if ($this->checkForSelf($branch)) { + foreach ($this->multiplySelectors($env) as $selectorPart) { + $newSelectors[$injectIteration] = $branch; + foreach($branch as $key2 => $leaf) { + if ($leaf === [static::$selfSelector]) { + $newSelectors[$injectIteration][$key2] = [$this->collapseSelectors([$selectorPart])]; + continue; + } + } + ++$injectIteration; + } + } else { + $newSelectors[$injectIteration] = $branch; + ++$injectIteration; + } + } + + return $newSelectors; + } + + /** + * Determine if a self(&) declaration is part of the selector part + * + * @param array $branch + * + * @return boolean + */ + private function checkForSelf($branch) + { + $hasSelf = false; + array_walk_recursive( + $branch, + function ($value, $key) use (&$hasSelf) { + if ($value === Type::T_SELF) { + $hasSelf = true; + return; + } + } + ); + return $hasSelf; + } + /** * Splice parse tree * @@ -1150,12 +1217,13 @@ protected function evalSelector($selector) * * @return array */ + protected function evalSelectorPart($part) { foreach ($part as &$p) { if (is_array($p) && ($p[0] === Type::T_INTERPOLATE || $p[0] === Type::T_STRING)) { $p = $this->compileValue($p); - + // force re-evaluation if (strpos($p, '&') !== false || strpos($p, ',') !== false) { $this->shouldEvaluate = true; @@ -1182,6 +1250,22 @@ protected function collapseSelectors($selectors) { $parts = []; + foreach ($selectors as $selectorColumn) { + $output = ''; + $rows = []; + + foreach($selectorColumn as $selectorRow) { + $rows[] = $this->collapseSelectorPart($selectorRow); + } + + $output .= implode(" ", $rows); + $parts[] = $output; + } + + return implode(', ', $parts); + + /* NOTE: Retaining for potential reversion + foreach ($selectors as $selector) { $output = ''; @@ -1196,6 +1280,35 @@ function ($value, $key) use (&$output) { } return implode(', ', $parts); + + */ + } + + /** + * Collapse a part of a selector + * + * @param array $part + * + * @return array + */ + protected function collapseSelectorPart($part) + { + $output = ""; + + array_walk_recursive( + $part, + function ($value, $key) use (&$output) { + if ($value === Type::T_SELF) { + + $output .= "&"; + + } else { + $output .= $value; + } + } + ); + + return $output; } /** @@ -1259,6 +1372,7 @@ protected function compileSelector($selector) */ protected function compileSelectorPart($piece) { + foreach ($piece as &$p) { if (! is_array($p)) { continue; @@ -1293,7 +1407,7 @@ protected function hasSelectorPlaceholder($selector) foreach ($selector as $parts) { foreach ($parts as $part) { - if (strlen($part) && '%' === $part[0]) { + if (is_string($part) && strlen($part) && '%' === $part[0]) { return true; } } @@ -1626,6 +1740,28 @@ protected function compileChild($child, OutputBlock $out) $isDefault = in_array('!default', $flags); $isGlobal = in_array('!global', $flags); + if ($value[0] === Type::T_SELF) { + $env = $this->env; + + if ($env !== null) { + $selfSelectors = array(); + foreach ($this->multiplySelectors($env) as $selectorPart) { + $selfSelectors[] = $this->collapseSelectors([$selectorPart]); + } + } + + $child[2][0] = $value[0] = Type::T_LIST; + $child[2][1] = $value[1] = ","; + $temp = array(); + + foreach($selfSelectors as $singleSelector) { + $temp[] = array(Type::T_KEYWORD,trim($singleSelector)); + } + + $child[2][2] = $value[2] = $temp; + } + + if ($isGlobal) { $this->set($name[1], $this->reduce($value), false, $this->rootEnv); break; @@ -1890,7 +2026,7 @@ protected function compileChild($child, OutputBlock $out) case Type::T_MIXIN_CONTENT: $content = $this->get(static::$namespaces['special'] . 'content', false, $this->getStoreEnv()) - ?: $this->get(static::$namespaces['special'] . 'content', false, $this->env); + ?: $this->get(static::$namespaces['special'] . 'content', false, $this->env); if (! $content) { $content = new \stdClass(); @@ -2783,6 +2919,23 @@ public function compileValue($value) // raw parse node list(, $exp) = $value; + if ($value[1][0] == Type::T_SELF) { + $env = $this->env; + + if ($env === null) return false; + $selfSelectors = array(); + foreach ($this->multiplySelectors($env) as $selectorPart) { + $selfSelectors[] = $this->collapseSelectors([$selectorPart]); + } + + $temp = array(); + foreach($selfSelectors as $singleSelector) { + $temp[] = array(Type::T_STRING,"'",[trim($singleSelector)]); + } + $exp = [Type::T_LIST,",",$temp]; + + } + // strip quotes if it's a string $reduced = $this->reduce($exp); @@ -2928,7 +3081,7 @@ protected function multiplySelectors(Environment $env) } /** - * Join selectors; looks for & to replace, or append parent before child + * Join selectors; looks for self(&) to replace, or append parent before child * * @param array $parent * @param array $child @@ -3184,7 +3337,8 @@ public function get($name, $shouldThrow = true, Environment $env = null) continue; } - $env = $this->rootEnv; + //$env = $this->rootEnv; + $env = $env->parent; continue; } diff --git a/src/Parser.php b/src/Parser.php index 748d38ae..8012eed3 100644 --- a/src/Parser.php +++ b/src/Parser.php @@ -60,6 +60,7 @@ class Parser private $count; private $env; private $inParens; + private $inNot; private $eatWhiteDefault; private $buffer; private $utf8; @@ -607,6 +608,12 @@ protected function parseChunk() $this->valueList($value) && $this->end() ) { + + if ($this->env->parent == NULL || ($this->env->type == Type::T_AT_ROOT)) { + $this->seek($s); + $this->throwParseError('property declarations must be inside a block'); + } + $name = [Type::T_STRING, '', [$name]]; $this->append([Type::T_ASSIGN, $name, $value], $s); @@ -1211,32 +1218,63 @@ protected function expression(&$out) { $s = $this->seek(); - if ($this->literal('(')) { - if ($this->literal(')')) { - $out = [Type::T_LIST, '', []]; - - return true; + if ($this->literal('&',false)) { + if ($this->literal('&',false)) { + $this->throwParseError('"&" may only be used at the beginning of a compound selector.'); + return false; } + //Assign parent selectors here + $out = Compiler::$selfSelector; + return true; + } + + $this->seek($s); - if ($this->valueList($out) && $this->literal(')') && $out[0] === Type::T_LIST) { + if ($this->literal('(')) { + if ($this->parenExpression($out, $s)) { + return true; } - $this->seek($s); + } - if ($this->map($out)) { - return true; - } + if ($this->value($lhs)) { + $out = $this->expHelper($lhs, 0); - $this->seek($s); + return true; } - if ($this->value($lhs)) { - $out = $this->expHelper($lhs, 0); + return false; + } + + /** + * Parse expression specifically checking for lists in parenthesis or brackets + * + * @param array $out + * @param integer $s + * @param string $closingParen + * + * @return boolean + */ + protected function parenExpression(&$out, $s, $closingParen = ")") + { + + if ($this->literal($closingParen)) { + $out = [Type::T_LIST, '', []]; + return true; + } + if ($this->valueList($out) && $this->literal($closingParen) && $out[0] === Type::T_LIST) { return true; } + $this->seek($s); + + if ($this->map($out)) { + return true; + } + + return false; } @@ -1354,7 +1392,7 @@ protected function value(&$out) $this->seek($s); - if ($this->parenValue($out) || + if ($this->parenOrBracketValue($out) || $this->interpolation($out) || $this->variable($out) || $this->color($out) || @@ -1412,6 +1450,42 @@ protected function parenValue(&$out) $this->inParens = $inParens; $this->seek($s); + return false; + + } + + /** + * Parse parenthesized value + * + * @param array $out + * + * @return boolean + */ + protected function parenOrBracketValue(&$out) + { + + if ($this->parenValue($out)) return true; + + if ($this->literal('[')) { + if ($this->literal(']')) { + $out = [Type::T_LIST, '', []]; + + return true; + } + + $this->inParens = true; + + if ($this->expression($exp) && $this->literal(']')) { + $out = $exp; + $this->inParens = $inParens; + + return true; + } + } + + $this->inParens = $inParens; + $this->seek($s); + return false; } @@ -2007,6 +2081,7 @@ protected function selectors(&$out) while ($this->literal(',')) { ; // ignore extra } + $this->match('\s+', $m); } if (count($selectors) === 0) { @@ -2034,6 +2109,7 @@ protected function selector(&$out) for (;;) { if ($this->match('[>+~]+', $m)) { $selector[] = [$m[0]]; + $this->match('\s+', $m); continue; } @@ -2092,6 +2168,10 @@ protected function selectorSingle(&$out) // self if ($this->literal('&', false)) { + if ($this->literal('&',false)) { + $this->throwParseError('"&" may only be used at the beginning of a compound selector.'); + return false; + } $parts[] = Compiler::$selfSelector; continue; } @@ -2148,6 +2228,44 @@ protected function selectorSingle(&$out) $ss = $this->seek(); + + // negation selector + if (in_array('not',$nameParts)) { + + if ($this->inNot === true) { + $this->inNot = false; + $this->throwParseError('psuedo-selector :not() may not contain another :not() psuedo-selector'); + } + + $this->inNot = true; + + if ($this->literal('(') && + //$this->selectors($selectors) && + $this->openString(')', $str, '(') && + $this->literal(')') + ) { + + $parts[] = '('; + + $strParts = explode("&",$str[2][0]); + $strPartsLength = count($strParts); + foreach($strParts as $index => $strTok) { + $parts[] = $strTok; + if ($index + 1 < $strPartsLength) { + $parts[] = Compiler::$selfSelector; + } + } + + $parts[] = ')'; + $this->inNot = false; + continue; + } + + $this->seek($ss); + } + + + if ($this->literal('(') && ($this->openString(')', $str, '(') || true) && $this->literal(')')