diff --git a/src/Compiler.php b/src/Compiler.php index d541207e..8171ff06 100644 --- a/src/Compiler.php +++ b/src/Compiler.php @@ -53,6 +53,11 @@ class Compiler const LINE_COMMENTS = 1; const DEBUG_INFO = 2; + const WITH_RULE = 1; + const WITH_MEDIA = 2; + const WITH_SUPPORTS = 4; + const WITH_ALL = 7; + /** * @var array */ @@ -104,6 +109,8 @@ class Compiler static public $emptyList = array('list', '', array()); static public $emptyMap = array('map', array(), array()); static public $emptyString = array('string', '"', array()); + static public $with = array('keyword', 'with'); + static public $without = array('keyword', 'without'); protected $importPaths = array(''); protected $importCache = array(); @@ -582,20 +589,9 @@ protected function compileDirective($block) */ protected function compileAtRoot($block) { - $env = $this->pushEnv($block); - - $envs = $this->compactEnv($env); - - if (isset($block->with)) { - // @todo move outside of nested directives, e.g., (without: all), (without: media supports), (with: rule) - } else { - // exclude selectors by default - $this->env->parent = $this->rootEnv; - } - - $this->scope = $this->makeOutputBlock('at-root'); - $this->scope->depth = 1; - $this->scope->parent->children[] = $this->scope; + $env = $this->pushEnv($block); + $envs = $this->compactEnv($env); + $without = isset($block->with) ? $this->compileWith($block->with) : self::WITH_RULE; // wrap inline selector if ($block->selector) { @@ -611,14 +607,207 @@ protected function compileAtRoot($block) $block->children = array(array('block', $wrapped)); } - $this->compileChildren($block->children, $this->scope); + $this->env = $this->filterWithout($envs, $without); + $newBlock = $this->spliceTree($envs, $block, $without); - $this->scope = $this->scope->parent; + $saveScope = $this->scope; + $this->scope = $this->rootBlock; + + $this->compileChild($newBlock, $this->scope); + + $this->scope = $saveScope; $this->env = $this->extractEnv($envs); $this->popEnv(); } + /** + * Splice parse tree + * + * @param array $envs + * @param \stdClass $block + * @param integer $without + * + * @return \stdClass + */ + private function spliceTree($envs, $block, $without) + { + $newBlock = null; + + foreach ($envs as $e) { + if (! isset($e->block)) { + continue; + } + + if (isset($e->block) && $e->block === $block) { + continue; + } + + if (isset($e->block->type) && $e->block->type === 'at-root') { + continue; + } + + if (($without & self::WITH_RULE) && isset($e->block->selectors)) { + continue; + } + + if (($without & self::WITH_MEDIA) && + isset($e->block->type) && $e->block->type === 'media' + ) { + continue; + } + + if (($without & self::WITH_SUPPORTS) && + isset($e->block->type) && $e->block->type === 'directive' && + isset($e->block->name) && $e->block->name === 'supports' + ) { + continue; + } + + $b = new \stdClass; + + if (isset($e->block->sourcePosition)) { + $b->sourcePosition = $e->block->sourcePosition; + } + + if (isset($e->block->sourceIndex)) { + $b->sourceIndex = $e->block->sourceIndex; + } + + $b->selectors = array(); + + if (isset($e->block->comments)) { + $b->comments = $e->block->comments; + } + + if (isset($e->block->type)) { + $b->type = $e->block->type; + } + + if (isset($e->block->name)) { + $b->name = $e->block->name; + } + + if (isset($e->block->queryList)) { + $b->queryList = $e->block->queryList; + } + + if (isset($e->block->value)) { + $b->value = $e->block->value; + } + + if ($newBlock) { + $type = isset($newBlock->type) ? $newBlock->type : 'block'; + + $b->children = array(array($type, $newBlock)); + + $newBlock->parent = $b; + } elseif (count($block->children)) { + foreach ($block->children as $child) { + if ($child[0] === 'block') { + $child[1]->parent = $b; + } + } + + $b->children = $block->children; + } + + $b->parent = null; + + $newBlock = $b; + } + + $type = isset($newBlock->type) ? $newBlock->type : 'block'; + + return array($type, $newBlock); + } + + /** + * Compile @at-root's with: inclusion / without: exclusion into filter flags + * + * @param array $with + * + * @return integer + */ + private function compileWith($with) + { + static $mapping = array( + 'rule' => self::WITH_RULE, + 'media' => self::WITH_MEDIA, + 'supports' => self::WITH_SUPPORTS, + 'all' => self::WITH_ALL, + ); + + // exclude selectors by default + $without = self::WITH_RULE; + + if ($this->libMapHasKey(array($with, self::$with))) { + $without = self::WITH_ALL; + + $list = $this->coerceList($this->libMapGet(array($with, self::$with))); + + foreach ($list[2] as $item) { + $keyword = $this->compileStringContent($this->coerceString($item)); + + if (array_key_exists($keyword, $mapping)) { + $without &= ~($mapping[$keyword]); + } + } + } + + if ($this->libMapHasKey(array($with, self::$without))) { + $without = 0; + + $list = $this->coerceList($this->libMapGet(array($with, self::$without))); + + foreach ($list[2] as $item) { + $keyword = $this->compileStringContent($this->coerceString($item)); + + if (array_key_exists($keyword, $mapping)) { + $without |= $mapping[$keyword]; + } + } + } + + return $without; + } + + /** + * Filter env stack + * + * @param array $envs + * @param integer $without + * + * @return \stdClass + */ + private function filterWithout($envs, $without) + { + $filtered = array(); + + foreach ($envs as $e) { + if (($without & self::WITH_RULE) && isset($e->block->selectors)) { + continue; + } + + if (($without & self::WITH_MEDIA) && + isset($e->block->type) && $e->block->type === 'media' + ) { + continue; + } + + if (($without & self::WITH_SUPPORTS) && + isset($e->block->type) && $e->block->type === 'directive' && + isset($e->block->name) && $e->block->name === 'supports' + ) { + continue; + } + + $filtered[] = $e; + } + + return $this->extractEnv($filtered); + } + /** * Compile keyframe block * @@ -4135,9 +4324,7 @@ protected function libQuote($args) protected static $libPercentage = array('value'); protected function libPercentage($args) { - return array('number', - $this->coercePercent($args[0]) * 100, - '%'); + return array('number', $this->coercePercent($args[0]) * 100, '%'); } protected static $libRound = array('value'); @@ -4359,11 +4546,11 @@ protected function libMapHasKey($args) for ($i = count($map[1]) - 1; $i >= 0; $i--) { if ($key === $this->compileStringContent($this->coerceString($map[1][$i]))) { - return self::$true; + return true; } } - return self::$false; + return false; } protected static $libMapMerge = array('map-1', 'map-2'); @@ -4605,13 +4792,13 @@ protected function libFunctionExists($args) // user defined functions if ($this->has(self::$namespaces['function'] . $name)) { - return self::$true; + return true; } $name = $this->normalizeName($name); if (isset($this->userFunctions[$name])) { - return self::$true; + return true; } // built-in functions diff --git a/tests/inputs/at_root.scss b/tests/inputs/at_root.scss index aa4f6584..1f00c727 100644 --- a/tests/inputs/at_root.scss +++ b/tests/inputs/at_root.scss @@ -1,24 +1,149 @@ +.parent-inline-selector { + color: white; + @at-root .child { color: black; } +} + +.parent-block { + color: white; + @at-root { + .child1 { color: green; } + .child2 { color: blue; } + } + .step-child { color: black; } +} + .first { color: red; } body{ -.second { - color: white; + .second { + color: white; - @at-root { - .nested1 { - color: blue; - @at-root { - .nested2 { - color: yellow; + @at-root { + .nested1 { + color: blue; + @at-root { + .nested2 { + color: yellow; + } } + color: orange; } - color: orange; } + color: black; } - color: black; -} } .third { color: green; } + +@media print { + .page { + width: 8in; + @at-root (without: media) { + color: red; + } + } +} + +.my-widget { + @media (min-width: 300px) { + .inside-mq { + inside-style: mq; + } + @at-root (without: media){ + //this without:media tells the screen what to bust out of + .outside-mq { + outside-style: mq; + } + } + // using (without:all) forces all nested selectors outside of all media + // queries, hence color:blue only in the .outside-everything class in generated css + @at-root (without:all){ + .outside-class { + color: blue; + } + } + + // using the (with:media) keeps .with-only inside media query hence .with-only selector + // inside @media ()min-width:300px + @at-root (with: media) { + .with-only { + // do this ONLY inside mq + color: pink; + } + } + } +} + +// without: rule - default - outside of all css rules, but inside directives +@media screen and (max-width:320px) { + .foo { + margin: 0; + + @at-root (without: rule) { + .bar { + padding: 0; + } + } + @at-root (without: media rule) { + .baar { + padding: 0; + } + } + @at-root (without: media) { + .barr { + padding: 0; + } + } + } +} + +// with: rule - outside of all directives but preserve any css rules +@media screen and (max-width:640px) { + .foo { + @supports ( display: flex ) { + @at-root (with: rule) { + .bar { + width: 0; + } + } + @at-root (with: supports) { + .baz { + height: 0; + } + } + @at-root (with: media) { + .qux { + margin: 0; + } + } + @at-root (with: media rule) { + .quux { + padding: 0; + } + } + @at-root (with: rule) { + .quix { + padding: 0; + } + } + } + } +} + +$table-padding: 10px !default; + +@mixin table($padding: $table-padding) { + @at-root { + tbody { + padding: $padding; + } + } +} +.test{ + .test2{ + padding: 0px; + @include table; + } +} diff --git a/tests/outputs/at_root.css b/tests/outputs/at_root.css index 86edb772..5383eaab 100644 --- a/tests/outputs/at_root.css +++ b/tests/outputs/at_root.css @@ -1,15 +1,76 @@ +.parent-inline-selector { + color: white; } + .child { + color: black; } + +.parent-block { + color: white; } + .child1 { + color: green; } + .child2 { + color: blue; } + .parent-block .step-child { + color: black; } + .first { color: red; } body .second { color: white; color: black; } - - .nested1 { - color: blue; - color: orange; } - .nested2 { - color: yellow; } + .nested1 { + color: blue; + color: orange; } + .nested2 { + color: yellow; } .third { color: green; } + +@media print { + .page { + width: 8in; } } + .page { + color: red; } + +@media (min-width: 300px) { + .my-widget .inside-mq { + inside-style: mq; } } + .my-widget .outside-mq { + outside-style: mq; } + .outside-class { + color: blue; } + @media (min-width: 300px) { + .with-only { + color: pink; } } + +@media screen and (max-width: 320px) { + .foo { + margin: 0; } } + @media screen and (max-width: 320px) { + .bar { + padding: 0; } } + .baar { + padding: 0; } + .foo .barr { + padding: 0; } + + + .foo .bar { + width: 0; } + @supports ( display: flex ) { + .baz { + height: 0; } } + @media screen and (max-width: 640px) { + .qux { + margin: 0; } } + @media screen and (max-width: 640px) { + .foo .quux { + padding: 0; } } + .foo .quix { + padding: 0; } + +.test .test2 { + padding: 0px; } + tbody { + padding: 10px; } diff --git a/tests/outputs_numbered/at_root.css b/tests/outputs_numbered/at_root.css index 873218f2..2ac5a07e 100644 --- a/tests/outputs_numbered/at_root.css +++ b/tests/outputs_numbered/at_root.css @@ -1,19 +1,109 @@ /* line 1, inputs/at_root.scss */ +.parent-inline-selector { + color: white; } + /* line 3, inputs/at_root.scss */ + .child { + color: black; } +/* line 6, inputs/at_root.scss */ +.parent-block { + color: white; } + /* line 9, inputs/at_root.scss */ + .child1 { + color: green; } +/* line 10, inputs/at_root.scss */ +.child2 { + color: blue; } +/* line 12, inputs/at_root.scss */ +.parent-block .step-child { + color: black; } +/* line 15, inputs/at_root.scss */ .first { color: red; } -/* line 4, inputs/at_root.scss */ -/* line 5, inputs/at_root.scss */ - body .second { - color: white; - color: black; } - -/* line 9, inputs/at_root.scss */ -.nested1 { -color: blue; -color: orange; } -/* line 12, inputs/at_root.scss */ -.nested2 { -color: yellow; } -/* line 22, inputs/at_root.scss */ +/* line 18, inputs/at_root.scss */ +/* line 19, inputs/at_root.scss */ + +body .second { + color: white; + color: black; } + +/* line 23, inputs/at_root.scss */ + .nested1 { + color: blue; + color: orange; } + /* line 26, inputs/at_root.scss */ + .nested2 { + color: yellow; } +/* line 36, inputs/at_root.scss */ .third { color: green; } + +@media print { +/* line 41, inputs/at_root.scss */ +.page { + width: 8in; } } + +.page { + color: red; } +/* line 49, inputs/at_root.scss */ +@media (min-width: 300px) { + /* line 51, inputs/at_root.scss */ + .my-widget .inside-mq { + inside-style: mq; } } + /* line 56, inputs/at_root.scss */ + .my-widget .outside-mq { + outside-style: mq; } + /* line 63, inputs/at_root.scss */ + .outside-class { + color: blue; } + @media (min-width: 300px) { +/* line 71, inputs/at_root.scss */ +.with-only { +color: pink; } } + +@media screen and (max-width: 320px) { +/* line 81, inputs/at_root.scss */ +.foo { + margin: 0; } } + +@media screen and (max-width: 320px) { +/* line 85, inputs/at_root.scss */ +.bar { + padding: 0; } } + +/* line 90, inputs/at_root.scss */ + .baar { + padding: 0; } + +/* line 95, inputs/at_root.scss */ + .foo .barr { + padding: 0; } + +@media screen and (max-width: 640px) { +/* line 104, inputs/at_root.scss */ } + /* line 107, inputs/at_root.scss */ + .foo .bar { + width: 0; } + @supports ( display: flex ) { +/* line 112, inputs/at_root.scss */ +.baz { +height: 0; } } + @media screen and (max-width: 640px) { +/* line 117, inputs/at_root.scss */ +.qux { +margin: 0; } } + @media screen and (max-width: 640px) { + /* line 122, inputs/at_root.scss */ + .foo .quux { + padding: 0; } } + /* line 127, inputs/at_root.scss */ + .foo .quix { + padding: 0; } +/* line 144, inputs/at_root.scss */ +/* line 145, inputs/at_root.scss */ + +.test .test2 { + padding: 0px; } + +/* line 139, inputs/at_root.scss */ + tbody { + padding: 10px; }