MDL-65761 lib: Update scssphp to 1.0.2
authorMathew May <mathewm@hotmail.co.nz>
Tue, 16 Jul 2019 01:29:37 +0000 (09:29 +0800)
committerMathew May <mathewm@hotmail.co.nz>
Tue, 16 Jul 2019 01:29:37 +0000 (09:29 +0800)
lib/scssphp/Cache.php
lib/scssphp/Compiler.php
lib/scssphp/Formatter.php
lib/scssphp/Formatter/Nested.php
lib/scssphp/Node/Number.php
lib/scssphp/Parser.php
lib/scssphp/Version.php
lib/thirdpartylibs.xml
lib/upgrade.txt

index 3311264..1cf496f 100644 (file)
@@ -32,7 +32,7 @@ use Exception;
  */
 class Cache
 {
-    const CACHE_VERSION = 0;
+    const CACHE_VERSION = 1;
 
     // directory used for storing data
     public static $cacheDir = false;
@@ -41,12 +41,12 @@ class Cache
     public static $prefix = 'scssphp_';
 
     // force a refresh : 'once' for refreshing the first hit on a cache only, true to never use the cache in this hit
-    public static $forceFefresh = false;
+    public static $forceRefresh = false;
 
     // specifies the number of seconds after which data cached will be seen as 'garbage' and potentially cleaned up
     public static $gcLifetime = 604800;
 
-    // array of already refreshed cache if $forceFefresh==='once'
+    // array of already refreshed cache if $forceRefresh==='once'
     protected static $refreshed = [];
 
     /**
@@ -74,7 +74,7 @@ class Cache
         }
 
         if (isset($options['forceRefresh'])) {
-            self::$forceFefresh = $options['force_refresh'];
+            self::$forceRefresh = $options['force_refresh'];
         }
 
         self::checkCacheDir();
@@ -97,13 +97,13 @@ class Cache
     {
         $fileCache = self::$cacheDir . self::cacheName($operation, $what, $options);
 
-        if ((! self::$forceRefresh || (self::$forceRefresh === 'once' && isset(self::$refreshed[$fileCache])))
-            && file_exists($fileCache)
+        if ((! self::$forceRefresh || (self::$forceRefresh === 'once' &&
+            isset(self::$refreshed[$fileCache]))) && file_exists($fileCache)
         ) {
             $cacheTime = filemtime($fileCache);
 
-            if ((is_null($lastModified) || $cacheTime > $lastModified)
-                && $cacheTime + self::$gcLifetime > time()
+            if ((is_null($lastModified) || $cacheTime > $lastModified) &&
+                $cacheTime + self::$gcLifetime > time()
             ) {
                 $c = file_get_contents($fileCache);
                 $c = unserialize($c);
index 3446aea..8f26740 100644 (file)
@@ -203,19 +203,14 @@ class Compiler
     public function compile($code, $path = null)
     {
         if ($this->cache) {
-            $cacheKey = ($path ? $path : "(stdin)") . ":" . md5($code);
+            $cacheKey       = ($path ? $path : "(stdin)") . ":" . md5($code);
             $compileOptions = $this->getCompileOptions();
-            $cache = $this->cache->getCache("compile", $cacheKey, $compileOptions);
+            $cache          = $this->cache->getCache("compile", $cacheKey, $compileOptions);
 
-            if (is_array($cache)
-                && isset($cache['dependencies'])
-                && isset($cache['out'])
-            ) {
+            if (is_array($cache) && isset($cache['dependencies']) && isset($cache['out'])) {
                 // check if any dependency file changed before accepting the cache
                 foreach ($cache['dependencies'] as $file => $mtime) {
-                    if (! file_exists($file)
-                        || filemtime($file) !== $mtime
-                    ) {
+                    if (! file_exists($file) || filemtime($file) !== $mtime) {
                         unset($cache);
                         break;
                     }
@@ -242,7 +237,7 @@ class Compiler
         $this->stderr         = fopen('php://stderr', 'w');
 
         $this->parser = $this->parserFactory($path);
-        $tree = $this->parser->parse($code);
+        $tree         = $this->parser->parse($code);
         $this->parser = null;
 
         $this->formatter = new $this->formatter();
@@ -503,8 +498,8 @@ class Compiler
             } else {
                 // a selector part finishing with a ) is the last part of a :not( or :nth-child(
                 // and need to be joined to this
-                if (count($new) && is_string($new[count($new) - 1])
-                    && strlen($part) && substr($part, -1) === ')' && strpos($part, '(') === false
+                if (count($new) && is_string($new[count($new) - 1]) &&
+                    strlen($part) && substr($part, -1) === ')' && strpos($part, '(') === false
                 ) {
                     $new[count($new) - 1] .= $part;
                 } else {
@@ -530,6 +525,10 @@ class Compiler
 
         $selector = $this->glueFunctionSelectors($selector);
 
+        if (count($selector) == 1 && in_array(reset($selector), $partsPile)) {
+            return;
+        }
+
         foreach ($selector as $i => $part) {
             if ($i < $from) {
                 continue;
@@ -925,7 +924,7 @@ class Compiler
     {
         $env     = $this->pushEnv($block);
         $envs    = $this->compactEnv($env);
-        $without = isset($block->with) ? $this->compileWith($block->with) : static::WITH_RULE;
+        list($with, $without) = $this->compileWith(isset($block->with) ? $block->with : null);
 
         // wrap inline selector
         if ($block->selector) {
@@ -952,10 +951,10 @@ class Compiler
             $selfParent = $block->parent;
         }
 
-        $this->env = $this->filterWithout($envs, $without);
+        $this->env = $this->filterWithWithout($envs, $with, $without);
 
         $saveScope   = $this->scope;
-        $this->scope = $this->filterScopeWithout($saveScope, $without);
+        $this->scope = $this->filterScopeWithWithout($saveScope, $with, $without);
 
         // propagate selfParent to the children where they still can be useful
         $this->compileChildrenNoReturn($block->children, $this->scope, $selfParent);
@@ -971,11 +970,12 @@ class Compiler
      * Filter at-root scope depending of with/without option
      *
      * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $scope
-     * @param mixed                                  $without
+     * @param array                                  $with
+     * @param array                                  $without
      *
      * @return mixed
      */
-    protected function filterScopeWithout($scope, $without)
+    protected function filterScopeWithWithout($scope, $with, $without)
     {
         $filteredScopes = [];
 
@@ -993,7 +993,7 @@ class Compiler
                 break;
             }
 
-            if (! $this->isWithout($without, $scope)) {
+            if ($this->isWith($scope, $with, $without)) {
                 $s = clone $scope;
                 $s->children = [];
                 $s->lines = [];
@@ -1084,69 +1084,60 @@ class Compiler
     }
 
     /**
-     * Compile @at-root's with: inclusion / without: exclusion into filter flags
+     * Compile @at-root's with: inclusion / without: exclusion into 2 lists uses to filter scope/env later
      *
-     * @param array $with
+     * @param array $withCondition
      *
-     * @return integer
+     * @return array
      */
-    protected function compileWith($with)
+    protected function compileWith($withCondition)
     {
-        static $mapping = [
-            'rule'     => self::WITH_RULE,
-            'media'    => self::WITH_MEDIA,
-            'supports' => self::WITH_SUPPORTS,
-            'all'      => self::WITH_ALL,
-        ];
-
-        // exclude selectors by default
-        $without = static::WITH_RULE;
+        // just compile what we have in 2 lists
+        $with = [];
+        $without = ['rule' => true];
 
-        if ($this->libMapHasKey([$with, static::$with])) {
-            $without = static::WITH_ALL;
+        if ($withCondition) {
+            if ($this->libMapHasKey([$withCondition, static::$with])) {
+                $without = []; // cancel the default
+                $list = $this->coerceList($this->libMapGet([$withCondition, static::$with]));
 
-            $list = $this->coerceList($this->libMapGet([$with, static::$with]));
-
-            foreach ($list[2] as $item) {
-                $keyword = $this->compileStringContent($this->coerceString($item));
+                foreach ($list[2] as $item) {
+                    $keyword = $this->compileStringContent($this->coerceString($item));
 
-                if (array_key_exists($keyword, $mapping)) {
-                    $without &= ~($mapping[$keyword]);
+                    $with[$keyword] = true;
                 }
             }
-        }
-
-        if ($this->libMapHasKey([$with, static::$without])) {
-            $without = 0;
 
-            $list = $this->coerceList($this->libMapGet([$with, static::$without]));
+            if ($this->libMapHasKey([$withCondition, static::$without])) {
+                $without = []; // cancel the default
+                $list = $this->coerceList($this->libMapGet([$withCondition, static::$without]));
 
-            foreach ($list[2] as $item) {
-                $keyword = $this->compileStringContent($this->coerceString($item));
+                foreach ($list[2] as $item) {
+                    $keyword = $this->compileStringContent($this->coerceString($item));
 
-                if (array_key_exists($keyword, $mapping)) {
-                    $without |= $mapping[$keyword];
+                    $without[$keyword] = true;
                 }
             }
         }
 
-        return $without;
+        return [$with, $without];
     }
 
     /**
      * Filter env stack
      *
      * @param array   $envs
-     * @param integer $without
+     * @param array $with
+     * @param array $without
      *
      * @return \ScssPhp\ScssPhp\Compiler\Environment
      */
-    protected function filterWithout($envs, $without)
+    protected function filterWithWithout($envs, $with, $without)
     {
         $filtered = [];
 
         foreach ($envs as $e) {
-            if ($e->block && $this->isWithout($without, $e->block)) {
+            if ($e->block && ! $this->isWith($e->block, $with, $without)) {
                 $ec = clone $e;
                 $ec->block = null;
                 $ec->selectors = [];
@@ -1162,36 +1153,59 @@ class Compiler
     /**
      * Filter WITH rules
      *
-     * @param integer                                                       $without
      * @param \ScssPhp\ScssPhp\Block|\ScssPhp\ScssPhp\Formatter\OutputBlock $block
+     * @param array                                                         $with
+     * @param array                                                         $without
      *
      * @return boolean
      */
-    protected function isWithout($without, $block)
+    protected function isWith($block, $with, $without)
     {
         if (isset($block->type)) {
             if ($block->type === Type::T_MEDIA) {
-                return ($without & static::WITH_MEDIA) ? true : false;
+                return $this->testWithWithout('media', $with, $without);
             }
 
             if ($block->type === Type::T_DIRECTIVE) {
-                if (isset($block->name) && $block->name === 'supports') {
-                    return ($without & static::WITH_SUPPORTS) ? true : false;
+                if (isset($block->name)) {
+                    return $this->testWithWithout($block->name, $with, $without);
                 }
-
-                if (isset($block->selectors) && strpos(serialize($block->selectors), '@supports') !== false) {
-                    return ($without & static::WITH_SUPPORTS) ? true : false;
+                elseif (isset($block->selectors) && preg_match(',@(\w+),ims', json_encode($block->selectors), $m)) {
+                    return $this->testWithWithout($m[1], $with, $without);
+                }
+                else {
+                    return $this->testWithWithout('???', $with, $without);
                 }
             }
         }
+        elseif (isset($block->selectors)) {
+            return $this->testWithWithout('rule', $with, $without);
+        }
 
-        if ((($without & static::WITH_RULE) && isset($block->selectors))) {
-            return true;
+        return true;
+    }
+
+    /**
+     * Test a single type of block against with/without lists
+     *
+     * @param string $what
+     * @param array  $with
+     * @param array  $without
+     * @return bool
+     *   true if the block should be kept, false to reject
+     */
+    protected function testWithWithout($what, $with, $without) {
+
+        // if without, reject only if in the list (or 'all' is in the list)
+        if (count($without)) {
+            return (isset($without[$what]) || isset($without['all'])) ? false : true;
         }
 
-        return false;
+        // otherwise reject all what is not in the with list
+        return (isset($with[$what]) || isset($with['all'])) ? true : false;
     }
 
+
     /**
      * Compile keyframe block
      *
@@ -1220,6 +1234,39 @@ class Compiler
         $this->popEnv();
     }
 
+    /**
+     * Compile nested properties lines
+     *
+     * @param \ScssPhp\ScssPhp\Block $block
+     * @param OutputBlock            $out
+     */
+    protected function compileNestedPropertiesBlock(Block $block, OutputBlock $out)
+    {
+        $prefix = $this->compileValue($block->prefix) . '-';
+
+        $nested = $this->makeOutputBlock($block->type);
+        $nested->parent = $out;
+
+        if ($block->hasValue) {
+            $nested->depth = $out->depth + 1;
+        }
+
+        $out->children[] = $nested;
+
+        foreach ($block->children as $child) {
+            switch ($child[0]) {
+                case Type::T_ASSIGN:
+                    array_unshift($child[1][2], $prefix);
+                    break;
+
+                case Type::T_NESTED_PROPERTY:
+                    array_unshift($child[1]->prefix[2], $prefix);
+                    break;
+            }
+            $this->compileChild($child, $nested);
+        }
+    }
+
     /**
      * Compile nested block
      *
@@ -1710,52 +1757,50 @@ class Compiler
      */
     protected function evaluateMediaQuery($queryList)
     {
+        static $parser = null;
+        $outQueryList = [];
         foreach ($queryList as $kql => $query) {
+            $shouldReparse = false;
             foreach ($query as $kq => $q) {
                 for ($i = 1; $i < count($q); $i++) {
                     $value = $this->compileValue($q[$i]);
 
                     // the parser had no mean to know if media type or expression if it was an interpolation
+                    // so you need to reparse if the T_MEDIA_TYPE looks like anything else a media type
                     if ($q[0] == Type::T_MEDIA_TYPE &&
                         (strpos($value, '(') !== false ||
                         strpos($value, ')') !== false ||
-                        strpos($value, ':') !== false)
+                        strpos($value, ':') !== false ||
+                        strpos($value, ',') !== false)
                     ) {
-                        $queryList[$kql][$kq][0] = Type::T_MEDIA_EXPRESSION;
-
-                        if (strpos($value, 'and') !== false) {
-                            $values = explode('and', $value);
-                            $value = trim(array_pop($values));
-
-                            while ($v = trim(array_pop($values))) {
-                                $type = Type::T_MEDIA_EXPRESSION;
-
-                                if (strpos($v, '(') === false &&
-                                    strpos($v, ')') === false &&
-                                    strpos($v, ':') === false
-                                ) {
-                                    $type = Type::T_MEDIA_TYPE;
-                                }
-
-                                if (substr($v, 0, 1) === '(' && substr($v, -1) === ')') {
-                                    $v = substr($v, 1, -1);
-                                }
-
-                                $queryList[$kql][] = [$type,[Type::T_KEYWORD, $v]];
-                            }
-                        }
-
-                        if (substr($value, 0, 1) === '(' && substr($value, -1) === ')') {
-                            $value = substr($value, 1, -1);
-                        }
+                        $shouldReparse = true;
                     }
 
                     $queryList[$kql][$kq][$i] = [Type::T_KEYWORD, $value];
                 }
             }
+            if ($shouldReparse) {
+                if (is_null($parser)) {
+                    $parser = $this->parserFactory(__METHOD__);
+                }
+                $queryString = $this->compileMediaQuery([$queryList[$kql]]);
+                $queryString = reset($queryString);
+                if (strpos($queryString, '@media ') === 0) {
+                    $queryString = substr($queryString, 7);
+                    $queries = [];
+                    if ($parser->parseMediaQueryList($queryString, $queries)) {
+                        $queries = $this->evaluateMediaQuery($queries[2]);
+                        while (count($queries)) {
+                            $outQueryList[] = array_shift($queries);
+                        }
+                        continue;
+                    }
+                }
+            }
+            $outQueryList[] = $queryList[$kql];
         }
 
-        return $queryList;
+        return $outQueryList;
     }
 
     /**
@@ -2038,6 +2083,94 @@ class Compiler
         return false;
     }
 
+
+    /**
+     * Append a root directive like @import or @charset as near as the possible from the source code
+     * (keeping before comments, @import and @charset coming before in the source code)
+     *
+     * @param string                                        $line
+     * @param @param \ScssPhp\ScssPhp\Formatter\OutputBlock $out
+     * @param array                                         $allowed
+     */
+    protected function appendRootDirective($line, $out, $allowed = [Type::T_COMMENT])
+    {
+        $root = $out;
+
+        while ($root->parent) {
+            $root = $root->parent;
+        }
+
+        $i = 0;
+
+        while ($i < count($root->children)) {
+            if (! isset($root->children[$i]->type) || ! in_array($root->children[$i]->type, $allowed)) {
+                break;
+            }
+
+            $i++;
+        }
+
+        // remove incompatible children from the bottom of the list
+        $saveChildren = [];
+
+        while ($i < count($root->children)) {
+            $saveChildren[] = array_pop($root->children);
+        }
+
+        // insert the directive as a comment
+        $child = $this->makeOutputBlock(Type::T_COMMENT);
+        $child->lines[] = $line;
+        $child->sourceName = $this->sourceNames[$this->sourceIndex];
+        $child->sourceLine = $this->sourceLine;
+        $child->sourceColumn = $this->sourceColumn;
+
+        $root->children[] = $child;
+
+        // repush children
+        while (count($saveChildren)) {
+            $root->children[] = array_pop($saveChildren);
+        }
+    }
+
+    /**
+     * Append lines to the courrent output block:
+     * directly to the block or through a child if necessary
+     *
+     * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $out
+     * @param string                                 $type
+     * @param string                                 $line
+     */
+    protected function appendOutputLine(OutputBlock $out, $type, $line)
+    {
+        $outWrite = &$out;
+
+        if ($type === Type::T_COMMENT) {
+            $parent = $out->parent;
+
+            if (end($parent->children) !== $out) {
+                $outWrite = &$parent->children[count($parent->children)-1];
+            }
+        }
+
+        // check if it's a flat output or not
+        if (count($out->children)) {
+            $lastChild = &$out->children[count($out->children) -1];
+
+            if ($lastChild->depth === $out->depth && is_null($lastChild->selectors) && ! count($lastChild->children)) {
+                $outWrite = $lastChild;
+            } else {
+                $nextLines = $this->makeOutputBlock($type);
+                $nextLines->parent = $out;
+                $nextLines->depth = $out->depth;
+
+                $out->children[] = $nextLines;
+                $outWrite = &$nextLines;
+            }
+        }
+
+        $outWrite->lines[] = $line;
+    }
+
     /**
      * Compile child; returns a value to halt execution
      *
@@ -2070,7 +2203,7 @@ class Compiler
                 $rawPath = $this->reduce($child[1]);
 
                 if (! $this->compileImport($rawPath, $out, true)) {
-                    $out->lines[] = '@import ' . $this->compileValue($rawPath) . ';';
+                    $this->appendRootDirective('@import ' . $this->compileValue($rawPath) . ';', $out);
                 }
                 break;
 
@@ -2078,7 +2211,7 @@ class Compiler
                 $rawPath = $this->reduce($child[1]);
 
                 if (! $this->compileImport($rawPath, $out)) {
-                    $out->lines[] = '@import ' . $this->compileValue($rawPath) . ';';
+                    $this->appendRootDirective('@import ' . $this->compileValue($rawPath) . ';', $out);
                 }
                 break;
 
@@ -2101,8 +2234,7 @@ class Compiler
             case Type::T_CHARSET:
                 if (! $this->charsetSeen) {
                     $this->charsetSeen = true;
-
-                    $out->lines[] = '@charset ' . $this->compileValue($child[1]) . ';';
+                    $this->appendRootDirective('@charset ' . $this->compileValue($child[1]) . ';', $out);
                 }
                 break;
 
@@ -2120,8 +2252,8 @@ class Compiler
                     }
 
                     $shouldSet = $isDefault &&
-                        (($result = $this->get($name[1], false)) === null
-                        || $result === static::$null);
+                        (($result = $this->get($name[1], false)) === null ||
+                        $result === static::$null);
 
                     if (! $isDefault || $shouldSet) {
                         $this->set($name[1], $this->reduce($value), true, null, $value);
@@ -2170,10 +2302,11 @@ class Compiler
 
                 $compiledValue = $this->compileValue($value);
 
-                $out->lines[] = $this->formatter->property(
+                $line = $this->formatter->property(
                     $compiledName,
                     $compiledValue
                 );
+                $this->appendOutputLine($out, Type::T_ASSIGN, $line);
                 break;
 
             case Type::T_COMMENT:
@@ -2182,14 +2315,14 @@ class Compiler
                     break;
                 }
 
-                $out->lines[] = $child[1];
+                $this->appendOutputLine($out, Type::T_COMMENT, $child[1]);
                 break;
 
             case Type::T_MIXIN:
             case Type::T_FUNCTION:
                 list(, $block) = $child;
 
-                $this->set(static::$namespaces[$block->type] . $block->name, $block);
+                $this->set(static::$namespaces[$block->type] . $block->name, $block, true);
                 break;
 
             case Type::T_EXTEND:
@@ -2327,26 +2460,7 @@ class Compiler
                 return $this->reduce($child[1], true);
 
             case Type::T_NESTED_PROPERTY:
-                list(, $prop) = $child;
-
-                $prefixed = [];
-                $prefix = $this->compileValue($prop->prefix) . '-';
-
-                foreach ($prop->children as $child) {
-                    switch ($child[0]) {
-                        case Type::T_ASSIGN:
-                            array_unshift($child[1][2], $prefix);
-                            break;
-
-                        case Type::T_NESTED_PROPERTY:
-                            array_unshift($child[1]->prefix[2], $prefix);
-                            break;
-                    }
-
-                    $prefixed[] = $child;
-                }
-
-                $this->compileChildrenNoReturn($prefixed, $out);
+                $this->compileNestedPropertiesBlock($child[1], $out);
                 break;
 
             case Type::T_INCLUDE:
@@ -2397,6 +2511,8 @@ class Compiler
                     $copyContent->scope = $callingScope;
 
                     $this->setRaw(static::$namespaces['special'] . 'content', $copyContent, $this->env);
+                } else {
+                    $this->setRaw(static::$namespaces['special'] . 'content', null, $this->env);
                 }
 
                 if (isset($mixin->args)) {
@@ -2419,7 +2535,7 @@ class Compiler
                 if (! $content) {
                     $content = new \stdClass();
                     $content->scope = new \stdClass();
-                    $content->children = $this->storeEnv->parent->block->children;
+                    $content->children = $env->parent->block->children;
                     break;
                 }
 
@@ -2567,9 +2683,9 @@ class Compiler
                 }
 
                 // special case: looks like css shorthand
-                if ($opName == 'div' && ! $inParens && ! $inExp && isset($right[2])
-                    && (($right[0] !== Type::T_NUMBER && $right[2] != '')
-                    || ($right[0] === Type::T_NUMBER && ! $right->unitless()))
+                if ($opName == 'div' && ! $inParens && ! $inExp && isset($right[2]) &&
+                    (($right[0] !== Type::T_NUMBER && $right[2] != '') ||
+                    ($right[0] === Type::T_NUMBER && ! $right->unitless()))
                 ) {
                     return $this->expToString($value);
                 }
@@ -3371,7 +3487,7 @@ class Compiler
                 return 'null';
 
             default:
-                $this->throwError("unknown value type: $value[0]");
+                $this->throwError("unknown value type: ".json_encode($value));
         }
     }
 
@@ -4364,40 +4480,118 @@ class Compiler
      *
      * @return array
      */
-    protected function sortArgs($prototype, $args)
+    protected function sortArgs($prototypes, $args)
     {
-        $keyArgs = [];
-        $posArgs = [];
+        static $parser = null;
 
-        // separate positional and keyword arguments
-        foreach ($args as $arg) {
-            list($key, $value) = $arg;
+        if (! isset($prototypes)) {
+            $keyArgs = [];
+            $posArgs = [];
 
-            $key = $key[1];
+            // separate positional and keyword arguments
+            foreach ($args as $arg) {
+                list($key, $value) = $arg;
 
-            if (empty($key)) {
-                $posArgs[] = empty($arg[2]) ? $value : $arg;
-            } else {
-                $keyArgs[$key] = $value;
+                $key = $key[1];
+
+                if (empty($key)) {
+                    $posArgs[] = empty($arg[2]) ? $value : $arg;
+                } else {
+                    $keyArgs[$key] = $value;
+                }
             }
-        }
 
-        if (! isset($prototype)) {
             return [$posArgs, $keyArgs];
         }
 
-        // copy positional args
-        $finalArgs = array_pad($posArgs, count($prototype), null);
+        $finalArgs = [];
+
+        if (! is_array(reset($prototypes))) {
+            $prototypes = [$prototypes];
+        }
+
+        $keyArgs = [];
+
+        // trying each prototypes
+        $prototypeHasMatch = false;
+        $exceptionMessage = '';
+
+        foreach ($prototypes as $prototype) {
+            $argDef = [];
+
+            foreach ($prototype as $i => $p) {
+                $default = null;
+                $p       = explode(':', $p, 2);
+                $name    = array_shift($p);
+
+                if (count($p)) {
+                    $p = trim(reset($p));
 
-        // overwrite positional args with keyword args
-        foreach ($prototype as $i => $names) {
-            foreach ((array) $names as $name) {
-                if (isset($keyArgs[$name])) {
-                    $finalArgs[$i] = $keyArgs[$name];
+                    if ($p === 'null') {
+                        // differentiate this null from the static::$null
+                        $default = [Type::T_KEYWORD, 'null'];
+                    } else {
+                        if (is_null($parser)) {
+                            $parser = $this->parserFactory(__METHOD__);
+                        }
+
+                        $parser->parseValue($p, $default);
+                    }
+                }
+
+                $isVariable = false;
+
+                if (substr($name, -3) === '...') {
+                    $isVariable = true;
+                    $name = substr($name, 0, -3);
+                }
+
+                $argDef[] = [$name, $default, $isVariable];
+            }
+
+            try {
+                $vars = $this->applyArguments($argDef, $args, false);
+
+                // ensure all args are populated
+                foreach ($prototype as $i => $p) {
+                    $name = explode(':', $p)[0];
+
+                    if (! isset($finalArgs[$i])) {
+                        $finalArgs[$i] = null;
+                    }
+                }
+
+                // apply positional args
+                foreach (array_values($vars) as $i => $val) {
+                    $finalArgs[$i] = $val;
                 }
+
+                $keyArgs = array_merge($keyArgs, $vars);
+                $prototypeHasMatch = true;
+
+                // overwrite positional args with keyword args
+                foreach ($prototype as $i => $p) {
+                    $name = explode(':', $p)[0];
+
+                    if (isset($keyArgs[$name])) {
+                        $finalArgs[$i] = $keyArgs[$name];
+                    }
+
+                    // special null value as default: translate to real null here
+                    if ($finalArgs[$i] === [Type::T_KEYWORD, 'null']) {
+                        $finalArgs[$i] = null;
+                    }
+                }
+                // should we break if this prototype seems fulfilled?
+            } catch (CompilerException $e) {
+                $exceptionMessage = $e->getMessage();
             }
         }
 
+        if ($exceptionMessage && ! $prototypeHasMatch) {
+            $this->throwError($exceptionMessage);
+        }
+
         return [$finalArgs, $keyArgs];
     }
 
@@ -4409,12 +4603,16 @@ class Compiler
      *
      * @throws \Exception
      */
-    protected function applyArguments($argDef, $argValues)
+    protected function applyArguments($argDef, $argValues, $storeInEnv = true)
     {
-        $storeEnv = $this->getStoreEnv();
+        $output = [];
 
-        $env = new Environment;
-        $env->store = $storeEnv->store;
+        if ($storeInEnv) {
+            $storeEnv = $this->getStoreEnv();
+
+            $env = new Environment;
+            $env->store = $storeEnv->store;
+        }
 
         $hasVariable = false;
         $args = [];
@@ -4426,14 +4624,18 @@ class Compiler
             $hasVariable |= $isVariable;
         }
 
-        $keywordArgs = [];
+        $splatSeparator      = null;
+        $keywordArgs         = [];
         $deferredKeywordArgs = [];
-        $remaining = [];
+        $remaining           = [];
+        $hasKeywordArgument  = false;
 
         // assign the keyword args
         foreach ((array) $argValues as $arg) {
             if (! empty($arg[0])) {
-                if (! isset($args[$arg[0][1]])) {
+                $hasKeywordArgument = true;
+
+                if (! isset($args[$arg[0][1]]) || $args[$arg[0][1]][3]) {
                     if ($hasVariable) {
                         $deferredKeywordArgs[$arg[0][1]] = $arg[1];
                     } else {
@@ -4446,17 +4648,30 @@ class Compiler
                 } else {
                     $keywordArgs[$arg[0][1]] = $arg[1];
                 }
-            } elseif (count($keywordArgs)) {
-                $this->throwError('Positional arguments must come before keyword arguments.');
-                break;
             } elseif ($arg[2] === true) {
                 $val = $this->reduce($arg[1], true);
 
                 if ($val[0] === Type::T_LIST) {
                     foreach ($val[2] as $name => $item) {
                         if (! is_numeric($name)) {
-                            $keywordArgs[$name] = $item;
+                            if (!isset($args[$name])) {
+                                foreach (array_keys($args) as $an) {
+                                    if (str_replace("_", "-", $an) === str_replace("_", "-", $name)) {
+                                        $name = $an;
+                                        break;
+                                    }
+                                }
+                            }
+
+                            if ($hasVariable) {
+                                $deferredKeywordArgs[$name] = $item;
+                            } else {
+                                $keywordArgs[$name] = $item;
+                            }
                         } else {
+                            if (is_null($splatSeparator)) {
+                                $splatSeparator = $val[1];
+                            }
                             $remaining[] = $item;
                         }
                     }
@@ -4466,14 +4681,32 @@ class Compiler
                         $item = $val[2][$i];
 
                         if (! is_numeric($name)) {
-                            $keywordArgs[$name] = $item;
+                            if (!isset($args[$name])) {
+                                foreach (array_keys($args) as $an) {
+                                    if (str_replace("_", "-", $an) === str_replace("_", "-", $name)) {
+                                        $name = $an;
+                                        break;
+                                    }
+                                }
+                            }
+                            if ($hasVariable) {
+                                $deferredKeywordArgs[$name] = $item;
+                            } else {
+                                $keywordArgs[$name] = $item;
+                            }
                         } else {
+                            if (is_null($splatSeparator)) {
+                                $splatSeparator = $val[1];
+                            }
                             $remaining[] = $item;
                         }
                     }
                 } else {
                     $remaining[] = $val;
                 }
+            } elseif ($hasKeywordArgument) {
+                $this->throwError('Positional arguments must come before keyword arguments.');
+                break;
             } else {
                 $remaining[] = $arg[1];
             }
@@ -4483,7 +4716,7 @@ class Compiler
             list($i, $name, $default, $isVariable) = $arg;
 
             if ($isVariable) {
-                $val = [Type::T_LIST, ',', [], $isVariable];
+                $val = [Type::T_LIST, is_null($splatSeparator) ? ',' : $splatSeparator , [], $isVariable];
 
                 for ($count = count($remaining); $i < $count; $i++) {
                     $val[2][] = $remaining[$i];
@@ -4503,10 +4736,16 @@ class Compiler
                 break;
             }
 
-            $this->set($name, $this->reduce($val, true), true, $env);
+            if ($storeInEnv) {
+                $this->set($name, $this->reduce($val, true), true, $env);
+            } else {
+                $output[$name] = $val;
+            }
         }
 
-        $storeEnv->store = $env->store;
+        if ($storeInEnv) {
+            $storeEnv->store = $env->store;
+        }
 
         foreach ($args as $arg) {
             list($i, $name, $default, $isVariable) = $arg;
@@ -4515,8 +4754,14 @@ class Compiler
                 continue;
             }
 
-            $this->set($name, $this->reduce($default, true), true);
+            if ($storeInEnv) {
+                $this->set($name, $this->reduce($default, true), true);
+            } else {
+                $output[$name] = $default;
+            }
         }
+
+        return $output;
     }
 
     /**
@@ -4617,10 +4862,20 @@ class Compiler
                 $key = $keys[$i];
                 $value = $values[$i];
 
+                switch ($key[0]) {
+                    case Type::T_LIST:
+                    case Type::T_MAP:
+                        break;
+
+                    default:
+                        $key = [Type::T_KEYWORD, $this->compileStringContent($this->coerceString($key))];
+                        break;
+                }
+
                 $list[] = [
                     Type::T_LIST,
                     '',
-                    [[Type::T_KEYWORD, $this->compileStringContent($this->coerceString($key))], $value]
+                    [$key, $value]
                 ];
             }
 
@@ -4923,46 +5178,27 @@ class Compiler
 
     // Built in functions
 
-    //protected static $libCall = ['name', 'args...'];
+    protected static $libCall = ['name', 'args...'];
     protected function libCall($args, $kwargs)
     {
         $name = $this->compileStringContent($this->coerceString($this->reduce(array_shift($args), true)));
+        $callArgs = [];
 
-        $posArgs = [];
-
-        foreach ($args as $arg) {
-            if (empty($arg[0])) {
-                if ($arg[2] === true) {
-                    $tmp = $this->reduce($arg[1]);
-
-                    if ($tmp[0] === Type::T_LIST) {
-                        foreach ($tmp[2] as $item) {
-                            $posArgs[] = [null, $item, false];
-                        }
-                    } else {
-                        $posArgs[] = [null, $tmp, true];
-                    }
-
-                    continue;
-                }
-
-                $posArgs[] = [null, $this->reduce($arg), false];
-                continue;
+        // $kwargs['args'] is [Type::T_LIST, ',', [..]]
+        foreach ($kwargs['args'][2] as $varname => $arg) {
+            if (is_numeric($varname)) {
+                $varname = null;
+            } else {
+                $varname = [ 'var', $varname];
             }
 
-            $posArgs[] = [null, $arg, false];
+            $callArgs[] = [$varname, $arg, false];
         }
 
-        if (count($kwargs)) {
-            foreach ($kwargs as $key => $value) {
-                $posArgs[] = [[Type::T_VARIABLE, $key], $value, false];
-            }
-        }
-
-        return $this->reduce([Type::T_FUNCTION_CALL, $name, $posArgs]);
+        return $this->reduce([Type::T_FUNCTION_CALL, $name, $callArgs]);
     }
 
-    protected static $libIf = ['condition', 'if-true', 'if-false'];
+    protected static $libIf = ['condition', 'if-true', 'if-false:'];
     protected function libIf($args)
     {
         list($cond, $t, $f) = $args;
@@ -5015,8 +5251,8 @@ class Compiler
     }
 
     protected static $libRgba = [
-        ['red', 'color'],
-        'green', 'blue', 'alpha'];
+        ['color', 'alpha:1'],
+        ['red', 'green', 'blue', 'alpha:1'] ];
     protected function libRgba($args)
     {
         if ($color = $this->coerceColor($args[0])) {
@@ -5045,11 +5281,11 @@ class Compiler
             }
         }
 
-        if (isset($args[4]) || isset($args[5]) || isset($args[6])) {
+        if (! empty($args[4]) || ! empty($args[5]) || ! empty($args[6])) {
             $hsl = $this->toHSL($color[1], $color[2], $color[3]);
 
             foreach ([4, 5, 6] as $i) {
-                if (isset($args[$i])) {
+                if (! empty($args[$i])) {
                     $val = $this->assertNumber($args[$i]);
                     $hsl[$i - 3] = call_user_func($fn, $hsl[$i - 3], $val, $i);
                 }
@@ -5068,8 +5304,8 @@ class Compiler
     }
 
     protected static $libAdjustColor = [
-        'color', 'red', 'green', 'blue',
-        'hue', 'saturation', 'lightness', 'alpha'
+        'color', 'red:null', 'green:null', 'blue:null',
+        'hue:null', 'saturation:null', 'lightness:null', 'alpha:null'
     ];
     protected function libAdjustColor($args)
     {
@@ -5079,8 +5315,8 @@ class Compiler
     }
 
     protected static $libChangeColor = [
-        'color', 'red', 'green', 'blue',
-        'hue', 'saturation', 'lightness', 'alpha'
+        'color', 'red:null', 'green:null', 'blue:null',
+        'hue:null', 'saturation:null', 'lightness:null', 'alpha:null'
     ];
     protected function libChangeColor($args)
     {
@@ -5090,8 +5326,8 @@ class Compiler
     }
 
     protected static $libScaleColor = [
-        'color', 'red', 'green', 'blue',
-        'hue', 'saturation', 'lightness', 'alpha'
+        'color', 'red:null', 'green:null', 'blue:null',
+        'hue:null', 'saturation:null', 'lightness:null', 'alpha:null'
     ];
     protected function libScaleColor($args)
     {
@@ -5185,7 +5421,7 @@ class Compiler
     }
 
     // mix two colors
-    protected static $libMix = ['color-1', 'color-2', 'weight'];
+    protected static $libMix = ['color-1', 'color-2', 'weight:0.5'];
     protected function libMix($args)
     {
         list($first, $second, $weight) = $args;
@@ -5307,7 +5543,7 @@ class Compiler
         return $this->adjustHsl($color, 3, -$amount);
     }
 
-    protected static $libSaturate = ['color', 'amount'];
+    protected static $libSaturate = [['color', 'amount'], ['number']];
     protected function libSaturate($args)
     {
         $value = $args[0];
@@ -5599,11 +5835,14 @@ class Compiler
     protected function libMapGet($args)
     {
         $map = $this->assertMap($args[0]);
-        $key = $this->compileStringContent($this->coerceString($args[1]));
+        $key = $args[1];
 
-        for ($i = count($map[1]) - 1; $i >= 0; $i--) {
-            if ($key === $this->compileStringContent($this->coerceString($map[1][$i]))) {
-                return $map[2][$i];
+        if (! is_null($key)) {
+            $key = $this->compileStringContent($this->coerceString($key));
+            for ($i = count($map[1]) - 1; $i >= 0; $i--) {
+                if ($key === $this->compileStringContent($this->coerceString($map[1][$i]))) {
+                    return $map[2][$i];
+                }
             }
         }
 
@@ -5716,7 +5955,7 @@ class Compiler
         }
     }
 
-    protected static $libJoin = ['list1', 'list2', 'separator'];
+    protected static $libJoin = ['list1', 'list2', 'separator:null'];
     protected function libJoin($args)
     {
         list($list1, $list2, $sep) = $args;
@@ -5728,7 +5967,7 @@ class Compiler
         return [Type::T_LIST, $sep, array_merge($list1[2], $list2[2])];
     }
 
-    protected static $libAppend = ['list', 'val', 'separator'];
+    protected static $libAppend = ['list', 'val', 'separator:null'];
     protected function libAppend($args)
     {
         list($list1, $value, $sep) = $args;
@@ -5873,7 +6112,7 @@ class Compiler
         return new Node\Number(strlen($stringContent), '');
     }
 
-    protected static $libStrSlice = ['string', 'start-at', 'end-at'];
+    protected static $libStrSlice = ['string', 'start-at', 'end-at:null'];
     protected function libStrSlice($args)
     {
         if (isset($args[2]) && $args[2][1] == 0) {
@@ -6182,9 +6421,12 @@ class Compiler
         return true;
     }
 
-    //protected static $libSelectorAppend = ['selector...'];
+    protected static $libSelectorAppend = ['selector...'];
     protected function libSelectorAppend($args)
     {
+        // get the selector... list
+        $args = reset($args);
+        $args = $args[2];
         if (count($args) < 1) {
             $this->throwError("selector-append() needs at least 1 argument");
         }
@@ -6328,9 +6570,12 @@ class Compiler
         return $extended;
     }
 
-    //protected static $libSelectorNest = ['selector...'];
+    protected static $libSelectorNest = ['selector...'];
     protected function libSelectorNest($args)
     {
+        // get the selector... list
+        $args = reset($args);
+        $args = $args[2];
         if (count($args) < 1) {
             $this->throwError("selector-nest() needs at least 1 argument");
         }
index b2efc2d..478aa6a 100644 (file)
@@ -126,9 +126,7 @@ abstract class Formatter
             return;
         }
 
-        if (($count = count($lines))
-            && substr($lines[$count - 1], -1) === ';'
-        ) {
+        if (($count = count($lines)) && substr($lines[$count - 1], -1) === ';') {
             $lines[$count - 1] = substr($lines[$count - 1], 0, -1);
         }
     }
@@ -217,6 +215,30 @@ abstract class Formatter
         }
     }
 
+    /**
+     * Test and clean safely empty children
+     * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $block
+     * @return bool
+     */
+    protected function testEmptyChildren($block)
+    {
+        $isEmpty = empty($block->lines);
+
+        if ($block->children) {
+            foreach ($block->children as $k => &$child) {
+                if (! $this->testEmptyChildren($child)) {
+                    $isEmpty = false;
+                } else {
+                    if ($child->type === Type::T_MEDIA || $child->type === Type::T_DIRECTIVE) {
+                        $child->children = [];
+                        $child->selectors = null;
+                    }
+                }
+            }
+        }
+        return $isEmpty;
+    }
+
     /**
      * Entry point to formatting a block
      *
@@ -237,6 +259,8 @@ abstract class Formatter
             $this->sourceMapGenerator = $sourceMapGenerator;
         }
 
+        $this->testEmptyChildren($block);
+
         ob_start();
 
         $this->block($block);
index 260ae8e..50a70ce 100644 (file)
@@ -13,6 +13,7 @@ namespace ScssPhp\ScssPhp\Formatter;
 
 use ScssPhp\ScssPhp\Formatter;
 use ScssPhp\ScssPhp\Formatter\OutputBlock;
+use ScssPhp\ScssPhp\Type;
 
 /**
  * Nested formatter
@@ -67,53 +68,56 @@ class Nested extends Formatter
         }
 
         $this->write($inner . implode($glue, $block->lines));
-
-        if (! empty($block->children)) {
-            $this->write($this->break);
-        }
     }
 
-    /**
-     * {@inheritdoc}
-     */
-    protected function blockSelectors(OutputBlock $block)
+    protected function hasFlatChild($block)
     {
-        $inner = $this->indentStr();
+        foreach ($block->children as $child) {
+            if (empty($child->selectors)) {
+                return true;
+            }
+        }
 
-        $this->write($inner
-            . implode($this->tagSeparator, $block->selectors)
-            . $this->open . $this->break);
+        return false;
     }
 
     /**
      * {@inheritdoc}
      */
-    protected function blockChildren(OutputBlock $block)
+    protected function block(OutputBlock $block)
     {
-        foreach ($block->children as $i => $child) {
-            $this->block($child);
+        static $depths;
+        static $downLevel;
+        static $closeBlock;
+        static $previousEmpty;
+        static $previousHasSelector;
 
-            if ($i < count($block->children) - 1) {
-                $this->write($this->break);
+        if ($block->type === 'root') {
+            $depths = [ 0 ];
+            $downLevel = '';
+            $closeBlock = '';
+            $this->depth = 0;
+            $previousEmpty = false;
+            $previousHasSelector = false;
+        }
 
-                if (isset($block->children[$i + 1])) {
-                    $next = $block->children[$i + 1];
+        $isMediaOrDirective = in_array($block->type, [Type::T_DIRECTIVE, Type::T_MEDIA]);
+        $isSupport = ($block->type === Type::T_DIRECTIVE
+            && $block->selectors && strpos(implode('', $block->selectors), '@supports') !== false);
 
-                    if ($next->depth === max($block->depth, 1) && $child->depth >= $next->depth) {
-                        $this->write($this->break);
-                    }
-                }
+        while ($block->depth < end($depths) || ($block->depth == 1 && end($depths) == 1)) {
+            array_pop($depths);
+            $this->depth--;
+
+            if (!$this->depth && ($block->depth <= 1 || (!$this->indentLevel && $block->type === Type::T_COMMENT)) &&
+                (($block->selectors && ! $isMediaOrDirective) || $previousHasSelector)
+            ) {
+                $downLevel = $this->break;
             }
-        }
-    }
 
-    /**
-     * {@inheritdoc}
-     */
-    protected function block(OutputBlock $block)
-    {
-        if ($block->type === 'root') {
-            $this->adjustAllChildren($block);
+            if (empty($block->lines) && empty($block->children)) {
+                $previousEmpty = true;
+            }
         }
 
         if (empty($block->lines) && empty($block->children)) {
@@ -122,80 +126,87 @@ class Nested extends Formatter
 
         $this->currentBlock = $block;
 
+        if (! empty($block->lines) || (! empty($block->children) && ($this->depth < 1 || $isSupport))) {
+            if ($block->depth > end($depths)) {
+                if (! $previousEmpty || $this->depth < 1) {
+                    $this->depth++;
+                    $depths[] = $block->depth;
+                } else {
+                    // keep the current depth unchanged but take the block depth as a new reference for following blocks
+                    array_pop($depths);
+                    $depths[] = $block->depth;
+                }
+            }
+        }
 
-        $this->depth = $block->depth;
+        $previousEmpty = ($block->type === Type::T_COMMENT);
+        $previousHasSelector = false;
 
         if (! empty($block->selectors)) {
+            if ($closeBlock) {
+                $this->write($closeBlock);
+                $closeBlock = '';
+            }
+
+            if ($downLevel) {
+                $this->write($downLevel);
+                $downLevel = '';
+            }
+
             $this->blockSelectors($block);
 
             $this->indentLevel++;
         }
 
         if (! empty($block->lines)) {
+            if ($closeBlock) {
+                $this->write($closeBlock);
+                $closeBlock = '';
+            }
+
+            if ($downLevel) {
+                $this->write($downLevel);
+                $downLevel = '';
+            }
+
             $this->blockLines($block);
+            $closeBlock = $this->break;
         }
 
         if (! empty($block->children)) {
-            $this->blockChildren($block);
+            if ($this->depth>0 && ($isMediaOrDirective || ! $this->hasFlatChild($block))) {
+                array_pop($depths);
+                $this->depth--;
+                $this->blockChildren($block);
+                $this->depth++;
+                $depths[] = $block->depth;
+            } else {
+                $this->blockChildren($block);
+            }
+        }
+
+        // reclear to not be spoiled by children if T_DIRECTIVE
+        if ($block->type === Type::T_DIRECTIVE) {
+            $previousHasSelector = false;
         }
 
         if (! empty($block->selectors)) {
             $this->indentLevel--;
 
             $this->write($this->close);
-        }
+            $closeBlock = $this->break;
 
-        if ($block->type === 'root') {
-            $this->write($this->break);
-        }
-    }
-
-    /**
-     * Adjust the depths of all children, depth first
-     *
-     * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $block
-     */
-    private function adjustAllChildren(OutputBlock $block)
-    {
-        // flatten empty nested blocks
-        $children = [];
-
-        foreach ($block->children as $i => $child) {
-            if (empty($child->lines) && empty($child->children)) {
-                if (isset($block->children[$i + 1])) {
-                    $block->children[$i + 1]->depth = $child->depth;
-                }
-
-                continue;
+            if ($this->depth > 1 && ! empty($block->children)) {
+                array_pop($depths);
+                $this->depth--;
             }
-
-            $children[] = $child;
-        }
-
-        $count = count($children);
-
-        for ($i = 0; $i < $count; $i++) {
-            $depth = $children[$i]->depth;
-            $j = $i + 1;
-
-            if (isset($children[$j]) && $depth < $children[$j]->depth) {
-                $childDepth = $children[$j]->depth;
-
-                for (; $j < $count; $j++) {
-                    if ($depth < $children[$j]->depth && $childDepth >= $children[$j]->depth) {
-                        $children[$j]->depth = $depth + 1;
-                    }
-                }
+            if (! $isMediaOrDirective) {
+                $previousHasSelector = true;
             }
         }
 
-        $block->children = $children;
-
-        // make relative to parent
-        foreach ($block->children as $child) {
-            $this->adjustAllChildren($child);
-
-            $child->depth = $child->depth - $block->depth;
+        if ($block->type === 'root') {
+            $this->write($this->break);
         }
     }
 }
index e184ca1..1e83bf8 100644 (file)
@@ -148,10 +148,10 @@ class Number extends Node implements \ArrayAccess
             return $this->sourceLine !== null;
         }
 
-        if ($offset === -1
-            || $offset === 0
-            || $offset === 1
-            || $offset === 2
+        if ($offset === -1 ||
+            $offset === 0 ||
+            $offset === 1 ||
+            $offset === 2
         ) {
             return true;
         }
index b809131..ed9b28f 100644 (file)
@@ -64,6 +64,7 @@ class Parser
     private $env;
     private $inParens;
     private $eatWhiteDefault;
+    private $discardComments;
     private $buffer;
     private $utf8;
     private $encoding;
@@ -88,6 +89,7 @@ class Parser
         $this->utf8             = ! $encoding || strtolower($encoding) === 'utf-8';
         $this->patternModifiers = $this->utf8 ? 'Aisu' : 'Ais';
         $this->commentsSeen     = [];
+        $this->discardComments  = false;
 
         if (empty(static::$operatorPattern)) {
             static::$operatorPattern = '([*\/%+-]|[!=]\=|\>\=?|\<\=\>|\<\=?|and|or)';
@@ -265,6 +267,34 @@ class Parser
         return $selector;
     }
 
+    /**
+     * Parse a media Query
+     *
+     * @api
+     *
+     * @param string $buffer
+     * @param string $out
+     *
+     * @return array
+     */
+    public function parseMediaQueryList($buffer, &$out)
+    {
+        $this->count           = 0;
+        $this->env             = null;
+        $this->inParens        = false;
+        $this->eatWhiteDefault = true;
+        $this->buffer          = (string) $buffer;
+
+        $this->saveEncoding();
+
+
+        $isMediaQuery = $this->mediaQueryList($out);
+
+        $this->restoreEncoding();
+
+        return $isMediaQuery;
+    }
+
     /**
      * Parse a single chunk off the head of the buffer and append it to the
      * current parse environment.
@@ -313,7 +343,7 @@ class Parser
             if ($this->literal('@at-root', 8) &&
                 ($this->selectors($selector) || true) &&
                 ($this->map($with) || true) &&
-                $this->matchChar('{')
+                $this->matchChar('{', false)
             ) {
                 $atRoot = $this->pushSpecialBlock(Type::T_AT_ROOT, $s);
                 $atRoot->selector = $selector;
@@ -324,7 +354,7 @@ class Parser
 
             $this->seek($s);
 
-            if ($this->literal('@media', 6) && $this->mediaQueryList($mediaQueryList) && $this->matchChar('{')) {
+            if ($this->literal('@media', 6) && $this->mediaQueryList($mediaQueryList) && $this->matchChar('{', false)) {
                 $media = $this->pushSpecialBlock(Type::T_MEDIA, $s);
                 $media->queryList = $mediaQueryList[2];
 
@@ -336,7 +366,7 @@ class Parser
             if ($this->literal('@mixin', 6) &&
                 $this->keyword($mixinName) &&
                 ($this->argumentDef($args) || true) &&
-                $this->matchChar('{')
+                $this->matchChar('{', false)
             ) {
                 $mixin = $this->pushSpecialBlock(Type::T_MIXIN, $s);
                 $mixin->name = $mixinName;
@@ -418,7 +448,7 @@ class Parser
             if ($this->literal('@function', 9) &&
                 $this->keyword($fnName) &&
                 $this->argumentDef($args) &&
-                $this->matchChar('{')
+                $this->matchChar('{', false)
             ) {
                 $func = $this->pushSpecialBlock(Type::T_FUNCTION, $s);
                 $func->name = $fnName;
@@ -457,7 +487,7 @@ class Parser
                 $this->genericList($varNames, 'variable', ',', false) &&
                 $this->literal('in', 2) &&
                 $this->valueList($list) &&
-                $this->matchChar('{')
+                $this->matchChar('{', false)
             ) {
                 $each = $this->pushSpecialBlock(Type::T_EACH, $s);
 
@@ -474,7 +504,7 @@ class Parser
 
             if ($this->literal('@while', 6) &&
                 $this->expression($cond) &&
-                $this->matchChar('{')
+                $this->matchChar('{', false)
             ) {
                 $while = $this->pushSpecialBlock(Type::T_WHILE, $s);
                 $while->cond = $cond;
@@ -491,7 +521,7 @@ class Parser
                 ($this->literal('through', 7) ||
                     ($forUntil = true && $this->literal('to', 2))) &&
                 $this->expression($end) &&
-                $this->matchChar('{')
+                $this->matchChar('{', false)
             ) {
                 $for = $this->pushSpecialBlock(Type::T_FOR, $s);
                 $for->var = $varName[1];
@@ -504,7 +534,7 @@ class Parser
 
             $this->seek($s);
 
-            if ($this->literal('@if', 3) && $this->valueList($cond) && $this->matchChar('{')) {
+            if ($this->literal('@if', 3) && $this->valueList($cond) && $this->matchChar('{', false)) {
                 $if = $this->pushSpecialBlock(Type::T_IF, $s);
                 $if->cond = $cond;
                 $if->cases = [];
@@ -561,9 +591,9 @@ class Parser
                 list(, $if) = $last;
 
                 if ($this->literal('@else', 5)) {
-                    if ($this->matchChar('{')) {
+                    if ($this->matchChar('{', false)) {
                         $else = $this->pushSpecialBlock(Type::T_ELSE, $s);
-                    } elseif ($this->literal('if', 2) && $this->valueList($cond) && $this->matchChar('{')) {
+                    } elseif ($this->literal('if', 2) && $this->valueList($cond) && $this->matchChar('{', false)) {
                         $else = $this->pushSpecialBlock(Type::T_ELSEIF, $s);
                         $else->cond = $cond;
                     }
@@ -601,11 +631,23 @@ class Parser
 
             $this->seek($s);
 
+            if ($this->literal('@supports', 9) &&
+                ($t1=$this->supportsQuery($supportQuery)) &&
+                ($t2=$this->matchChar('{', false)) ) {
+                $directive = $this->pushSpecialBlock(Type::T_DIRECTIVE, $s);
+                $directive->name = 'supports';
+                $directive->value = $supportQuery;
+
+                return true;
+            }
+
+            $this->seek($s);
+
             // doesn't match built in directive, do generic one
             if ($this->matchChar('@', false) &&
                 $this->keyword($dirName) &&
                 ($this->variable($dirValue) || $this->openString('{', $dirValue) || true) &&
-                $this->matchChar('{')
+                $this->matchChar('{', false)
             ) {
                 if ($dirName === 'media') {
                     $directive = $this->pushSpecialBlock(Type::T_MEDIA, $s);
@@ -688,9 +730,11 @@ class Parser
                 $foundSomething = true;
             }
 
-            if ($this->matchChar('{')) {
+            if ($this->matchChar('{', false)) {
                 $propBlock = $this->pushSpecialBlock(Type::T_NESTED_PROPERTY, $s);
                 $propBlock->prefix = $name;
+                $propBlock->hasValue = $foundSomething;
+
                 $foundSomething = true;
             } elseif ($foundSomething) {
                 $foundSomething = $this->end();
@@ -704,9 +748,15 @@ class Parser
         $this->seek($s);
 
         // closing a block
-        if ($this->matchChar('}')) {
+        if ($this->matchChar('}', false)) {
             $block = $this->popBlock();
 
+            if (!isset($block->type) || $block->type !== Type::T_IF) {
+                if ($this->env->parent) {
+                    $this->append(null); // collect comments before next statement if needed
+                }
+            }
+
             if (isset($block->type) && $block->type === Type::T_INCLUDE) {
                 $include = $block->child;
                 unset($block->child);
@@ -717,6 +767,15 @@ class Parser
                 $this->append([$type, $block], $s);
             }
 
+            // collect comments just after the block closing if needed
+            if ($this->eatWhiteDefault) {
+                $this->whitespace();
+
+                if ($this->env->comments) {
+                    $this->append(null);
+                }
+            }
+
             return true;
         }
 
@@ -764,6 +823,15 @@ class Parser
 
         $this->env = $b;
 
+        // collect comments at the begining of a block if needed
+        if ($this->eatWhiteDefault) {
+            $this->whitespace();
+
+            if ($this->env->comments) {
+                $this->append(null);
+            }
+        }
+
         return $b;
     }
 
@@ -792,6 +860,13 @@ class Parser
      */
     protected function popBlock()
     {
+
+        // collect comments ending just before of a block closing
+        if ($this->env->comments) {
+            $this->append(null);
+        }
+
+        // pop the block
         $block = $this->env;
 
         if (empty($block->parent)) {
@@ -807,13 +882,6 @@ class Parser
 
         unset($block->parent);
 
-        $comments = $block->comments;
-
-        if ($comments) {
-            $this->env->comments = $comments;
-            unset($block->comments);
-        }
-
         return $block;
     }
 
@@ -1049,11 +1117,13 @@ class Parser
      */
     protected function appendComment($comment)
     {
-        if ($comment[0] === Type::T_COMMENT && is_string($comment[1])) {
-            $comment[1] = substr(preg_replace(['/^\s+/m', '/^(.)/m'], ['', ' \1'], $comment[1]), 1);
-        }
+        if (! $this->discardComments) {
+            if ($comment[0] === Type::T_COMMENT && is_string($comment[1])) {
+                $comment[1] = substr(preg_replace(['/^\s+/m', '/^(.)/m'], ['', ' \1'], $comment[1]), 1);
+            }
 
-        $this->env->comments[] = $comment;
+            $this->env->comments[] = $comment;
+        }
     }
 
     /**
@@ -1162,6 +1232,118 @@ class Parser
         return true;
     }
 
+    /**
+     * Parse supports query
+     *
+     * @param array $out
+     *
+     * @return boolean
+     */
+    protected function supportsQuery(&$out)
+    {
+        $expressions = null;
+        $parts = [];
+
+        $s = $this->count;
+
+        $not = false;
+        if (($this->literal('not', 3) && ($not = true) || true) &&
+            $this->matchChar('(') &&
+            ($this->expression($property)) &&
+            $this->literal(': ', 2) &&
+            $this->valueList($value) &&
+            $this->matchChar(')')) {
+            $support = [Type::T_STRING, '', [[Type::T_KEYWORD, ($not ? 'not ' : '') . '(']]];
+            $support[2][] = $property;
+            $support[2][] = [Type::T_KEYWORD, ': '];
+            $support[2][] = $value;
+            $support[2][] = [Type::T_KEYWORD, ')'];
+
+            $parts[] = $support;
+            $s = $this->count;
+        } else {
+            $this->seek($s);
+        }
+
+        if ($this->matchChar('(') &&
+            $this->supportsQuery($subQuery) &&
+            $this->matchChar(')')) {
+            $parts[] = [Type::T_STRING, '', [[Type::T_KEYWORD, '('], $subQuery, [Type::T_KEYWORD, ')']]];
+            $s = $this->count;
+        } else {
+            $this->seek($s);
+        }
+
+        if ($this->literal('not', 3) &&
+            $this->supportsQuery($subQuery)) {
+            $parts[] = [Type::T_STRING, '', [[Type::T_KEYWORD, 'not '], $subQuery]];
+            $s = $this->count;
+        } else {
+            $this->seek($s);
+        }
+
+        if ($this->literal('selector(', 9) &&
+            $this->selector($selector) &&
+            $this->matchChar(')')) {
+            $support = [Type::T_STRING, '', [[Type::T_KEYWORD, 'selector(']]];
+
+            $selectorList = [Type::T_LIST, '', []];
+            foreach ($selector as $sc) {
+                $compound = [Type::T_STRING, '', []];
+                foreach ($sc as $scp) {
+                    if (is_array($scp)) {
+                        $compound[2][] = $scp;
+                    } else {
+                        $compound[2][] = [Type::T_KEYWORD, $scp];
+                    }
+                }
+                $selectorList[2][] = $compound;
+            }
+            $support[2][] = $selectorList;
+            $support[2][] = [Type::T_KEYWORD, ')'];
+            $parts[] = $support;
+            $s = $this->count;
+        } else {
+            $this->seek($s);
+        }
+
+        if ($this->variable($var) or $this->interpolation($var)) {
+            $parts[] = $var;
+            $s = $this->count;
+        } else {
+            $this->seek($s);
+        }
+
+        if ($this->literal('and', 3) &&
+            $this->genericList($expressions, 'supportsQuery', ' and', false)) {
+            array_unshift($expressions[2], [Type::T_STRING, '', $parts]);
+            $parts = [$expressions];
+            $s = $this->count;
+        } else {
+            $this->seek($s);
+        }
+
+        if ($this->literal('or', 2) &&
+            $this->genericList($expressions, 'supportsQuery', ' or', false)) {
+            array_unshift($expressions[2], [Type::T_STRING, '', $parts]);
+            $parts = [$expressions];
+            $s = $this->count;
+        } else {
+            $this->seek($s);
+        }
+
+        if (count($parts)) {
+            if ($this->eatWhiteDefault) {
+                $this->whitespace();
+            }
+            $out = [Type::T_STRING, '', $parts];
+            return true;
+        }
+
+        return false;
+    }
+
+
     /**
      * Parse media expression
      *
@@ -1320,9 +1502,12 @@ class Parser
     protected function expression(&$out)
     {
         $s = $this->count;
+        $discard = $this->discardComments;
+        $this->discardComments = true;
 
         if ($this->matchChar('(')) {
             if ($this->parenExpression($out, $s, ")")) {
+                $this->discardComments = $discard;
                 return true;
             }
 
@@ -1335,6 +1520,7 @@ class Parser
                     $out = [Type::T_STRING, '', [ '[', $out, ']' ]];
                 }
 
+                $this->discardComments = $discard;
                 return true;
             }
 
@@ -1344,9 +1530,11 @@ class Parser
         if ($this->value($lhs)) {
             $out = $this->expHelper($lhs, 0);
 
+            $this->discardComments = $discard;
             return true;
         }
 
+        $this->discardComments = $discard;
         return false;
     }
 
@@ -1978,10 +2166,11 @@ class Parser
                     $content[] = $m[2] . "'";
                 } elseif ($this->literal("\\", 1, false)) {
                     $content[] = $m[2] . "\\";
-                } elseif ($this->literal("\r\n", 2, false)
-                  || $this->matchChar("\r", false)
-                  || $this->matchChar("\n", false)
-                  || $this->matchChar("\f", false)) {
+                } elseif ($this->literal("\r\n", 2, false) ||
+                  $this->matchChar("\r", false) ||
+                  $this->matchChar("\n", false) ||
+                  $this->matchChar("\f", false)
+                ) {
                     // this is a continuation escaping, to be ignored
                 } else {
                     $content[] = $m[2];
@@ -2022,11 +2211,12 @@ class Parser
     /**
      * Parse keyword or interpolation
      *
-     * @param array $out
+     * @param array   $out
+     * @param boolean $restricted
      *
      * @return boolean
      */
-    protected function mixedKeyword(&$out)
+    protected function mixedKeyword(&$out, $restricted = false)
     {
         $parts = [];
 
@@ -2034,7 +2224,7 @@ class Parser
         $this->eatWhiteDefault = false;
 
         for (;;) {
-            if ($this->keyword($key)) {
+            if ($restricted ? $this->restrictedKeyword($key) : $this->keyword($key)) {
                 $parts[] = $key;
                 continue;
             }
@@ -2149,7 +2339,7 @@ class Parser
                 $out = $value;
             } else {
                 if ($lookWhite) {
-                    $left = preg_match('/\s/', $this->buffer[$s - 1]) ? ' ' : '';
+                    $left = ($s > 0 && preg_match('/\s/', $this->buffer[$s - 1])) ? ' ' : '';
                     $right = preg_match('/\s/', $this->buffer[$this->count]) ? ' ': '';
                 } else {
                     $left = $right = false;
@@ -2406,7 +2596,7 @@ class Parser
                     $part = ':';
                 }
 
-                if ($this->mixedKeyword($nameParts)) {
+                if ($this->mixedKeyword($nameParts, true)) {
                     $parts[] = $part;
 
                     foreach ($nameParts as $sub) {
@@ -2489,7 +2679,7 @@ class Parser
                 continue;
             }
 
-            if ($this->keyword($name)) {
+            if ($this->restrictedKeyword($name)) {
                 $parts[] = $name;
                 continue;
             }
@@ -2555,6 +2745,27 @@ class Parser
         return false;
     }
 
+    /**
+     * Parse a keyword that should not start with a number
+     *
+     * @param string  $word
+     * @param boolean $eatWhitespace
+     *
+     * @return boolean
+     */
+    protected function restrictedKeyword(&$word, $eatWhitespace = null)
+    {
+        $s = $this->count;
+
+        if ($this->keyword($word, $eatWhitespace) && (ord($word[0]) > 57 || ord($word[0]) < 48)) {
+            return true;
+        }
+
+        $this->seek($s);
+
+        return false;
+    }
+
     /**
      * Parse a placeholder
      *
index 56c7165..d5e6dc0 100644 (file)
@@ -18,5 +18,5 @@ namespace ScssPhp\ScssPhp;
  */
 class Version
 {
-    const VERSION = 'v1.0.0';
+    const VERSION = 'v1.0.2';
 }
index 0fd029e..a61887d 100644 (file)
     <location>scssphp</location>
     <name>scssphp</name>
     <license>MIT</license>
-    <version>1.0.0</version>
+    <version>1.0.2</version>
   </library>
   <library>
     <location>spout</location>
index a1b1003..dab84aa 100644 (file)
@@ -14,7 +14,7 @@ information provided here is intended especially for developers.
     * allow_switch()
 * Remove duplicate font-awesome SCSS, Please see /theme/boost/scss/fontawesome for usage (MDL-65936)
 * Remove lib/pear/Crypt/CHAP.php (MDL-65747)
-* Upgrade scssphp to v1.0.0, This involves renaming classes from Leafo => ScssPhp as the repo has changed.
+* Upgrade scssphp to v1.0.2, This involves renaming classes from Leafo => ScssPhp as the repo has changed.
 
 === 3.7 ===