MDL-67379 libraries: Upgrade scssphp to 1.0.6
authorShamim Rezaie <shamim@moodle.com>
Mon, 16 Dec 2019 10:35:35 +0000 (21:35 +1100)
committerShamim Rezaie <shamim@moodle.com>
Wed, 15 Jan 2020 13:24:09 +0000 (00:24 +1100)
13 files changed:
lib/scssphp/Cache.php
lib/scssphp/Colors.php
lib/scssphp/Compiler.php
lib/scssphp/Formatter.php
lib/scssphp/Formatter/Expanded.php
lib/scssphp/Formatter/Nested.php
lib/scssphp/Node/Number.php
lib/scssphp/Parser.php
lib/scssphp/SourceMap/Base64VLQ.php
lib/scssphp/SourceMap/Base64VLQEncoder.php [deleted file]
lib/scssphp/Version.php
lib/scssphp/moodle_readme.txt
lib/thirdpartylibs.xml

index 1cf496f..422497f 100644 (file)
@@ -57,12 +57,12 @@ class Cache
     public function __construct($options)
     {
         // check $cacheDir
-        if (isset($options['cache_dir'])) {
-            self::$cacheDir = $options['cache_dir'];
+        if (isset($options['cacheDir'])) {
+            self::$cacheDir = $options['cacheDir'];
         }
 
         if (empty(self::$cacheDir)) {
-            throw new Exception('cache_dir not set');
+            throw new Exception('cacheDir not set');
         }
 
         if (isset($options['prefix'])) {
@@ -74,7 +74,7 @@ class Cache
         }
 
         if (isset($options['forceRefresh'])) {
-            self::$forceRefresh = $options['force_refresh'];
+            self::$forceRefresh = $options['forceRefresh'];
         }
 
         self::checkCacheDir();
@@ -97,7 +97,7 @@ class Cache
     {
         $fileCache = self::$cacheDir . self::cacheName($operation, $what, $options);
 
-        if ((! self::$forceRefresh || (self::$forceRefresh === 'once' &&
+        if (((self::$forceRefresh === false) || (self::$forceRefresh === 'once' &&
             isset(self::$refreshed[$fileCache]))) && file_exists($fileCache)
         ) {
             $cacheTime = filemtime($fileCache);
@@ -176,13 +176,13 @@ class Cache
         self::$cacheDir = str_replace('\\', '/', self::$cacheDir);
         self::$cacheDir = rtrim(self::$cacheDir, '/') . '/';
 
-        if (! file_exists(self::$cacheDir)) {
+        if (! is_dir(self::$cacheDir)) {
             if (! mkdir(self::$cacheDir)) {
                 throw new Exception('Cache directory couldn\'t be created: ' . self::$cacheDir);
             }
-        } elseif (! is_dir(self::$cacheDir)) {
-            throw new Exception('Cache directory doesn\'t exist: ' . self::$cacheDir);
-        } elseif (! is_writable(self::$cacheDir)) {
+        }
+
+        if (! is_writable(self::$cacheDir)) {
             throw new Exception('Cache directory isn\'t writable: ' . self::$cacheDir);
         }
     }
index ad45924..ef6409a 100644 (file)
@@ -25,7 +25,7 @@ class Colors
      *
      * @var array
      */
-    public static $cssColors = [
+    protected static $cssColors = [
         'aliceblue' => '240,248,255',
         'antiquewhite' => '250,235,215',
         'aqua' => '0,255,255',
@@ -176,4 +176,71 @@ class Colors
         'yellow' => '255,255,0',
         'yellowgreen' => '154,205,50',
     ];
+
+    /**
+     * Convert named color in a [r,g,b[,a]] array
+     *
+     * @param string $colorName
+     *
+     * @return array|null
+     */
+    public static function colorNameToRGBa($colorName)
+    {
+        if (is_string($colorName) && isset(static::$cssColors[$colorName])) {
+            $rgba = explode(',', static::$cssColors[$colorName]);
+
+            // only case with opacity is transparent, with opacity=0, so we can intval on opacity also
+            $rgba = array_map('intval', $rgba);
+
+            return $rgba;
+        }
+
+        return null;
+    }
+
+    /**
+     * Reverse conversion : from RGBA to a color name if possible
+     *
+     * @param integer $r
+     * @param integer $g
+     * @param integer $b
+     * @param integer $a
+     *
+     * @return string|null
+     */
+    public static function RGBaToColorName($r, $g, $b, $a = 1)
+    {
+        static $reverseColorTable = null;
+
+        if (! is_numeric($r) || ! is_numeric($g) || ! is_numeric($b) || ! is_numeric($a)) {
+            return null;
+        }
+
+        if ($a < 1) {
+            # specific case we dont' revert according to spec
+            #if (! $a && ! $r && ! $g && ! $b) {
+            #    return 'transparent';
+            #}
+
+            return null;
+        }
+
+        if (is_null($reverseColorTable)) {
+            $reverseColorTable = [];
+
+            foreach (static::$cssColors as $name => $rgb_str) {
+                $rgb_str = explode(',', $rgb_str);
+
+                if (count($rgb_str) == 3) {
+                    $reverseColorTable[intval($rgb_str[0])][intval($rgb_str[1])][intval($rgb_str[2])] = $name;
+                }
+            }
+        }
+
+        if (isset($reverseColorTable[intval($r)][intval($g)][intval($b)])) {
+            return $reverseColorTable[intval($r)][intval($g)][intval($b)];
+        }
+
+        return null;
+    }
 }
index 75c5d67..711d338 100644 (file)
@@ -164,6 +164,8 @@ class Compiler
 
     /**
      * Constructor
+     *
+     * @param array|null $cacheOptions
      */
     public function __construct($cacheOptions = null)
     {
@@ -173,8 +175,15 @@ class Compiler
         if ($cacheOptions) {
             $this->cache = new Cache($cacheOptions);
         }
+
+        $this->stderr = fopen('php://stderr', 'w');
     }
 
+    /**
+     * Get compiler options
+     *
+     * @return array
+     */
     public function getCompileOptions()
     {
         $options = [
@@ -190,6 +199,16 @@ class Compiler
         return $options;
     }
 
+    /**
+     * Set an alternative error output stream, for testing purpose only
+     *
+     * @param resource $handle
+     */
+    public function setErrorOuput($handle)
+    {
+        $this->stderr = $handle;
+    }
+
     /**
      * Compile scss
      *
@@ -210,7 +229,7 @@ class Compiler
             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 (! is_file($file) || filemtime($file) !== $mtime) {
                         unset($cache);
                         break;
                     }
@@ -234,7 +253,6 @@ class Compiler
         $this->storeEnv       = null;
         $this->charsetSeen    = null;
         $this->shouldEvaluate = null;
-        $this->stderr         = fopen('php://stderr', 'w');
 
         $this->parser = $this->parserFactory($path);
         $tree         = $this->parser->parse($code);
@@ -329,9 +347,9 @@ class Compiler
     /**
      * Push extends
      *
-     * @param array     $target
-     * @param array     $origin
-     * @param \stdClass $block
+     * @param array      $target
+     * @param array      $origin
+     * @param array|null $block
      */
     protected function pushExtends($target, $origin, $block)
     {
@@ -362,12 +380,12 @@ class Compiler
     protected function makeOutputBlock($type, $selectors = null)
     {
         $out = new OutputBlock;
-        $out->type         = $type;
-        $out->lines        = [];
-        $out->children     = [];
-        $out->parent       = $this->scope;
-        $out->selectors    = $selectors;
-        $out->depth        = $this->env->depth;
+        $out->type      = $type;
+        $out->lines     = [];
+        $out->children  = [];
+        $out->parent    = $this->scope;
+        $out->selectors = $selectors;
+        $out->depth     = $this->env->depth;
 
         if ($this->env->block instanceof Block) {
             $out->sourceName   = $this->env->block->sourceName;
@@ -501,6 +519,9 @@ class Compiler
                 if (count($new) && is_string($new[count($new) - 1]) &&
                     strlen($part) && substr($part, -1) === ')' && strpos($part, '(') === false
                 ) {
+                    while (count($new)>1 && substr($new[count($new) - 1], -1) !== '(') {
+                        $part = array_pop($new) . $part;
+                    }
                     $new[count($new) - 1] .= $part;
                 } else {
                     $new[] = $part;
@@ -522,13 +543,13 @@ class Compiler
     protected function matchExtends($selector, &$out, $from = 0, $initial = true)
     {
         static $partsPile = [];
-
         $selector = $this->glueFunctionSelectors($selector);
 
         if (count($selector) == 1 && in_array(reset($selector), $partsPile)) {
             return;
         }
 
+        $outRecurs = [];
         foreach ($selector as $i => $part) {
             if ($i < $from) {
                 continue;
@@ -544,11 +565,10 @@ class Compiler
                 }
             }
 
-            if ($this->matchExtendsSingle($part, $origin)) {
-                $partsPile[] = $part;
+            $partsPile[] = $part;
+            if ($this->matchExtendsSingle($part, $origin, $initial)) {
                 $after       = array_slice($selector, $i + 1);
                 $before      = array_slice($selector, 0, $i);
-
                 list($before, $nonBreakableBefore) = $this->extractRelationshipFromFragment($before);
 
                 foreach ($origin as $new) {
@@ -560,6 +580,9 @@ class Compiler
                             $k++;
                         }
                     }
+                    if (count($nonBreakableBefore) and $k == count($new)) {
+                        $k--;
+                    }
 
                     $replacement = [];
                     $tempReplacement = $k > 0 ? array_slice($new, $k) : $new;
@@ -596,36 +619,93 @@ class Compiler
                         continue;
                     }
 
-                    $out[] = $result;
+                    $this->pushOrMergeExtentedSelector($out, $result);
 
                     // recursively check for more matches
                     $startRecurseFrom = count($before) + min(count($nonBreakableBefore), count($mergedBefore));
-                    $this->matchExtends($result, $out, $startRecurseFrom, false);
+                    if (count($origin) > 1) {
+                        $this->matchExtends($result, $out, $startRecurseFrom, false);
+                    } else {
+                        $this->matchExtends($result, $outRecurs, $startRecurseFrom, false);
+                    }
 
                     // selector sequence merging
                     if (! empty($before) && count($new) > 1) {
                         $preSharedParts = $k > 0 ? array_slice($before, 0, $k) : [];
                         $postSharedParts = $k > 0 ? array_slice($before, $k) : $before;
 
-                        list($betweenSharedParts, $nonBreakable2) = $this->extractRelationshipFromFragment($afterBefore);
+                        list($betweenSharedParts, $nonBreakabl2) = $this->extractRelationshipFromFragment($afterBefore);
 
                         $result2 = array_merge(
                             $preSharedParts,
                             $betweenSharedParts,
                             $postSharedParts,
-                            $nonBreakable2,
+                            $nonBreakabl2,
                             $nonBreakableBefore,
                             $replacement,
                             $after
                         );
 
-                        $out[] = $result2;
+                        $this->pushOrMergeExtentedSelector($out, $result2);
                     }
                 }
+            }
+            array_pop($partsPile);
+        }
+        while (count($outRecurs)) {
+            $result = array_shift($outRecurs);
+            $this->pushOrMergeExtentedSelector($out, $result);
+        }
+    }
+
+    /**
+     * Test a part for being a pseudo selector
+     * @param string $part
+     * @param array $matches
+     * @return bool
+     */
+    protected function isPseudoSelector($part, &$matches)
+    {
+        if (strpos($part, ":") === 0
+            && preg_match(",^::?([\w-]+)\((.+)\)$,", $part, $matches)) {
+            return true;
+        }
+        return false;
+    }
 
-                array_pop($partsPile);
+    /**
+     * Push extended selector except if
+     *  - this is a pseudo selector
+     *  - same as previous
+     *  - in a white list
+     * in this case we merge the pseudo selector content
+     * @param array $out
+     * @param array $extended
+     */
+    protected function pushOrMergeExtentedSelector(&$out, $extended)
+    {
+        if (count($out) && count($extended) === 1 && count(reset($extended)) === 1) {
+            $single = reset($extended);
+            $part = reset($single);
+            if ($this->isPseudoSelector($part, $matchesExtended)
+              && in_array($matchesExtended[1], [ 'slotted' ])) {
+                $prev = end($out);
+                $prev = $this->glueFunctionSelectors($prev);
+                if (count($prev) === 1 && count(reset($prev)) === 1) {
+                    $single = reset($prev);
+                    $part = reset($single);
+                    if ($this->isPseudoSelector($part, $matchesPrev)
+                      && $matchesPrev[1] === $matchesExtended[1]) {
+                        $extended = explode($matchesExtended[1] . '(', $matchesExtended[0], 2);
+                        $extended[1] = $matchesPrev[2] . ", " . $extended[1];
+                        $extended = implode($matchesExtended[1] . '(', $extended);
+                        $extended = [ [ $extended ]];
+                        array_pop($out);
+                    }
+                }
             }
         }
+        $out[] = $extended;
     }
 
     /**
@@ -633,10 +713,11 @@ class Compiler
      *
      * @param array $rawSingle
      * @param array $outOrigin
+     * @param bool $initial
      *
      * @return boolean
      */
-    protected function matchExtendsSingle($rawSingle, &$outOrigin)
+    protected function matchExtendsSingle($rawSingle, &$outOrigin, $initial = true)
     {
         $counts = [];
         $single = [];
@@ -666,17 +747,41 @@ class Compiler
             $extendingDecoratedTag = preg_match('/^[a-z0-9]+$/i', $single[0], $matches) ? $matches[0] : false;
         }
 
-        foreach ($single as $part) {
+        $outOrigin = [];
+        $found = false;
+
+        foreach ($single as $k => $part) {
             if (isset($this->extendsMap[$part])) {
                 foreach ($this->extendsMap[$part] as $idx) {
                     $counts[$idx] = isset($counts[$idx]) ? $counts[$idx] + 1 : 1;
                 }
             }
+            if ($initial
+                && $this->isPseudoSelector($part, $matches)
+                && ! in_array($matches[1], [ 'not' ])) {
+                $buffer    = $matches[2];
+                $parser    = $this->parserFactory(__METHOD__);
+                if ($parser->parseSelector($buffer, $subSelectors)) {
+                    foreach ($subSelectors as $ksub => $subSelector) {
+                        $subExtended = [];
+                        $this->matchExtends($subSelector, $subExtended, 0, false);
+                        if ($subExtended) {
+                            $subSelectorsExtended = $subSelectors;
+                            $subSelectorsExtended[$ksub] = $subExtended;
+                            foreach ($subSelectorsExtended as $ksse => $sse) {
+                                $subSelectorsExtended[$ksse] = $this->collapseSelectors($sse);
+                            }
+                            $subSelectorsExtended = implode(', ', $subSelectorsExtended);
+                            $singleExtended = $single;
+                            $singleExtended[$k] = str_replace("(".$buffer.")", "($subSelectorsExtended)", $part);
+                            $outOrigin[] = [ $singleExtended ];
+                            $found = true;
+                        }
+                    }
+                }
+            }
         }
 
-        $outOrigin = [];
-        $found = false;
-
         foreach ($counts as $idx => $count) {
             list($target, $origin, /* $block */) = $this->extends[$idx];
 
@@ -738,12 +843,13 @@ class Compiler
     {
         $parents = [];
         $children = [];
+
         $j = $i = count($fragment);
 
         for (;;) {
             $children = $j != $i ? array_slice($fragment, $j, $i - $j) : [];
-            $parents = array_slice($fragment, 0, $j);
-            $slice = end($parents);
+            $parents  = array_slice($fragment, 0, $j);
+            $slice    = end($parents);
 
             if (empty($slice) || ! $this->isImmediateRelationshipCombinator($slice[0])) {
                 break;
@@ -765,22 +871,25 @@ class Compiler
      */
     protected function combineSelectorSingle($base, $other)
     {
-        $tag = [];
-        $out = [];
-        $wasTag = true;
+        $tag    = [];
+        $out    = [];
+        $wasTag = false;
 
-        foreach ([$base, $other] as $single) {
+        foreach ([array_reverse($base), array_reverse($other)] as $single) {
             foreach ($single as $part) {
-                if (preg_match('/^[\[.:#]/', $part)) {
+                if (preg_match('/^[\[:]/', $part)) {
                     $out[] = $part;
                     $wasTag = false;
+                } elseif (preg_match('/^[\.#]/', $part)) {
+                    array_unshift($out, $part);
+                    $wasTag = false;
                 } elseif (preg_match('/^[^_-]/', $part)) {
                     $tag[] = $part;
                     $wasTag = true;
                 } elseif ($wasTag) {
                     $tag[count($tag) - 1] .= $part;
                 } else {
-                    $out[count($out) - 1] .= $part;
+                    $out[= $part;
                 }
             }
         }
@@ -832,16 +941,17 @@ class Compiler
 
             if ($needsWrap) {
                 $wrapped = new Block;
-                $wrapped->sourceName = $media->sourceName;
-                $wrapped->sourceIndex = $media->sourceIndex;
-                $wrapped->sourceLine = $media->sourceLine;
+                $wrapped->sourceName   = $media->sourceName;
+                $wrapped->sourceIndex  = $media->sourceIndex;
+                $wrapped->sourceLine   = $media->sourceLine;
                 $wrapped->sourceColumn = $media->sourceColumn;
-                $wrapped->selectors = [];
-                $wrapped->comments = [];
-                $wrapped->parent = $media;
-                $wrapped->children = $media->children;
+                $wrapped->selectors    = [];
+                $wrapped->comments     = [];
+                $wrapped->parent       = $media;
+                $wrapped->children     = $media->children;
 
                 $media->children = [[Type::T_BLOCK, $wrapped]];
+
                 if (isset($this->lineNumberStyle)) {
                     $annotation = $this->makeOutputBlock(Type::T_COMMENT);
                     $annotation->depth = 0;
@@ -898,20 +1008,29 @@ class Compiler
     /**
      * Compile directive
      *
-     * @param \ScssPhp\ScssPhp\Block $block
+     * @param \ScssPhp\ScssPhp\Block|array $block
+     * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $out
      */
-    protected function compileDirective(Block $block)
+    protected function compileDirective($directive, OutputBlock $out)
     {
-        $s = '@' . $block->name;
+        if (is_array($directive)) {
+            $s = '@' . $directive[0];
+            if (! empty($directive[1])) {
+                $s .= ' ' . $this->compileValue($directive[1]);
+            }
+            $this->appendRootDirective($s . ';', $out);
+        } else {
+            $s = '@' . $directive->name;
 
-        if (! empty($block->value)) {
-            $s .= ' ' . $this->compileValue($block->value);
-        }
+            if (! empty($directive->value)) {
+                $s .= ' ' . $this->compileValue($directive->value);
+            }
 
-        if ($block->name === 'keyframes' || substr($block->name, -10) === '-keyframes') {
-            $this->compileKeyframeBlock($block, [$s]);
-        } else {
-            $this->compileNestedBlock($block, [$s]);
+            if ($directive->name === 'keyframes' || substr($directive->name, -10) === '-keyframes') {
+                $this->compileKeyframeBlock($directive, [$s]);
+            } else {
+                $this->compileNestedBlock($directive, [$s]);
+            }
         }
     }
 
@@ -978,6 +1097,7 @@ class Compiler
     protected function filterScopeWithWithout($scope, $with, $without)
     {
         $filteredScopes = [];
+        $childStash = [];
 
         if ($scope->type === TYPE::T_ROOT) {
             return $scope;
@@ -985,6 +1105,7 @@ class Compiler
 
         // start from the root
         while ($scope->parent && $scope->parent->type !== TYPE::T_ROOT) {
+            array_unshift($childStash, $scope);
             $scope = $scope->parent;
         }
 
@@ -996,8 +1117,8 @@ class Compiler
             if ($this->isWith($scope, $with, $without)) {
                 $s = clone $scope;
                 $s->children = [];
-                $s->lines = [];
-                $s->parent = null;
+                $s->lines    = [];
+                $s->parent   = null;
 
                 if ($s->type !== Type::T_MEDIA && $s->type !== Type::T_DIRECTIVE) {
                     $s->selectors = [];
@@ -1006,7 +1127,9 @@ class Compiler
                 $filteredScopes[] = $s;
             }
 
-            if ($scope->children) {
+            if (count($childStash)) {
+                $scope = array_shift($childStash);
+            } elseif ($scope->children) {
                 $scope = end($scope->children);
             } else {
                 $scope = null;
@@ -1027,8 +1150,9 @@ class Compiler
         while (count($filteredScopes)) {
             $s = array_shift($filteredScopes);
             $s->parent = $p;
-            $p->children[] = &$s;
-            $p = $s;
+            $p->children[] = $s;
+            $newScope = &$p->children[0];
+            $p = &$p->children[0];
         }
 
         return $newScope;
@@ -1139,8 +1263,9 @@ class Compiler
         foreach ($envs as $e) {
             if ($e->block && ! $this->isWith($e->block, $with, $without)) {
                 $ec = clone $e;
-                $ec->block = null;
+                $ec->block     = null;
                 $ec->selectors = [];
+
                 $filtered[] = $ec;
             } else {
                 $filtered[] = $e;
@@ -1169,16 +1294,26 @@ class Compiler
             if ($block->type === Type::T_DIRECTIVE) {
                 if (isset($block->name)) {
                     return $this->testWithWithout($block->name, $with, $without);
-                }
-                elseif (isset($block->selectors) && preg_match(',@(\w+),ims', json_encode($block->selectors), $m)) {
+                } elseif (isset($block->selectors) && preg_match(',@(\w+),ims', json_encode($block->selectors), $m)) {
                     return $this->testWithWithout($m[1], $with, $without);
-                }
-                else {
+                } else {
                     return $this->testWithWithout('???', $with, $without);
                 }
             }
-        }
-        elseif (isset($block->selectors)) {
+        } elseif (isset($block->selectors)) {
+            // a selector starting with number is a keyframe rule
+            if (count($block->selectors)) {
+                $s = reset($block->selectors);
+
+                while (is_array($s)) {
+                    $s = reset($s);
+                }
+
+                if (is_object($s) && $s instanceof Node\Number) {
+                    return $this->testWithWithout('keyframes', $with, $without);
+                }
+            }
+
             return $this->testWithWithout('rule', $with, $without);
         }
 
@@ -1191,10 +1326,12 @@ class Compiler
      * @param string $what
      * @param array  $with
      * @param array  $without
-     * @return bool
+     *
+     * @return boolean
      *   true if the block should be kept, false to reject
      */
-    protected function testWithWithout($what, $with, $without) {
+    protected function testWithWithout($what, $with, $without)
+    {
 
         // if without, reject only if in the list (or 'all' is in the list)
         if (count($without)) {
@@ -1263,6 +1400,7 @@ class Compiler
                     array_unshift($child[1]->prefix[2], $prefix);
                     break;
             }
+
             $this->compileChild($child, $nested);
         }
     }
@@ -1295,15 +1433,15 @@ class Compiler
 
             if ($needWrapping) {
                 $wrapped = new Block;
-                $wrapped->sourceName = $block->sourceName;
-                $wrapped->sourceIndex = $block->sourceIndex;
-                $wrapped->sourceLine = $block->sourceLine;
+                $wrapped->sourceName   = $block->sourceName;
+                $wrapped->sourceIndex  = $block->sourceIndex;
+                $wrapped->sourceLine   = $block->sourceLine;
                 $wrapped->sourceColumn = $block->sourceColumn;
-                $wrapped->selectors = [];
-                $wrapped->comments = [];
-                $wrapped->parent = $block;
-                $wrapped->children = $block->children;
-                $wrapped->selfParent = $block->selfParent;
+                $wrapped->selectors    = [];
+                $wrapped->comments     = [];
+                $wrapped->parent       = $block;
+                $wrapped->children     = $block->children;
+                $wrapped->selfParent   = $block->selfParent;
 
                 $block->children = [[Type::T_BLOCK, $wrapped]];
             }
@@ -1380,17 +1518,50 @@ class Compiler
 
             $this->compileChildrenNoReturn($block->children, $out, $block->selfParent);
 
-            // and revert for the following childs of the same block
+            // and revert for the following children of the same block
             if ($selfParentSelectors) {
                 $block->selfParent->selectors = $selfParentSelectors;
             }
         }
 
-        $this->formatter->stripSemicolon($out->lines);
-
         $this->popEnv();
     }
 
+
+    /**
+     * Compile the value of a comment that can have interpolation
+     *
+     * @param array   $value
+     * @param boolean $pushEnv
+     *
+     * @return array|mixed|string
+     */
+    protected function compileCommentValue($value, $pushEnv = false)
+    {
+        $c = $value[1];
+
+        if (isset($value[2])) {
+            if ($pushEnv) {
+                $this->pushEnv();
+                $storeEnv = $this->storeEnv;
+                $this->storeEnv = $this->env;
+            }
+
+            try {
+                $c = $this->compileValue($value[2]);
+            } catch (\Exception $e) {
+                // ignore error in comment compilation which are only interpolation
+            }
+
+            if ($pushEnv) {
+                $this->storeEnv = $storeEnv;
+                $this->popEnv();
+            }
+        }
+
+        return $c;
+    }
+
     /**
      * Compile root level comment
      *
@@ -1399,7 +1570,7 @@ class Compiler
     protected function compileComment($block)
     {
         $out = $this->makeOutputBlock(Type::T_COMMENT);
-        $out->lines[] = is_string($block[1]) ? $block[1] : $this->compileValue($block[1]);
+        $out->lines[] = $this->compileCommentValue($block, true);
 
         $this->scope->children[] = $out;
     }
@@ -1420,8 +1591,8 @@ class Compiler
         // after evaluating interpolates, we might need a second pass
         if ($this->shouldEvaluate) {
             $selectors = $this->revertSelfSelector($selectors);
-            $buffer = $this->collapseSelectors($selectors);
-            $parser = $this->parserFactory(__METHOD__);
+            $buffer    = $this->collapseSelectors($selectors);
+            $parser    = $this->parserFactory(__METHOD__);
 
             if ($parser->parseSelector($buffer, $newSelectors)) {
                 $selectors = array_map([$this, 'evalSelector'], $newSelectors);
@@ -1505,6 +1676,7 @@ class Compiler
                     } else {
                         $output[] = $compound;
                     }
+
                     $glueNext = true;
                 } elseif ($glueNext) {
                     $output[count($output) - 1] .= ' ' . $compound;
@@ -1518,6 +1690,7 @@ class Compiler
                 foreach ($output as &$o) {
                     $o = [Type::T_STRING, '', [$o]];
                 }
+
                 $output = [Type::T_LIST, ' ', $output];
             } else {
                 $output = implode(' ', $output);
@@ -1675,6 +1848,7 @@ class Compiler
             // not displayed but you can var_dump it to deep debug
             $msg = $this->callStackMessage(true, 100);
             $msg = "Infinite calling loop";
+
             $this->throwError($msg);
         }
     }
@@ -1729,7 +1903,7 @@ class Compiler
                 $stm[1]->selfParent = $selfParent;
                 $ret = $this->compileChild($stm, $out);
                 $stm[1]->selfParent = null;
-            } elseif ($selfParent && $stm[0] === TYPE::T_INCLUDE) {
+            } elseif ($selfParent && in_array($stm[0], [TYPE::T_INCLUDE, TYPE::T_EXTEND])) {
                 $stm['selfParent'] = $selfParent;
                 $ret = $this->compileChild($stm, $out);
                 unset($stm['selfParent']);
@@ -1758,9 +1932,12 @@ 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]);
@@ -1779,24 +1956,31 @@ class Compiler
                     $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];
         }
 
@@ -1812,9 +1996,9 @@ class Compiler
      */
     protected function compileMediaQuery($queryList)
     {
-        $start = '@media ';
+        $start   = '@media ';
         $default = trim($start);
-        $out = [];
+        $out     = [];
         $current = "";
 
         foreach ($queryList as $query) {
@@ -1834,6 +2018,7 @@ class Compiler
                 switch ($q[0]) {
                     case Type::T_MEDIA_TYPE:
                         $newType = array_map([$this, 'compileValue'], array_slice($q, 1));
+
                         // combining not and anything else than media type is too risky and should be avoided
                         if (! $mediaTypeOnly) {
                             if (in_array(Type::T_NOT, $newType) || ($type && in_array(Type::T_NOT, $type) )) {
@@ -1854,8 +2039,8 @@ class Compiler
                                 }
 
                                 $current = "";
-                                $type = null;
-                                $parts = [];
+                                $type    = null;
+                                $parts   = [];
                             }
                         }
 
@@ -1987,23 +2172,19 @@ class Compiler
             return $type1;
         }
 
-        $m1 = '';
-        $t1 = '';
-
         if (count($type1) > 1) {
-            $m1= strtolower($type1[0]);
-            $t1= strtolower($type1[1]);
+            $m1 = strtolower($type1[0]);
+            $t1 = strtolower($type1[1]);
         } else {
+            $m1 = '';
             $t1 = strtolower($type1[0]);
         }
 
-        $m2 = '';
-        $t2 = '';
-
         if (count($type2) > 1) {
             $m2 = strtolower($type2[0]);
             $t2 = strtolower($type2[1]);
         } else {
+            $m2 = '';
             $t2 = strtolower($type2[0]);
         }
 
@@ -2058,6 +2239,8 @@ class Compiler
                 return true;
             }
 
+            $this->appendRootDirective('@import ' . $this->compileValue($rawPath). ';', $out);
+
             return false;
         }
 
@@ -2069,17 +2252,21 @@ class Compiler
 
             foreach ($rawPath[2] as $path) {
                 if ($path[0] !== Type::T_STRING) {
+                    $this->appendRootDirective('@import ' . $this->compileValue($rawPath) . ';', $out);
+
                     return false;
                 }
             }
 
             foreach ($rawPath[2] as $path) {
-                $this->compileImport($path, $out);
+                $this->compileImport($path, $out, $once);
             }
 
             return true;
         }
 
+        $this->appendRootDirective('@import ' . $this->compileValue($rawPath) . ';', $out);
+
         return false;
     }
 
@@ -2119,9 +2306,9 @@ class Compiler
 
         // 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->lines[]      = $line;
+        $child->sourceName   = $this->sourceNames[$this->sourceIndex];
+        $child->sourceLine   = $this->sourceLine;
         $child->sourceColumn = $this->sourceColumn;
 
         $root->children[] = $child;
@@ -2133,12 +2320,12 @@ class Compiler
     }
 
     /**
-     * Append lines to the courrent output block:
+     * Append lines to the current output block:
      * directly to the block or through a child if necessary
      *
      * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $out
      * @param string                                 $type
-     * @param string                                 $line
+     * @param string|mixed                           $line
      */
     protected function appendOutputLine(OutputBlock $out, $type, $line)
     {
@@ -2148,20 +2335,20 @@ class Compiler
             $parent = $out->parent;
 
             if (end($parent->children) !== $out) {
-                $outWrite = &$parent->children[count($parent->children)-1];
+                $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];
+            $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;
+                $nextLines->depth  = $out->depth;
 
                 $out->children[] = $nextLines;
                 $outWrite = &$nextLines;
@@ -2182,16 +2369,17 @@ class Compiler
     protected function compileChild($child, OutputBlock $out)
     {
         if (isset($child[Parser::SOURCE_LINE])) {
-            $this->sourceIndex = isset($child[Parser::SOURCE_INDEX]) ? $child[Parser::SOURCE_INDEX] : null;
-            $this->sourceLine = isset($child[Parser::SOURCE_LINE]) ? $child[Parser::SOURCE_LINE] : -1;
+            $this->sourceIndex  = isset($child[Parser::SOURCE_INDEX]) ? $child[Parser::SOURCE_INDEX] : null;
+            $this->sourceLine   = isset($child[Parser::SOURCE_LINE]) ? $child[Parser::SOURCE_LINE] : -1;
             $this->sourceColumn = isset($child[Parser::SOURCE_COLUMN]) ? $child[Parser::SOURCE_COLUMN] : -1;
         } elseif (is_array($child) && isset($child[1]->sourceLine)) {
-            $this->sourceIndex = $child[1]->sourceIndex;
-            $this->sourceLine = $child[1]->sourceLine;
+            $this->sourceIndex  = $child[1]->sourceIndex;
+            $this->sourceLine   = $child[1]->sourceLine;
             $this->sourceColumn = $child[1]->sourceColumn;
         } elseif (! empty($out->sourceLine) && ! empty($out->sourceName)) {
-            $this->sourceLine = $out->sourceLine;
-            $this->sourceIndex = array_search($out->sourceName, $this->sourceNames);
+            $this->sourceLine   = $out->sourceLine;
+            $this->sourceIndex  = array_search($out->sourceName, $this->sourceNames);
+            $this->sourceColumn = $out->sourceColumn;
 
             if ($this->sourceIndex === false) {
                 $this->sourceIndex = null;
@@ -2202,21 +2390,17 @@ class Compiler
             case Type::T_SCSSPHP_IMPORT_ONCE:
                 $rawPath = $this->reduce($child[1]);
 
-                if (! $this->compileImport($rawPath, $out, true)) {
-                    $this->appendRootDirective('@import ' . $this->compileValue($rawPath) . ';', $out);
-                }
+                $this->compileImport($rawPath, $out, true);
                 break;
 
             case Type::T_IMPORT:
                 $rawPath = $this->reduce($child[1]);
 
-                if (! $this->compileImport($rawPath, $out)) {
-                    $this->appendRootDirective('@import ' . $this->compileValue($rawPath) . ';', $out);
-                }
+                $this->compileImport($rawPath, $out);
                 break;
 
             case Type::T_DIRECTIVE:
-                $this->compileDirective($child[1]);
+                $this->compileDirective($child[1], $out);
                 break;
 
             case Type::T_AT_ROOT:
@@ -2242,9 +2426,9 @@ class Compiler
                 list(, $name, $value) = $child;
 
                 if ($name[0] === Type::T_VARIABLE) {
-                    $flags = isset($child[3]) ? $child[3] : [];
+                    $flags     = isset($child[3]) ? $child[3] : [];
                     $isDefault = in_array('!default', $flags);
-                    $isGlobal = in_array('!global', $flags);
+                    $isGlobal  = in_array('!global', $flags);
 
                     if ($isGlobal) {
                         $this->set($name[1], $this->reduce($value), false, $this->rootEnv, $value);
@@ -2252,7 +2436,7 @@ class Compiler
                     }
 
                     $shouldSet = $isDefault &&
-                        (($result = $this->get($name[1], false)) === null ||
+                        (is_null($result = $this->get($name[1], false)) ||
                         $result === static::$null);
 
                     if (! $isDefault || $shouldSet) {
@@ -2263,28 +2447,77 @@ class Compiler
 
                 $compiledName = $this->compileValue($name);
 
-                // handle shorthand syntax: size / line-height
-                if ($compiledName === 'font' || $compiledName === 'grid-row' || $compiledName === 'grid-column') {
+                // handle shorthand syntaxes : size / line-height...
+                if (in_array($compiledName, ['font', 'grid-row', 'grid-column', 'border-radius'])) {
                     if ($value[0] === Type::T_VARIABLE) {
                         // if the font value comes from variable, the content is already reduced
                         // (i.e., formulas were already calculated), so we need the original unreduced value
                         $value = $this->get($value[1], true, null, true);
                     }
 
-                    $fontValue=&$value;
+                    $shorthandValue=&$value;
+
+                    $shorthandDividerNeedsUnit = false;
+                    $maxListElements           = null;
+                    $maxShorthandDividers      = 1;
 
-                    if ($value[0] === Type::T_LIST && $value[1]==',') {
+                    switch ($compiledName) {
+                        case 'border-radius':
+                            $maxListElements = 4;
+                            $shorthandDividerNeedsUnit = true;
+                            break;
+                    }
+
+                    if ($compiledName === 'font' and $value[0] === Type::T_LIST && $value[1]==',') {
                         // this is the case if more than one font is given: example: "font: 400 1em/1.3 arial,helvetica"
                         // we need to handle the first list element
-                        $fontValue=&$value[2][0];
+                        $shorthandValue=&$value[2][0];
                     }
 
-                    if ($fontValue[0] === Type::T_EXPRESSION && $fontValue[1] === '/') {
-                        $fontValue = $this->expToString($fontValue);
-                    } elseif ($fontValue[0] === Type::T_LIST) {
-                        foreach ($fontValue[2] as &$item) {
+                    if ($shorthandValue[0] === Type::T_EXPRESSION && $shorthandValue[1] === '/') {
+                        $revert = true;
+
+                        if ($shorthandDividerNeedsUnit) {
+                            $divider = $shorthandValue[3];
+
+                            if (is_array($divider)) {
+                                $divider = $this->reduce($divider, true);
+                            }
+
+                            if (intval($divider->dimension) and !count($divider->units)) {
+                                $revert = false;
+                            }
+                        }
+
+                        if ($revert) {
+                            $shorthandValue = $this->expToString($shorthandValue);
+                        }
+                    } elseif ($shorthandValue[0] === Type::T_LIST) {
+                        foreach ($shorthandValue[2] as &$item) {
                             if ($item[0] === Type::T_EXPRESSION && $item[1] === '/') {
-                                $item = $this->expToString($item);
+                                if ($maxShorthandDividers > 0) {
+                                    $revert = true;
+                                    // if the list of values is too long, this has to be a shorthand,
+                                    // otherwise it could be a real division
+                                    if (is_null($maxListElements) or count($shorthandValue[2]) <= $maxListElements) {
+                                        if ($shorthandDividerNeedsUnit) {
+                                            $divider = $item[3];
+
+                                            if (is_array($divider)) {
+                                                $divider = $this->reduce($divider, true);
+                                            }
+
+                                            if (intval($divider->dimension) and !count($divider->units)) {
+                                                $revert = false;
+                                            }
+                                        }
+                                    }
+
+                                    if ($revert) {
+                                        $item = $this->expToString($item);
+                                        $maxShorthandDividers--;
+                                    }
+                                }
                             }
                         }
                     }
@@ -2315,13 +2548,15 @@ class Compiler
                     break;
                 }
 
-                $this->appendOutputLine($out, Type::T_COMMENT, $child[1]);
+                $line = $this->compileCommentValue($child, true);
+                $this->appendOutputLine($out, Type::T_COMMENT, $line);
                 break;
 
             case Type::T_MIXIN:
             case Type::T_FUNCTION:
                 list(, $block) = $child;
-
+                // the block need to be able to go up to it's parent env to resolve vars
+                $block->parentEnv = $this->getStoreEnv();
                 $this->set(static::$namespaces[$block->type] . $block->name, $block, true);
                 break;
 
@@ -2332,8 +2567,13 @@ class Compiler
                     foreach ($results as $result) {
                         // only use the first one
                         $result = current($result);
+                        $selectors = $out->selectors;
 
-                        $this->pushExtends($result, $out->selectors, $child);
+                        if (! $selectors && isset($child['selfParent'])) {
+                            $selectors = $this->multiplySelectors($this->env, $child['selfParent']);
+                        }
+
+                        $this->pushExtends($result, $selectors, $child);
                     }
                 }
                 break;
@@ -2465,7 +2705,7 @@ class Compiler
 
             case Type::T_INCLUDE:
                 // including a mixin
-                list(, $name, $argValues, $content) = $child;
+                list(, $name, $argValues, $content, $argUsing) = $child;
 
                 $mixin = $this->get(static::$namespaces['mixin'] . $name, false);
 
@@ -2508,19 +2748,32 @@ class Compiler
                 // i.e., recursive @include of the same mixin
                 if (isset($content)) {
                     $copyContent = clone $content;
-                    $copyContent->scope = $callingScope;
+                    $copyContent->scope = clone $callingScope;
 
                     $this->setRaw(static::$namespaces['special'] . 'content', $copyContent, $this->env);
                 } else {
                     $this->setRaw(static::$namespaces['special'] . 'content', null, $this->env);
                 }
 
+                // save the "using" argument list for applying it to when "@content" is invoked
+                if (isset($argUsing)) {
+                    $this->setRaw(static::$namespaces['special'] . 'using', $argUsing, $this->env);
+                } else {
+                    $this->setRaw(static::$namespaces['special'] . 'using', null, $this->env);
+                }
+
                 if (isset($mixin->args)) {
                     $this->applyArguments($mixin->args, $argValues);
                 }
 
                 $this->env->marker = 'mixin';
 
+                if (! empty($mixin->parentEnv)) {
+                    $this->env->declarationScopeParent = $mixin->parentEnv;
+                } else {
+                    $this->throwError("@mixin $name() without parentEnv");
+                }
+
                 $this->compileChildrenNoReturn($mixin->children, $out, $selfParent, $this->env->marker . " " . $name);
 
                 $this->storeEnv = $storeEnv;
@@ -2529,18 +2782,35 @@ class Compiler
                 break;
 
             case Type::T_MIXIN_CONTENT:
-                $env = isset($this->storeEnv) ? $this->storeEnv : $this->env;
-                $content = $this->get(static::$namespaces['special'] . 'content', false, $env);
+                $env        = isset($this->storeEnv) ? $this->storeEnv : $this->env;
+                $content    = $this->get(static::$namespaces['special'] . 'content', false, $env);
+                $argUsing   = $this->get(static::$namespaces['special'] . 'using', false, $env);
+                $argContent = $child[1];
 
                 if (! $content) {
                     $content = new \stdClass();
-                    $content->scope = new \stdClass();
+                    $content->scope    = new \stdClass();
                     $content->children = $env->parent->block->children;
                     break;
                 }
 
                 $storeEnv = $this->storeEnv;
+                $varsUsing = [];
+
+                if (isset($argUsing) && isset($argContent)) {
+                    // Get the arguments provided for the content with the names provided in the "using" argument list
+                    $this->storeEnv = $this->env;
+                    $varsUsing = $this->applyArguments($argUsing, $argContent, false);
+                }
+
+                // restore the scope from the @content
                 $this->storeEnv = $content->scope;
+
+                // append the vars from using if any
+                foreach ($varsUsing as $name => $val) {
+                    $this->set($name, $val, true, $this->storeEnv);
+                }
+
                 $this->compileChildrenNoReturn($content->children, $out);
 
                 $this->storeEnv = $storeEnv;
@@ -2550,8 +2820,9 @@ class Compiler
                 list(, $value) = $child;
 
                 $fname = $this->sourceNames[$this->sourceIndex];
-                $line = $this->sourceLine;
+                $line  = $this->sourceLine;
                 $value = $this->compileValue($this->reduce($value, true));
+
                 fwrite($this->stderr, "File $fname on line $line DEBUG: $value\n");
                 break;
 
@@ -2559,8 +2830,9 @@ class Compiler
                 list(, $value) = $child;
 
                 $fname = $this->sourceNames[$this->sourceIndex];
-                $line = $this->sourceLine;
+                $line  = $this->sourceLine;
                 $value = $this->compileValue($this->reduce($value, true));
+
                 fwrite($this->stderr, "File $fname on line $line WARN: $value\n");
                 break;
 
@@ -2568,8 +2840,9 @@ class Compiler
                 list(, $value) = $child;
 
                 $fname = $this->sourceNames[$this->sourceIndex];
-                $line = $this->sourceLine;
+                $line  = $this->sourceLine;
                 $value = $this->compileValue($this->reduce($value, true));
+
                 $this->throwError("File $fname on line $line ERROR: $value\n");
                 break;
 
@@ -2664,11 +2937,10 @@ class Compiler
      * @param array   $value
      * @param boolean $inExp
      *
-     * @return null|array|\ScssPhp\ScssPhp\Node\Number
+     * @return null|string|array|\ScssPhp\ScssPhp\Node\Number
      */
     protected function reduce($value, $inExp = false)
     {
-
         if (is_null($value)) {
             return null;
         }
@@ -2694,9 +2966,8 @@ class Compiler
                     return $this->expToString($value);
                 }
 
-                $left = $this->coerceForExpression($left);
+                $left  = $this->coerceForExpression($left);
                 $right = $this->coerceForExpression($right);
-
                 $ltype = $left[0];
                 $rtype = $right[0];
 
@@ -2837,6 +3108,7 @@ class Compiler
 
             case Type::T_INTERPOLATE:
                 $value[1] = $this->reduce($value[1]);
+
                 if ($inExp) {
                     return $value[1];
                 }
@@ -2849,6 +3121,7 @@ class Compiler
             case Type::T_SELF:
                 $selfSelector = $this->multiplySelectors($this->env);
                 $selfSelector = $this->collapseSelectors($selfSelector, true);
+
                 return $selfSelector;
 
             default:
@@ -2923,6 +3196,10 @@ class Compiler
                     $value[2][$key] = $this->normalizeValue($item);
                 }
 
+                if (! empty($value['enclosing'])) {
+                    unset($value['enclosing']);
+                }
+
                 return $value;
 
             case Type::T_STRING:
@@ -3339,7 +3616,7 @@ class Compiler
      *
      * @param array $value
      *
-     * @return string
+     * @return string|array
      */
     public function compileValue($value)
     {
@@ -3356,14 +3633,38 @@ class Compiler
                 // [4] - optional alpha component
                 list(, $r, $g, $b) = $value;
 
-                $r = round($r);
-                $g = round($g);
-                $b = round($b);
+                $r = $this->compileRGBAValue($r);
+                $g = $this->compileRGBAValue($g);
+                $b = $this->compileRGBAValue($b);
+
+                if (count($value) === 5) {
+                    $alpha = $this->compileRGBAValue($value[4], true);
 
-                if (count($value) === 5 && $value[4] !== 1) { // rgba
-                    $a = new Node\Number($value[4], '');
+                    if (! is_numeric($alpha) || $alpha < 1) {
+                        $colorName = Colors::RGBaToColorName($r, $g, $b, $alpha);
+
+                        if (! is_null($colorName)) {
+                            return $colorName;
+                        }
+
+                        if (is_numeric($alpha)) {
+                            $a = new Node\Number($alpha, '');
+                        } else {
+                            $a = $alpha;
+                        }
 
-                    return 'rgba(' . $r . ', ' . $g . ', ' . $b . ', ' . $a . ')';
+                        return 'rgba(' . $r . ', ' . $g . ', ' . $b . ', ' . $a . ')';
+                    }
+                }
+
+                if (! is_numeric($r) || ! is_numeric($g) || ! is_numeric($b)) {
+                    return 'rgb(' . $r . ', ' . $g . ', ' . $b . ')';
+                }
+
+                $colorName = Colors::RGBaToColorName($r, $g, $b);
+
+                if (! is_null($colorName)) {
+                    return $colorName;
                 }
 
                 $h = sprintf('#%02x%02x%02x', $r, $g, $b);
@@ -3394,9 +3695,28 @@ class Compiler
                 }
 
                 list(, $delim, $items) = $value;
+                $pre = $post = "";
+                if (! empty($value['enclosing'])) {
+                    switch ($value['enclosing']) {
+                        case 'parent':
+                            //$pre = "(";
+                            //$post = ")";
+                            break;
+                        case 'forced_parent':
+                            $pre = "(";
+                            $post = ")";
+                            break;
+                        case 'bracket':
+                        case 'forced_bracket':
+                            $pre = "[";
+                            $post = "]";
+                            break;
+                    }
+                }
 
+                $prefix_value = '';
                 if ($delim !== ' ') {
-                    $delim .= ' ';
+                    $prefix_value = ' ';
                 }
 
                 $filtered = [];
@@ -3406,14 +3726,18 @@ class Compiler
                         continue;
                     }
 
-                    $filtered[] = $this->compileValue($item);
+                    $compiled = $this->compileValue($item);
+                    if ($prefix_value && strlen($compiled)) {
+                        $compiled = $prefix_value . $compiled;
+                    }
+                    $filtered[] = $compiled;
                 }
 
-                return implode("$delim", $filtered);
+                return $pre . substr(implode("$delim", $filtered), strlen($prefix_value)) . $post;
 
             case Type::T_MAP:
-                $keys = $value[1];
-                $values = $value[2];
+                $keys     = $value[1];
+                $values   = $value[2];
                 $filtered = [];
 
                 for ($i = 0, $s = count($keys); $i < $s; $i++) {
@@ -3431,11 +3755,23 @@ class Compiler
                 list(, $interpolate, $left, $right) = $value;
                 list(,, $whiteLeft, $whiteRight) = $interpolate;
 
+                $delim = $left[1];
+
+                if ($delim && $delim !== ' ' && ! $whiteLeft) {
+                    $delim .= ' ';
+                }
+
                 $left = count($left[2]) > 0 ?
-                    $this->compileValue($left) . $whiteLeft : '';
+                    $this->compileValue($left) . $delim . $whiteLeft: '';
+
+                $delim = $right[1];
+
+                if ($delim && $delim !== ' ') {
+                    $delim .= ' ';
+                }
 
                 $right = count($right[2]) > 0 ?
-                    $whiteRight . $this->compileValue($right) : '';
+                    $whiteRight . $delim . $this->compileValue($right) : '';
 
                 return $left . $this->compileValue($interpolate) . $right;
 
@@ -3465,6 +3801,7 @@ class Compiler
                             }
 
                             $temp = $this->compileValue([Type::T_KEYWORD, $item]);
+
                             if ($temp[0] === Type::T_STRING) {
                                 $filtered[] = $this->compileStringContent($temp);
                             } elseif ($temp[0] === Type::T_KEYWORD) {
@@ -3490,6 +3827,9 @@ class Compiler
             case Type::T_NULL:
                 return 'null';
 
+            case Type::T_COMMENT:
+                return $this->compileCommentValue($value);
+
             default:
                 $this->throwError("unknown value type: ".json_encode($value));
         }
@@ -3580,9 +3920,9 @@ class Compiler
             $selectors = $env->selectors;
 
             do {
-                $stillHasSelf = false;
+                $stillHasSelf  = false;
                 $prevSelectors = $selectors;
-                $selectors = [];
+                $selectors     = [];
 
                 foreach ($prevSelectors as $selector) {
                     foreach ($parentSelectors as $parent) {
@@ -3648,9 +3988,11 @@ class Compiler
                         foreach ($parentPart as $pp) {
                             if (is_array($pp)) {
                                 $flatten = [];
+
                                 array_walk_recursive($pp, function ($a) use (&$flatten) {
                                     $flatten[] = $a;
                                 });
+
                                 $pp = implode($flatten);
                             }
 
@@ -3694,12 +4036,14 @@ class Compiler
             : [[[Type::T_MEDIA_VALUE, $env->block->value]]];
 
         $store = [$this->env, $this->storeEnv];
-        $this->env = $env;
+
+        $this->env      = $env;
         $this->storeEnv = null;
-        $parentQueries = $this->evaluateMediaQuery($parentQueries);
+        $parentQueries  = $this->evaluateMediaQuery($parentQueries);
+
         list($this->env, $this->storeEnv) = $store;
 
-        if ($childQueries === null) {
+        if (is_null($childQueries)) {
             $childQueries = $parentQueries;
         } else {
             $originalQueries = $childQueries;
@@ -3891,7 +4235,6 @@ class Compiler
             $env = $this->getStoreEnv();
         }
 
-        $nextIsRoot = false;
         $hasNamespace = $normalizedName[0] === '^' || $normalizedName[0] === '@' || $normalizedName[0] === '%';
 
         $maxDepth = 10000;
@@ -3910,12 +4253,16 @@ class Compiler
             }
 
             if (! $hasNamespace && isset($env->marker)) {
-                if (! $nextIsRoot && ! empty($env->store[$specialContentKey])) {
+                if (! empty($env->store[$specialContentKey])) {
                     $env = $env->store[$specialContentKey]->scope;
                     continue;
                 }
 
-                $env = $this->rootEnv;
+                if (! empty($env->declarationScopeParent)) {
+                    $env = $env->declarationScopeParent;
+                } else {
+                    $env = $this->rootEnv;
+                }
                 continue;
             }
 
@@ -3927,7 +4274,7 @@ class Compiler
         }
 
         if ($shouldThrow) {
-            $this->throwError("Undefined variable \$$name" . ($maxDepth<=0 ? " (infinite recursion)" : ""));
+            $this->throwError("Undefined variable \$$name" . ($maxDepth <= 0 ? " (infinite recursion)" : ""));
         }
 
         // found nothing
@@ -3944,7 +4291,7 @@ class Compiler
      */
     protected function has($name, Environment $env = null)
     {
-        return $this->get($name, false, $env) !== null;
+        return ! is_null($this->get($name, false, $env));
     }
 
     /**
@@ -4018,7 +4365,7 @@ class Compiler
      */
     public function addParsedFile($path)
     {
-        if (isset($path) && file_exists($path)) {
+        if (isset($path) && is_file($path)) {
             $this->parsedFiles[realpath($path)] = filemtime($path);
         }
     }
@@ -4183,6 +4530,7 @@ class Compiler
         }
 
         $pi = pathinfo($path);
+
         array_unshift($this->importPaths, $pi['dirname']);
         $this->compileChildrenNoReturn($tree->children, $out);
         array_shift($this->importPaths);
@@ -4202,9 +4550,9 @@ class Compiler
         $urls = [];
 
         // for "normal" scss imports (ignore vanilla css and external requests)
-        if (! preg_match('/\.css$|^https?:\/\//', $url)) {
+        if (! preg_match('~\.css$|^https?://~', $url)) {
             // try both normal and the _partial filename
-            $urls = [$url, preg_replace('/[^\/]+$/', '_\0', $url)];
+            $urls = [$url, preg_replace('~[^/]+$~', '_\0', $url)];
         }
 
         $hasExtension = preg_match('/[.]s?css$/', $url);
@@ -4220,8 +4568,8 @@ class Compiler
                     ) ? '/' : '';
                     $full = $dir . $separator . $full;
 
-                    if ($this->fileExists($file = $full . '.scss') ||
-                        ($hasExtension && $this->fileExists($file = $full))
+                    if (is_file($file = $full . '.scss') ||
+                        ($hasExtension && is_file($file = $full))
                     ) {
                         return $file;
                     }
@@ -4230,7 +4578,7 @@ class Compiler
                 // check custom callback for import path
                 $file = call_user_func($dir, $url);
 
-                if ($file !== null) {
+                if (! is_null($file)) {
                     return $file;
                 }
             }
@@ -4325,9 +4673,10 @@ class Compiler
                           ? $this->sourceNames[$call[Parser::SOURCE_INDEX]]
                           : '(unknown file)');
                     $msg .= " on line " . $call[Parser::SOURCE_LINE];
+
                     $callStackMsg[] = $msg;
 
-                    if (! is_null($limit) && $ncall>$limit) {
+                    if (! is_null($limit) && $ncall > $limit) {
                         break;
                     }
                 }
@@ -4347,6 +4696,10 @@ class Compiler
     protected function handleImportLoop($name)
     {
         for ($env = $this->env; $env; $env = $env->parent) {
+            if (! $env->block) {
+                continue;
+            }
+
             $file = $this->sourceNames[$env->block->sourceIndex];
 
             if (realpath($file) === $name) {
@@ -4356,18 +4709,6 @@ class Compiler
         }
     }
 
-    /**
-     * Does file exist?
-     *
-     * @param string $name
-     *
-     * @return boolean
-     */
-    protected function fileExists($name)
-    {
-        return file_exists($name) && is_file($name);
-    }
-
     /**
      * Call SCSS @function
      *
@@ -4401,6 +4742,11 @@ class Compiler
         $tmp->children = [];
 
         $this->env->marker = 'function';
+        if (! empty($func->parentEnv)) {
+            $this->env->declarationScopeParent = $func->parentEnv;
+        } else {
+            $this->throwError("@function $name() without parentEnv");
+        }
 
         $ret = $this->compileChildren($func->children, $tmp, $this->env->marker . " " . $name);
 
@@ -4437,11 +4783,17 @@ class Compiler
             return false;
         }
 
-        @list($sorted, $kwargs) = $this->sortArgs($prototype, $args);
+        @list($sorted, $kwargs) = $this->sortNativeFunctionArgs($libName, $prototype, $args);
 
         if ($name !== 'if' && $name !== 'call') {
+            $inExp = true;
+
+            if ($name === 'join') {
+                $inExp = false;
+            }
+
             foreach ($sorted as &$val) {
-                $val = $this->reduce($val, true);
+                $val = $this->reduce($val, $inExp);
             }
         }
 
@@ -4479,12 +4831,13 @@ class Compiler
     /**
      * Sorts keyword arguments
      *
-     * @param array $prototype
-     * @param array $args
+     * @param string $functionName
+     * @param array  $prototypes
+     * @param array  $args
      *
      * @return array
      */
-    protected function sortArgs($prototypes, $args)
+    protected function sortNativeFunctionArgs($functionName, $prototypes, $args)
     {
         static $parser = null;
 
@@ -4508,6 +4861,23 @@ class Compiler
             return [$posArgs, $keyArgs];
         }
 
+        // specific cases ?
+        if (in_array($functionName, ['libRgb', 'libRgba', 'libHsl', 'libHsla'])) {
+            // notation 100 127 255 / 0 is in fact a simple list of 4 values
+            foreach ($args as $k => $arg) {
+                if ($arg[1][0] === Type::T_LIST && count($arg[1][2]) === 3) {
+                    $last = end($arg[1][2]);
+
+                    if ($last[0] === Type::T_EXPRESSION && $last[1] === '/') {
+                        array_pop($arg[1][2]);
+                        $arg[1][2][] = $last[2];
+                        $arg[1][2][] = $last[3];
+                        $args[$k] = $arg;
+                    }
+                }
+            }
+        }
+
         $finalArgs = [];
 
         if (! is_array(reset($prototypes))) {
@@ -4554,7 +4924,7 @@ class Compiler
             }
 
             try {
-                $vars = $this->applyArguments($argDef, $args, false);
+                $vars = $this->applyArguments($argDef, $args, false, false);
 
                 // ensure all args are populated
                 foreach ($prototype as $i => $p) {
@@ -4602,14 +4972,22 @@ class Compiler
     /**
      * Apply argument values per definition
      *
-     * @param array $argDef
-     * @param array $argValues
+     * @param array   $argDef
+     * @param array   $argValues
+     * @param boolean $storeInEnv
+     * @param boolean $reduce
+     *   only used if $storeInEnv = false
+     *
+     * @return array
      *
      * @throws \Exception
      */
-    protected function applyArguments($argDef, $argValues, $storeInEnv = true)
+    protected function applyArguments($argDef, $argValues, $storeInEnv = true, $reduce = true)
     {
         $output = [];
+        if (is_array($argValues) && count($argValues) && end($argValues) === static::$null) {
+            array_pop($argValues);
+        }
 
         if ($storeInEnv) {
             $storeEnv = $this->getStoreEnv();
@@ -4639,18 +5017,27 @@ class Compiler
             if (! empty($arg[0])) {
                 $hasKeywordArgument = true;
 
-                if (! isset($args[$arg[0][1]]) || $args[$arg[0][1]][3]) {
+                $name = $arg[0][1];
+                if (! isset($args[$name])) {
+                    foreach (array_keys($args) as $an) {
+                        if (str_replace("_", "-", $an) === str_replace("_", "-", $name)) {
+                            $name = $an;
+                            break;
+                        }
+                    }
+                }
+                if (! isset($args[$name]) || $args[$name][3]) {
                     if ($hasVariable) {
-                        $deferredKeywordArgs[$arg[0][1]] = $arg[1];
+                        $deferredKeywordArgs[$name] = $arg[1];
                     } else {
                         $this->throwError("Mixin or function doesn't have an argument named $%s.", $arg[0][1]);
                         break;
                     }
-                } elseif ($args[$arg[0][1]][0] < count($remaining)) {
+                } elseif ($args[$name][0] < count($remaining)) {
                     $this->throwError("The argument $%s was passed both by position and by name.", $arg[0][1]);
                     break;
                 } else {
-                    $keywordArgs[$arg[0][1]] = $arg[1];
+                    $keywordArgs[$name] = $arg[1];
                 }
             } elseif ($arg[2] === true) {
                 $val = $this->reduce($arg[1], true);
@@ -4658,7 +5045,7 @@ class Compiler
                 if ($val[0] === Type::T_LIST) {
                     foreach ($val[2] as $name => $item) {
                         if (! is_numeric($name)) {
-                            if (!isset($args[$name])) {
+                            if (! isset($args[$name])) {
                                 foreach (array_keys($args) as $an) {
                                     if (str_replace("_", "-", $an) === str_replace("_", "-", $name)) {
                                         $name = $an;
@@ -4676,6 +5063,7 @@ class Compiler
                             if (is_null($splatSeparator)) {
                                 $splatSeparator = $val[1];
                             }
+
                             $remaining[] = $item;
                         }
                     }
@@ -4685,7 +5073,7 @@ class Compiler
                         $item = $val[2][$i];
 
                         if (! is_numeric($name)) {
-                            if (!isset($args[$name])) {
+                            if (! isset($args[$name])) {
                                 foreach (array_keys($args) as $an) {
                                     if (str_replace("_", "-", $an) === str_replace("_", "-", $name)) {
                                         $name = $an;
@@ -4693,6 +5081,7 @@ class Compiler
                                     }
                                 }
                             }
+
                             if ($hasVariable) {
                                 $deferredKeywordArgs[$name] = $item;
                             } else {
@@ -4702,6 +5091,7 @@ class Compiler
                             if (is_null($splatSeparator)) {
                                 $splatSeparator = $val[1];
                             }
+
                             $remaining[] = $item;
                         }
                     }
@@ -4743,7 +5133,7 @@ class Compiler
             if ($storeInEnv) {
                 $this->set($name, $this->reduce($val, true), true, $env);
             } else {
-                $output[$name] = $val;
+                $output[$name] = ($reduce ? $this->reduce($val, true) : $val);
             }
         }
 
@@ -4761,7 +5151,7 @@ class Compiler
             if ($storeInEnv) {
                 $this->set($name, $this->reduce($default, true), true);
             } else {
-                $output[$name] = $default;
+                $output[$name] = ($reduce ? $this->reduce($default, true) : $default);
             }
         }
 
@@ -4785,7 +5175,7 @@ class Compiler
             return $this->toBool($value);
         }
 
-        if ($value === null) {
+        if (is_null($value)) {
             return static::$null;
         }
 
@@ -4797,30 +5187,14 @@ class Compiler
             return static::$emptyString;
         }
 
-        if (preg_match('/^(#([0-9a-f]{6})|#([0-9a-f]{3}))$/i', $value, $m)) {
-            $color = [Type::T_COLOR];
-
-            if (isset($m[3])) {
-                $num = hexdec($m[3]);
-
-                foreach ([3, 2, 1] as $i) {
-                    $t = $num & 0xf;
-                    $color[$i] = $t << 4 | $t;
-                    $num >>= 4;
-                }
-            } else {
-                $num = hexdec($m[2]);
-
-                foreach ([3, 2, 1] as $i) {
-                    $color[$i] = $num & 0xff;
-                    $num >>= 8;
-                }
-            }
+        $value = [Type::T_KEYWORD, $value];
+        $color = $this->coerceColor($value);
 
+        if ($color) {
             return $color;
         }
 
-        return [Type::T_KEYWORD, $value];
+        return $value;
     }
 
     /**
@@ -4836,7 +5210,9 @@ class Compiler
             return $item;
         }
 
-        if ($item === static::$emptyList) {
+        if ($item[0] === static::$emptyList[0]
+            && $item[1] === static::$emptyList[1]
+            && $item[2] === static::$emptyList[2]) {
             return static::$emptyMap;
         }
 
@@ -4869,6 +5245,7 @@ class Compiler
                 switch ($key[0]) {
                     case Type::T_LIST:
                     case Type::T_MAP:
+                    case Type::T_STRING:
                         break;
 
                     default:
@@ -4912,21 +5289,106 @@ class Compiler
      *
      * @return array|null
      */
-    protected function coerceColor($value)
+    protected function coerceColor($value, $inRGBFunction = false)
     {
         switch ($value[0]) {
             case Type::T_COLOR:
+                for ($i = 1; $i <= 3; $i++) {
+                    if (! is_numeric($value[$i])) {
+                        $cv = $this->compileRGBAValue($value[$i]);
+
+                        if (! is_numeric($cv)) {
+                            return null;
+                        }
+
+                        $value[$i] = $cv;
+                    }
+
+                    if (isset($value[4])) {
+                        if (! is_numeric($value[4])) {
+                            $cv = $this->compileRGBAValue($value[4], true);
+
+                            if (! is_numeric($cv)) {
+                                return null;
+                            }
+
+                            $value[4] = $cv;
+                        }
+                    }
+                }
+
                 return $value;
 
+            case Type::T_LIST:
+                if ($inRGBFunction) {
+                    if (count($value[2]) == 3 || count($value[2]) == 4) {
+                        $color = $value[2];
+                        array_unshift($color, Type::T_COLOR);
+
+                        return $this->coerceColor($color);
+                    }
+                }
+
+                return null;
+
             case Type::T_KEYWORD:
+                if (! is_string($value[1])) {
+                    return null;
+                }
+
                 $name = strtolower($value[1]);
+                // hexa color?
+                if (preg_match('/^#([0-9a-f]+)$/i', $name, $m)) {
+                    $nofValues = strlen($m[1]);
+
+                    if (in_array($nofValues, [3, 4, 6, 8])) {
+                        $nbChannels = 3;
+                        $color      = [];
+                        $num        = hexdec($m[1]);
+
+                        switch ($nofValues) {
+                            case 4:
+                                $nbChannels = 4;
+                                // then continuing with the case 3:
+                            case 3:
+                                for ($i = 0; $i < $nbChannels; $i++) {
+                                    $t = $num & 0xf;
+                                    array_unshift($color, $t << 4 | $t);
+                                    $num >>= 4;
+                                }
+
+                                break;
 
-                if (isset(Colors::$cssColors[$name])) {
-                    $rgba = explode(',', Colors::$cssColors[$name]);
+                            case 8:
+                                $nbChannels = 4;
+                                // then continuing with the case 6:
+                            case 6:
+                                for ($i = 0; $i < $nbChannels; $i++) {
+                                    array_unshift($color, $num & 0xff);
+                                    $num >>= 8;
+                                }
+
+                                break;
+                        }
 
+                        if ($nbChannels === 4) {
+                            if ($color[3] === 255) {
+                                $color[3] = 1; // fully opaque
+                            } else {
+                                $color[3] = round($color[3] / 255, 3);
+                            }
+                        }
+
+                        array_unshift($color, Type::T_COLOR);
+
+                        return $color;
+                    }
+                }
+
+                if ($rgba = Colors::colorNameToRGBa($name)) {
                     return isset($rgba[3])
-                        ? [Type::T_COLOR, (int) $rgba[0], (int) $rgba[1], (int) $rgba[2], (int) $rgba[3]]
-                        : [Type::T_COLOR, (int) $rgba[0], (int) $rgba[1], (int) $rgba[2]];
+                        ? [Type::T_COLOR, $rgba[0], $rgba[1], $rgba[2], $rgba[3]]
+                        : [Type::T_COLOR, $rgba[0], $rgba[1], $rgba[2]];
                 }
 
                 return null;
@@ -4935,6 +5397,88 @@ class Compiler
         return null;
     }
 
+    /**
+     * @param integer|\ScssPhp\ScssPhp\Node\Number $value
+     * @param boolean                              $isAlpha
+     *
+     * @return integer|mixed
+     */
+    protected function compileRGBAValue($value, $isAlpha = false)
+    {
+        if ($isAlpha) {
+            return $this->compileColorPartValue($value, 0, 1, false);
+        }
+
+        return $this->compileColorPartValue($value, 0, 255, true);
+    }
+
+    /**
+     * @param mixed         $value
+     * @param integer|float $min
+     * @param integer|float $max
+     * @param boolean       $isInt
+     * @param boolean       $clamp
+     * @param boolean       $modulo
+     *
+     * @return integer|mixed
+     */
+    protected function compileColorPartValue($value, $min, $max, $isInt = true, $clamp = true, $modulo = false)
+    {
+        if (! is_numeric($value)) {
+            if (is_array($value)) {
+                $reduced = $this->reduce($value);
+
+                if (is_object($reduced) && $value->type === Type::T_NUMBER) {
+                    $value = $reduced;
+                }
+            }
+
+            if (is_object($value) && $value->type === Type::T_NUMBER) {
+                $num = $value->dimension;
+
+                if (count($value->units)) {
+                    $unit = array_keys($value->units);
+                    $unit = reset($unit);
+
+                    switch ($unit) {
+                        case '%':
+                            $num *= $max / 100;
+                            break;
+                        default:
+                            break;
+                    }
+                }
+
+                $value = $num;
+            } elseif (is_array($value)) {
+                $value = $this->compileValue($value);
+            }
+        }
+
+        if (is_numeric($value)) {
+            if ($isInt) {
+                $value = round($value);
+            }
+
+            if ($clamp) {
+                $value = min($max, max($min, $value));
+            }
+
+            if ($modulo) {
+                $value = $value % $max;
+
+                // still negative?
+                while ($value < $min) {
+                    $value += $max;
+                }
+            }
+
+            return $value;
+        }
+
+        return $value;
+    }
+
     /**
      * Coerce value to string
      *
@@ -5246,30 +5790,68 @@ class Compiler
         return false === $key ? static::$null : $key + 1;
     }
 
-    protected static $libRgb = ['red', 'green', 'blue'];
-    protected function libRgb($args)
+    protected static $libRgb = [
+        ['color'],
+        ['color', 'alpha'],
+        ['channels'],
+        ['red', 'green', 'blue'],
+        ['red', 'green', 'blue', 'alpha'] ];
+    protected function libRgb($args, $kwargs, $funcName = 'rgb')
     {
-        list($r, $g, $b) = $args;
+        switch (count($args)) {
+            case 1:
+                if (! $color = $this->coerceColor($args[0], true)) {
+                    $color = [Type::T_STRING, '', [$funcName . '(', $args[0], ')']];
+                }
+                break;
 
-        return [Type::T_COLOR, $r[1], $g[1], $b[1]];
-    }
+            case 3:
+                $color = [Type::T_COLOR, $args[0], $args[1], $args[2]];
 
-    protected static $libRgba = [
-        ['color', 'alpha:1'],
-        ['red', 'green', 'blue', 'alpha:1'] ];
-    protected function libRgba($args)
-    {
-        if ($color = $this->coerceColor($args[0])) {
-            $num = isset($args[3]) ? $args[3] : $args[1];
-            $alpha = $this->assertNumber($num);
-            $color[4] = $alpha;
+                if (! $color = $this->coerceColor($color)) {
+                    $color = [Type::T_STRING, '', [$funcName .'(', $args[0], ', ', $args[1], ', ', $args[2], ')']];
+                }
 
-            return $color;
+                return $color;
+
+            case 2:
+                if ($color = $this->coerceColor($args[0], true)) {
+                    $alpha = $this->compileRGBAValue($args[1], true);
+
+                    if (is_numeric($alpha)) {
+                        $color[4] = $alpha;
+                    } else {
+                        $color = [Type::T_STRING, '',
+                            [$funcName . '(', $color[1], ', ', $color[2], ', ', $color[3], ', ', $alpha, ')']];
+                    }
+                } else {
+                    $color = [Type::T_STRING, '', [$funcName . '(', $args[0], ')']];
+                }
+                break;
+
+            case 4:
+            default:
+                $color = [Type::T_COLOR, $args[0], $args[1], $args[2], $args[3]];
+
+                if (! $color = $this->coerceColor($color)) {
+                    $color = [Type::T_STRING, '',
+                        [$funcName . '(', $args[0], ', ', $args[1], ', ', $args[2], ', ', $args[3], ')']];
+                }
+                break;
         }
 
-        list($r, $g, $b, $a) = $args;
+        return $color;
+    }
 
-        return [Type::T_COLOR, $r[1], $g[1], $b[1], $a[1]];
+    protected static $libRgba = [
+        ['color'],
+        ['color', 'alpha'],
+        ['channels'],
+        ['red', 'green', 'blue'],
+        ['red', 'green', 'blue', 'alpha'] ];
+    protected function libRgba($args, $kwargs)
+    {
+        return $this->libRgb($args, $kwargs, 'rgba');
     }
 
     // helper function for adjust_color, change_color, and scale_color
@@ -5277,21 +5859,25 @@ class Compiler
     {
         $color = $this->assertColor($args[0]);
 
-        foreach ([1, 2, 3, 7] as $i) {
-            if (isset($args[$i])) {
-                $val = $this->assertNumber($args[$i]);
-                $ii = $i === 7 ? 4 : $i; // alpha
-                $color[$ii] = call_user_func($fn, isset($color[$ii]) ? $color[$ii] : 0, $val, $i);
+        foreach ([1 => 1, 2 => 2, 3 => 3, 7 => 4] as $iarg => $irgba) {
+            if (isset($args[$iarg])) {
+                $val = $this->assertNumber($args[$iarg]);
+
+                if (! isset($color[$irgba])) {
+                    $color[$irgba] = (($irgba < 4) ? 0 : 1);
+                }
+
+                $color[$irgba] = call_user_func($fn, $color[$irgba], $val, $iarg);
             }
         }
 
         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 (! empty($args[$i])) {
-                    $val = $this->assertNumber($args[$i]);
-                    $hsl[$i - 3] = call_user_func($fn, $hsl[$i - 3], $val, $i);
+            foreach ([4 => 1, 5 => 2, 6 => 3] as $iarg => $ihsl) {
+                if (! empty($args[$iarg])) {
+                    $val = $this->assertNumber($args[$iarg]);
+                    $hsl[$ihsl] = call_user_func($fn, $hsl[$ihsl], $val, $iarg);
                 }
             }
 
@@ -5374,7 +5960,7 @@ class Compiler
         $color = $this->coerceColor($args[0]);
         $color[4] = isset($color[4]) ? round(255 * $color[4]) : 255;
 
-        return sprintf('#%02X%02X%02X%02X', $color[4], $color[1], $color[2], $color[3]);
+        return [Type::T_STRING, '', [sprintf('#%02X%02X%02X%02X', $color[4], $color[1], $color[2], $color[3])]];
     }
 
     protected static $libRed = ['color'];
@@ -5461,25 +6047,56 @@ class Compiler
         return $this->fixColor($new);
     }
 
-    protected static $libHsl = ['hue', 'saturation', 'lightness'];
-    protected function libHsl($args)
+    protected static $libHsl =[
+        ['channels'],
+        ['hue', 'saturation', 'lightness'],
+        ['hue', 'saturation', 'lightness', 'alpha'] ];
+    protected function libHsl($args, $kwargs, $funcName = 'hsl')
     {
-        list($h, $s, $l) = $args;
+        if (count($args) == 1) {
+            if ($args[0][0] !== Type::T_LIST || count($args[0][2]) < 3 || count($args[0][2]) > 4) {
+                return [Type::T_STRING, '', [$funcName . '(', $args[0], ')']];
+            }
 
-        return $this->toRGB($h[1], $s[1], $l[1]);
-    }
+            $args = $args[0][2];
+        }
 
-    protected static $libHsla = ['hue', 'saturation', 'lightness', 'alpha'];
-    protected function libHsla($args)
-    {
-        list($h, $s, $l, $a) = $args;
+        $hue = $this->compileColorPartValue($args[0], 0, 360, false, false, true);
+        $saturation = $this->compileColorPartValue($args[1], 0, 100, false);
+        $lightness = $this->compileColorPartValue($args[2], 0, 100, false);
 
-        $color = $this->toRGB($h[1], $s[1], $l[1]);
-        $color[4] = $a[1];
+        $alpha = null;
+
+        if (count($args) === 4) {
+            $alpha = $this->compileColorPartValue($args[3], 0, 100, false);
+
+            if (! is_numeric($hue) || ! is_numeric($saturation) || ! is_numeric($lightness) || ! is_numeric($alpha)) {
+                return [Type::T_STRING, '',
+                    [$funcName . '(', $args[0], ', ', $args[1], ', ', $args[2], ', ', $args[3], ')']];
+            }
+        } else {
+            if (! is_numeric($hue) || ! is_numeric($saturation) || ! is_numeric($lightness)) {
+                return [Type::T_STRING, '', [$funcName . '(', $args[0], ', ', $args[1], ', ', $args[2], ')']];
+            }
+        }
+
+        $color = $this->toRGB($hue, $saturation, $lightness);
+
+        if (! is_null($alpha)) {
+            $color[4] = $alpha;
+        }
 
         return $color;
     }
 
+    protected static $libHsla = [
+            ['channels'],
+            ['hue', 'saturation', 'lightness', 'alpha:1'] ];
+    protected function libHsla($args, $kwargs)
+    {
+        return $this->libHsl($args, $kwargs, 'hsla');
+    }
+
     protected static $libHue = ['color'];
     protected function libHue($args)
     {
@@ -5589,21 +6206,32 @@ class Compiler
         return $this->adjustHsl($this->assertColor($args[0]), 1, 180);
     }
 
-    protected static $libInvert = ['color'];
+    protected static $libInvert = ['color', 'weight:1'];
     protected function libInvert($args)
     {
-        $value = $args[0];
+        list($value, $weight) = $args;
+
+        if (! isset($weight)) {
+            $weight = 1;
+        } else {
+            $weight = $this->coercePercent($weight);
+        }
 
         if ($value[0] === Type::T_NUMBER) {
             return null;
         }
 
         $color = $this->assertColor($value);
-        $color[1] = 255 - $color[1];
-        $color[2] = 255 - $color[2];
-        $color[3] = 255 - $color[3];
+        $inverted = $color;
+        $inverted[1] = 255 - $inverted[1];
+        $inverted[2] = 255 - $inverted[2];
+        $inverted[3] = 255 - $inverted[3];
 
-        return $color;
+        if ($weight < 1) {
+            return $this->libMix([$inverted, $color, [Type::T_NUMBER, $weight]]);
+        }
+
+        return $inverted;
     }
 
     // increases opacity by amount
@@ -5712,7 +6340,7 @@ class Compiler
         $min = null;
 
         foreach ($numbers as $key => $number) {
-            if (null === $min || $number[1] <= $min[1]) {
+            if (is_null($min) || $number[1] <= $min[1]) {
                 $min = [$key, $number[1]];
             }
         }
@@ -5726,7 +6354,7 @@ class Compiler
         $max = null;
 
         foreach ($numbers as $key => $number) {
-            if (null === $max || $number[1] >= $max[1]) {
+            if (is_null($max) || $number[1] >= $max[1]) {
                 $max = [$key, $number[1]];
             }
         }
@@ -5743,9 +6371,9 @@ class Compiler
      */
     protected function getNormalizedNumbers($args)
     {
-        $unit = null;
+        $unit         = null;
         $originalUnit = null;
-        $numbers = [];
+        $numbers      = [];
 
         foreach ($args as $key => $item) {
             if ($item[0] !== Type::T_NUMBER) {
@@ -5755,7 +6383,7 @@ class Compiler
 
             $number = $item->normalize();
 
-            if (null === $unit) {
+            if (is_null($unit)) {
                 $unit = $number[2];
                 $originalUnit = $item->unitStr();
             } elseif ($number[1] && $unit !== $number[2]) {
@@ -5843,6 +6471,7 @@ class Compiler
 
         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];
@@ -5941,6 +6570,18 @@ class Compiler
         return [Type::T_MAP, $keys, $values];
     }
 
+    protected static $libIsBracketed = ['list'];
+    protected function libIsBracketed($args)
+    {
+        $list = $args[0];
+        $this->coerceList($list, ' ');
+        if (! empty($list['enclosing']) && $list['enclosing'] === 'bracket') {
+            return true;
+        }
+        return false;
+    }
+
+
     protected function listSeparatorForJoin($list1, $sep)
     {
         if (! isset($sep)) {
@@ -5952,23 +6593,53 @@ class Compiler
                 return ',';
 
             case 'space':
-                return '';
+                return ' ';
 
             default:
                 return $list1[1];
         }
     }
 
-    protected static $libJoin = ['list1', 'list2', 'separator:null'];
+    protected static $libJoin = ['list1', 'list2', 'separator:null', 'bracketed:auto'];
     protected function libJoin($args)
     {
-        list($list1, $list2, $sep) = $args;
+        list($list1, $list2, $sep, $bracketed) = $args;
 
         $list1 = $this->coerceList($list1, ' ');
         $list2 = $this->coerceList($list2, ' ');
-        $sep = $this->listSeparatorForJoin($list1, $sep);
+        $sep   = $this->listSeparatorForJoin($list1, $sep);
+
+        if ($bracketed === static::$true) {
+            $bracketed = true;
+        } elseif ($bracketed === static::$false) {
+            $bracketed = false;
+        } elseif ($bracketed === [Type::T_KEYWORD, 'auto']) {
+            $bracketed = 'auto';
+        } elseif ($bracketed === static::$null) {
+            $bracketed = false;
+        } else {
+            $bracketed = $this->compileValue($bracketed);
+            $bracketed = ! ! $bracketed;
+            if ($bracketed === true) {
+                $bracketed = true;
+            }
+        }
+
+        if ($bracketed === 'auto') {
+            $bracketed = false;
+            if (! empty($list1['enclosing']) && $list1['enclosing'] === 'bracket') {
+                $bracketed = true;
+            }
+        }
 
-        return [Type::T_LIST, $sep, array_merge($list1[2], $list2[2])];
+        $res = [Type::T_LIST, $sep, array_merge($list1[2], $list2[2])];
+        if (isset($list1['enclosing'])) {
+            $res['enlcosing'] = $list1['enclosing'];
+        }
+        if ($bracketed) {
+            $res['enclosing'] = 'bracket';
+        }
+        return $res;
     }
 
     protected static $libAppend = ['list', 'val', 'separator:null'];
@@ -5979,13 +6650,17 @@ class Compiler
         $list1 = $this->coerceList($list1, ' ');
         $sep = $this->listSeparatorForJoin($list1, $sep);
 
-        return [Type::T_LIST, $sep, array_merge($list1[2], [$value])];
+        $res = [Type::T_LIST, $sep, array_merge($list1[2], [$value])];
+        if (isset($list1['enclosing'])) {
+            $res['enclosing'] = $list1['enclosing'];
+        }
+        return $res;
     }
 
     protected function libZip($args)
     {
-        foreach ($args as $arg) {
-            $this->assertList($arg);
+        foreach ($args as $key => $arg) {
+            $args[$key] = $this->coerceList($arg);
         }
 
         $lists = [];
@@ -6116,10 +6791,10 @@ class Compiler
         return new Node\Number(strlen($stringContent), '');
     }
 
-    protected static $libStrSlice = ['string', 'start-at', 'end-at:null'];
+    protected static $libStrSlice = ['string', 'start-at', 'end-at:-1'];
     protected function libStrSlice($args)
     {
-        if (isset($args[2]) && $args[2][1] == 0) {
+        if (isset($args[2]) && ! $args[2][1]) {
             return static::$nullString;
         }
 
@@ -6132,7 +6807,7 @@ class Compiler
             $start--;
         }
 
-        $end    = (int) $args[2][1];
+        $end    = isset($args[2]) ? (int) $args[2][1] : -1;
         $length = $end < 0 ? $end + 1 : ($end > 0 ? $end - $start : $end);
 
         $string[2] = $length
@@ -6270,14 +6945,40 @@ class Compiler
         return [Type::T_STRING, '', ['u' . str_pad(base_convert($id, 10, 36), 8, '0', STR_PAD_LEFT)]];
     }
 
-    protected static $libInspect = ['value'];
-    protected function libInspect($args)
+    protected function inspectFormatValue($value, $force_enclosing_display = false)
     {
-        if ($args[0] === static::$null) {
-            return [Type::T_KEYWORD, 'null'];
+        if ($value === static::$null) {
+            $value = [Type::T_KEYWORD, 'null'];
+        }
+        $stringValue = [$value];
+        if ($value[0] === Type::T_LIST) {
+            if (end($value[2]) === static::$null) {
+                array_pop($value[2]);
+                $value[2][] = [Type::T_STRING, '', ['']];
+                $force_enclosing_display = true;
+            }
+            if (! empty($value['enclosing'])) {
+                if ($force_enclosing_display
+                    || ($value['enclosing'] === 'bracket' )
+                    || !count($value[2])) {
+                    $value['enclosing'] = 'forced_'.$value['enclosing'];
+                    $force_enclosing_display = true;
+                }
+            }
+            foreach ($value[2] as $k => $listelement) {
+                $value[2][$k] = $this->inspectFormatValue($listelement, $force_enclosing_display);
+            }
+            $stringValue = [$value];
         }
 
-        return $args[0];
+        return [Type::T_STRING, '', $stringValue];
+    }
+
+    protected static $libInspect = ['value'];
+    protected function libInspect($args)
+    {
+        $value = $args[0];
+        return $this->inspectFormatValue($value);
     }
 
     /**
@@ -6431,6 +7132,7 @@ class Compiler
         // get the selector... list
         $args = reset($args);
         $args = $args[2];
+
         if (count($args) < 1) {
             $this->throwError("selector-append() needs at least 1 argument");
         }
@@ -6497,8 +7199,8 @@ class Compiler
         list($selectors, $extendee, $extender) = $args;
 
         $selectors = $this->getSelectorArg($selectors);
-        $extendee = $this->getSelectorArg($extendee);
-        $extender = $this->getSelectorArg($extender);
+        $extendee  = $this->getSelectorArg($extendee);
+        $extender  = $this->getSelectorArg($extender);
 
         if (! $selectors || ! $extendee || ! $extender) {
             $this->throwError("selector-extend() invalid arguments");
@@ -6514,8 +7216,8 @@ class Compiler
     {
         list($selectors, $original, $replacement) = $args;
 
-        $selectors = $this->getSelectorArg($selectors);
-        $original = $this->getSelectorArg($original);
+        $selectors   = $this->getSelectorArg($selectors);
+        $original    = $this->getSelectorArg($original);
         $replacement = $this->getSelectorArg($replacement);
 
         if (! $selectors || ! $original || ! $replacement) {
@@ -6580,13 +7282,14 @@ class Compiler
         // get the selector... list
         $args = reset($args);
         $args = $args[2];
+
         if (count($args) < 1) {
             $this->throwError("selector-nest() needs at least 1 argument");
         }
 
         $selectorsMap = array_map([$this, 'getSelectorArg'], $args);
-
         $envs = [];
+
         foreach ($selectorsMap as $selectors) {
             $env = new Environment();
             $env->selectors = $selectors;
@@ -6594,8 +7297,8 @@ class Compiler
             $envs[] = $env;
         }
 
-        $envs = array_reverse($envs);
-        $env = $this->extractEnv($envs);
+        $envs            = array_reverse($envs);
+        $env             = $this->extractEnv($envs);
         $outputSelectors = $this->multiplySelectors($env);
 
         return $this->formatOutputSelector($outputSelectors);
@@ -6638,6 +7341,7 @@ class Compiler
      *
      * @param array $compound1
      * @param array $compound2
+     *
      * @return array|mixed
      */
     protected function unifyCompoundSelectors($compound1, $compound2)
@@ -6653,7 +7357,7 @@ class Compiler
         // check that last part are compatible
         $lastPart1 = array_pop($compound1);
         $lastPart2 = array_pop($compound2);
-        $last = $this->mergeParts($lastPart1, $lastPart2);
+        $last      = $this->mergeParts($lastPart1, $lastPart2);
 
         if (! $last) {
             return [[]];
@@ -6676,6 +7380,7 @@ class Compiler
 
                 $c = $this->mergeParts($part1, $part2);
                 $unifiedSelectors = $this->prependSelectors($unifiedSelectors, [$c]);
+
                 $part1 = $part2 = null;
 
                 array_pop($compound1);
@@ -6690,6 +7395,7 @@ class Compiler
 
                 $c = $this->mergeParts($part2, $part1);
                 $unifiedSelectors = $this->prependSelectors($unifiedSelectors, [$c]);
+
                 $part1 = $part2 = null;
 
                 array_pop($compound2);
@@ -6701,9 +7407,9 @@ class Compiler
                 array_pop($compound1);
                 array_pop($compound2);
 
-                $s = $this->prependSelectors($unifiedSelectors, [$part2]);
+                $s   = $this->prependSelectors($unifiedSelectors, [$part2]);
                 $new = array_merge($new, $this->prependSelectors($s, [$part1]));
-                $s = $this->prependSelectors($unifiedSelectors, [$part1]);
+                $s   = $this->prependSelectors($unifiedSelectors, [$part1]);
                 $new = array_merge($new, $this->prependSelectors($s, [$part2]));
             } elseif ($part1) {
                 array_pop($compound1);
@@ -6757,8 +7463,8 @@ class Compiler
     protected function matchPartInCompound($part, $compound)
     {
         $partTag = $this->findTagName($part);
-        $before = $compound;
-        $after = [];
+        $before  = $compound;
+        $after   = [];
 
         // try to find a match by tag name first
         while (count($before)) {
@@ -6804,7 +7510,7 @@ class Compiler
     {
         $tag1 = $this->findTagName($parts1);
         $tag2 = $this->findTagName($parts2);
-        $tag = $this->checkCompatibleTags($tag1, $tag2);
+        $tag  = $this->checkCompatibleTags($tag1, $tag2);
 
         // not compatible tags
         if ($tag === false) {
@@ -6855,12 +7561,12 @@ class Compiler
         $tags = array_unique($tags);
         $tags = array_filter($tags);
 
-        if (count($tags)>1) {
+        if (count($tags) > 1) {
             $tags = array_diff($tags, ['*']);
         }
 
         // not compatible nodes
-        if (count($tags)>1) {
+        if (count($tags) > 1) {
             return false;
         }
 
index 478aa6a..e177704 100644 (file)
@@ -81,6 +81,11 @@ abstract class Formatter
      */
     protected $sourceMapGenerator;
 
+    /**
+     * @var string
+     */
+    protected $strippedSemicolon;
+
     /**
      * Initialize formatter
      *
@@ -113,24 +118,6 @@ abstract class Formatter
         return rtrim($name) . $this->assignSeparator . $value . ';';
     }
 
-    /**
-     * Strip semi-colon appended by property(); it's a separator, not a terminator
-     *
-     * @api
-     *
-     * @param array $lines
-     */
-    public function stripSemicolon(&$lines)
-    {
-        if ($this->keepSemicolons) {
-            return;
-        }
-
-        if (($count = count($lines)) && substr($lines[$count - 1], -1) === ';') {
-            $lines[$count - 1] = substr($lines[$count - 1], 0, -1);
-        }
-    }
-
     /**
      * Output lines inside a block
      *
@@ -207,6 +194,10 @@ abstract class Formatter
         if (! empty($block->selectors)) {
             $this->indentLevel--;
 
+            if (! $this->keepSemicolons) {
+                $this->strippedSemicolon = '';
+            }
+
             if (empty($block->children)) {
                 $this->write($this->break);
             }
@@ -217,8 +208,10 @@ abstract class Formatter
 
     /**
      * Test and clean safely empty children
+     *
      * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $block
-     * @return bool
+     *
+     * @return boolean
      */
     protected function testEmptyChildren($block)
     {
@@ -228,14 +221,16 @@ abstract class Formatter
             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;
-                    }
+                    continue;
+                }
+
+                if ($child->type === Type::T_MEDIA || $child->type === Type::T_DIRECTIVE) {
+                    $child->children = [];
+                    $child->selectors = null;
                 }
             }
         }
+
         return $isEmpty;
     }
 
@@ -254,8 +249,8 @@ abstract class Formatter
         $this->sourceMapGenerator = null;
 
         if ($sourceMapGenerator) {
-            $this->currentLine = 1;
-            $this->currentColumn = 0;
+            $this->currentLine        = 1;
+            $this->currentColumn      = 0;
             $this->sourceMapGenerator = $sourceMapGenerator;
         }
 
@@ -271,10 +266,32 @@ abstract class Formatter
     }
 
     /**
+     * Output content
+     *
      * @param string $str
      */
     protected function write($str)
     {
+        if (! empty($this->strippedSemicolon)) {
+            echo $this->strippedSemicolon;
+
+            $this->strippedSemicolon = '';
+        }
+
+        /*
+         * Maybe Strip semi-colon appended by property(); it's a separator, not a terminator
+         * will be striped for real before a closing, otherwise displayed unchanged starting the next write
+         */
+        if (! $this->keepSemicolons &&
+            $str &&
+            (strpos($str, ';') !== false) &&
+            (substr($str, -1) === ';')
+        ) {
+            $str = substr($str, 0, -1);
+
+            $this->strippedSemicolon = ';';
+        }
+
         if ($this->sourceMapGenerator) {
             $this->sourceMapGenerator->addMapping(
                 $this->currentLine,
index 8eec475..9549c6c 100644 (file)
@@ -55,7 +55,7 @@ class Expanded extends Formatter
 
         foreach ($block->lines as $index => $line) {
             if (substr($line, 0, 2) === '/*') {
-                $block->lines[$index] = preg_replace('/(\r|\n)+/', $glue, $line);
+                $block->lines[$index] = preg_replace('/[\r\n]+/', $glue, $line);
             }
         }
 
index 50a70ce..f9e7f03 100644 (file)
@@ -63,24 +63,13 @@ class Nested extends Formatter
 
         foreach ($block->lines as $index => $line) {
             if (substr($line, 0, 2) === '/*') {
-                $block->lines[$index] = preg_replace('/(\r|\n)+/', $glue, $line);
+                $block->lines[$index] = preg_replace('/[\r\n]+/', $glue, $line);
             }
         }
 
         $this->write($inner . implode($glue, $block->lines));
     }
 
-    protected function hasFlatChild($block)
-    {
-        foreach ($block->children as $child) {
-            if (empty($child->selectors)) {
-                return true;
-            }
-        }
-
-        return false;
-    }
-
     /**
      * {@inheritdoc}
      */
@@ -109,7 +98,7 @@ class Nested extends Formatter
             array_pop($depths);
             $this->depth--;
 
-            if (!$this->depth && ($block->depth <= 1 || (!$this->indentLevel && $block->type === Type::T_COMMENT)) &&
+            if (! $this->depth && ($block->depth <= 1 || (! $this->indentLevel && $block->type === Type::T_COMMENT)) &&
                 (($block->selectors && ! $isMediaOrDirective) || $previousHasSelector)
             ) {
                 $downLevel = $this->break;
@@ -170,15 +159,18 @@ class Nested extends Formatter
             }
 
             $this->blockLines($block);
+
             $closeBlock = $this->break;
         }
 
         if (! empty($block->children)) {
-            if ($this->depth>0 && ($isMediaOrDirective || ! $this->hasFlatChild($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);
@@ -193,13 +185,19 @@ class Nested extends Formatter
         if (! empty($block->selectors)) {
             $this->indentLevel--;
 
+            if (! $this->keepSemicolons) {
+                $this->strippedSemicolon = '';
+            }
+
             $this->write($this->close);
+
             $closeBlock = $this->break;
 
             if ($this->depth > 1 && ! empty($block->children)) {
                 array_pop($depths);
                 $this->depth--;
             }
+
             if (! $isMediaOrDirective) {
                 $previousHasSelector = true;
             }
@@ -209,4 +207,22 @@ class Nested extends Formatter
             $this->write($this->break);
         }
     }
+
+    /**
+     * Block has flat child
+     *
+     * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $block
+     *
+     * @return boolean
+     */
+    private function hasFlatChild($block)
+    {
+        foreach ($block->children as $child) {
+            if (empty($child->selectors)) {
+                return true;
+            }
+        }
+
+        return false;
+    }
 }
index 1e83bf8..acbabff 100644 (file)
@@ -141,11 +141,11 @@ class Number extends Node implements \ArrayAccess
     public function offsetExists($offset)
     {
         if ($offset === -3) {
-            return $this->sourceColumn !== null;
+            return ! is_null($this->sourceColumn);
         }
 
         if ($offset === -2) {
-            return $this->sourceLine !== null;
+            return ! is_null($this->sourceLine);
         }
 
         if ($offset === -1 ||
index ed9b28f..6a30af1 100644 (file)
@@ -218,8 +218,8 @@ class Parser
      *
      * @api
      *
-     * @param string $buffer
-     * @param string $out
+     * @param string       $buffer
+     * @param string|array $out
      *
      * @return boolean
      */
@@ -245,8 +245,8 @@ class Parser
      *
      * @api
      *
-     * @param string $buffer
-     * @param string $out
+     * @param string       $buffer
+     * @param string|array $out
      *
      * @return boolean
      */
@@ -272,10 +272,10 @@ class Parser
      *
      * @api
      *
-     * @param string $buffer
-     * @param string $out
+     * @param string       $buffer
+     * @param string|array $out
      *
-     * @return array
+     * @return boolean
      */
     public function parseMediaQueryList($buffer, &$out)
     {
@@ -287,7 +287,6 @@ class Parser
 
         $this->saveEncoding();
 
-
         $isMediaQuery = $this->mediaQueryList($out);
 
         $this->restoreEncoding();
@@ -343,11 +342,14 @@ class Parser
             if ($this->literal('@at-root', 8) &&
                 ($this->selectors($selector) || true) &&
                 ($this->map($with) || true) &&
+                (($this->matchChar('(')
+                    && $this->interpolation($with)
+                    && $this->matchChar(')')) || true) &&
                 $this->matchChar('{', false)
             ) {
                 $atRoot = $this->pushSpecialBlock(Type::T_AT_ROOT, $s);
                 $atRoot->selector = $selector;
-                $atRoot->with = $with;
+                $atRoot->with     = $with;
 
                 return true;
             }
@@ -383,9 +385,18 @@ class Parser
                     ($this->argValues($argValues) || true) &&
                     $this->matchChar(')') || true) &&
                 ($this->end() ||
+                    ($this->literal('using', 5) &&
+                        $this->argumentDef($argUsing) &&
+                        ($this->end() || $this->matchChar('{') && $hasBlock = true)) ||
                     $this->matchChar('{') && $hasBlock = true)
             ) {
-                $child = [Type::T_INCLUDE, $mixinName, isset($argValues) ? $argValues : null, null];
+                $child = [
+                    Type::T_INCLUDE,
+                    $mixinName,
+                    isset($argValues) ? $argValues : null,
+                    null,
+                    isset($argUsing) ? $argUsing : null
+                ];
 
                 if (! empty($hasBlock)) {
                     $include = $this->pushSpecialBlock(Type::T_INCLUDE, $s);
@@ -524,9 +535,9 @@ class Parser
                 $this->matchChar('{', false)
             ) {
                 $for = $this->pushSpecialBlock(Type::T_FOR, $s);
-                $for->var = $varName[1];
+                $for->var   = $varName[1];
                 $for->start = $start;
-                $for->end = $end;
+                $for->end   = $end;
                 $for->until = isset($forUntil);
 
                 return true;
@@ -536,7 +547,13 @@ class Parser
 
             if ($this->literal('@if', 3) && $this->valueList($cond) && $this->matchChar('{', false)) {
                 $if = $this->pushSpecialBlock(Type::T_IF, $s);
-                $if->cond = $cond;
+                while ($cond[0] === Type::T_LIST
+                    && !empty($cond['enclosing'])
+                    && $cond['enclosing'] === 'parent'
+                    && count($cond[2]) == 1) {
+                    $cond = reset($cond[2]);
+                }
+                $if->cond  = $cond;
                 $if->cases = [];
 
                 return true;
@@ -577,8 +594,15 @@ class Parser
 
             $this->seek($s);
 
-            if ($this->literal('@content', 8) && $this->end()) {
-                $this->append([Type::T_MIXIN_CONTENT], $s);
+            #if ($this->literal('@content', 8))
+
+            if ($this->literal('@content', 8) &&
+                ($this->end() ||
+                    $this->matchChar('(') &&
+                        $this->argValues($argContent) &&
+                        $this->matchChar(')') &&
+                    $this->end())) {
+                $this->append([Type::T_MIXIN_CONTENT, isset($argContent) ? $argContent : null], $s);
 
                 return true;
             }
@@ -633,9 +657,10 @@ class Parser
 
             if ($this->literal('@supports', 9) &&
                 ($t1=$this->supportsQuery($supportQuery)) &&
-                ($t2=$this->matchChar('{', false)) ) {
+                ($t2=$this->matchChar('{', false))
+            ) {
                 $directive = $this->pushSpecialBlock(Type::T_DIRECTIVE, $s);
-                $directive->name = 'supports';
+                $directive->name  = 'supports';
                 $directive->value = $supportQuery;
 
                 return true;
@@ -665,6 +690,19 @@ class Parser
 
             $this->seek($s);
 
+            // maybe it's a generic blockless directive
+            if ($this->matchChar('@', false) &&
+                $this->keyword($dirName) &&
+                $this->valueList($dirValue) &&
+                $this->end()
+            ) {
+                $this->append([Type::T_DIRECTIVE, [$dirName, $dirValue]], $s);
+
+                return true;
+            }
+
+            $this->seek($s);
+
             return false;
         }
 
@@ -709,7 +747,7 @@ class Parser
 
             if ($this->eatWhiteDefault) {
                 $this->whitespace();
-                $this->append(null); // collect comments at the begining if needed
+                $this->append(null); // collect comments at the beginning if needed
             }
 
             return true;
@@ -751,7 +789,7 @@ class Parser
         if ($this->matchChar('}', false)) {
             $block = $this->popBlock();
 
-            if (!isset($block->type) || $block->type !== Type::T_IF) {
+            if (! isset($block->type) || $block->type !== Type::T_IF) {
                 if ($this->env->parent) {
                     $this->append(null); // collect comments before next statement if needed
                 }
@@ -823,7 +861,7 @@ class Parser
 
         $this->env = $b;
 
-        // collect comments at the begining of a block if needed
+        // collect comments at the beginning of a block if needed
         if ($this->eatWhiteDefault) {
             $this->whitespace();
 
@@ -1055,16 +1093,17 @@ class Parser
             if (isset($m[1]) && empty($this->commentsSeen[$this->count])) {
                 // comment that are kept in the output CSS
                 $comment = [];
+                $startCommentCount = $this->count;
                 $endCommentCount = $this->count + strlen($m[1]);
 
                 // find interpolations in comment
                 $p = strpos($this->buffer, '#{', $this->count);
 
                 while ($p !== false && $p < $endCommentCount) {
-                    $c = substr($this->buffer, $this->count, $p - $this->count);
-                    $comment[] = $c;
+                    $c           = substr($this->buffer, $this->count, $p - $this->count);
+                    $comment[]   = $c;
                     $this->count = $p;
-                    $out = null;
+                    $out         = null;
 
                     if ($this->interpolation($out)) {
                         // keep right spaces in the following string part
@@ -1076,7 +1115,7 @@ class Parser
                             $out[3] = '';
                         }
 
-                        $comment[] = $out;
+                        $comment[] = [Type::T_COMMENT, substr($this->buffer, $p, $this->count - $p), $out];
                     } else {
                         $comment[] = substr($this->buffer, $this->count, 2);
 
@@ -1094,10 +1133,11 @@ class Parser
                     $this->appendComment([Type::T_COMMENT, $c]);
                 } else {
                     $comment[] = $c;
-                    $this->appendComment([Type::T_COMMENT, [Type::T_STRING, '', $comment]]);
+                    $staticComment = substr($this->buffer, $startCommentCount, $endCommentCount - $startCommentCount);
+                    $this->appendComment([Type::T_COMMENT, $staticComment, [Type::T_STRING, '', $comment]]);
                 }
 
-                $this->commentsSeen[$this->count] = true;
+                $this->commentsSeen[$startCommentCount] = true;
                 $this->count = $endCommentCount;
             } else {
                 // comment that are ignored and not kept in the output css
@@ -1118,8 +1158,21 @@ class Parser
     protected function appendComment($comment)
     {
         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);
+            if ($comment[0] === Type::T_COMMENT) {
+                if (is_string($comment[1])) {
+                    $comment[1] = substr(preg_replace(['/^\s+/m', '/^(.)/m'], ['', ' \1'], $comment[1]), 1);
+                }
+                if (isset($comment[2]) and is_array($comment[2]) and $comment[2][0] === Type::T_STRING) {
+                    foreach ($comment[2][2] as $k => $v) {
+                        if (is_string($v)) {
+                            $p = strpos($v, "\n");
+                            if ($p !== false) {
+                                $comment[2][2][$k] = substr($v, 0, $p + 1)
+                                    . preg_replace(['/^\s+/m', '/^(.)/m'], ['', ' \1'], substr($v, $p+1));
+                            }
+                        }
+                    }
+                }
             }
 
             $this->env->comments[] = $comment;
@@ -1135,7 +1188,7 @@ class Parser
     protected function append($statement, $pos = null)
     {
         if (! is_null($statement)) {
-            if ($pos !== null) {
+            if (! is_null($pos)) {
                 list($line, $column) = $this->getSourcePosition($pos);
 
                 $statement[static::SOURCE_LINE]   = $line;
@@ -1247,12 +1300,14 @@ class Parser
         $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(')')) {
+            $this->matchChar(')')
+         ) {
             $support = [Type::T_STRING, '', [[Type::T_KEYWORD, ($not ? 'not ' : '') . '(']]];
             $support[2][] = $property;
             $support[2][] = [Type::T_KEYWORD, ': '];
@@ -1267,7 +1322,8 @@ class Parser
 
         if ($this->matchChar('(') &&
             $this->supportsQuery($subQuery) &&
-            $this->matchChar(')')) {
+            $this->matchChar(')')
+        ) {
             $parts[] = [Type::T_STRING, '', [[Type::T_KEYWORD, '('], $subQuery, [Type::T_KEYWORD, ')']]];
             $s = $this->count;
         } else {
@@ -1275,7 +1331,8 @@ class Parser
         }
 
         if ($this->literal('not', 3) &&
-            $this->supportsQuery($subQuery)) {
+            $this->supportsQuery($subQuery)
+        ) {
             $parts[] = [Type::T_STRING, '', [[Type::T_KEYWORD, 'not '], $subQuery]];
             $s = $this->count;
         } else {
@@ -1284,12 +1341,15 @@ class Parser
 
         if ($this->literal('selector(', 9) &&
             $this->selector($selector) &&
-            $this->matchChar(')')) {
+            $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;
@@ -1297,6 +1357,7 @@ class Parser
                         $compound[2][] = [Type::T_KEYWORD, $scp];
                     }
                 }
+
                 $selectorList[2][] = $compound;
             }
             $support[2][] = $selectorList;
@@ -1317,6 +1378,7 @@ class Parser
         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 {
@@ -1326,6 +1388,7 @@ class Parser
         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 {
@@ -1336,7 +1399,9 @@ class Parser
             if ($this->eatWhiteDefault) {
                 $this->whitespace();
             }
+
             $out = [Type::T_STRING, '', $parts];
+
             return true;
         }
 
@@ -1408,6 +1473,7 @@ class Parser
 
         if (! $this->variable($keyword) || ! $this->matchChar(':')) {
             $this->seek($s);
+
             $keyword = null;
         }
 
@@ -1436,7 +1502,12 @@ class Parser
      */
     protected function valueList(&$out)
     {
-        return $this->genericList($out, 'spaceList', ',');
+        $discardComments = $this->discardComments;
+        $this->discardComments = true;
+        $res = $this->genericList($out, 'spaceList', ',');
+        $this->discardComments = $discardComments;
+
+        return $res;
     }
 
     /**
@@ -1463,17 +1534,19 @@ class Parser
      */
     protected function genericList(&$out, $parseItem, $delim = '', $flatten = true)
     {
-        $s = $this->count;
+        $s     = $this->count;
         $items = [];
         $value = null;
 
         while ($this->$parseItem($value)) {
+            $trailing_delim = false;
             $items[] = $value;
 
             if ($delim) {
                 if (! $this->literal($delim, strlen($delim))) {
                     break;
                 }
+                $trailing_delim = true;
             }
         }
 
@@ -1483,6 +1556,9 @@ class Parser
             return false;
         }
 
+        if ($trailing_delim) {
+            $items[] = [Type::T_NULL];
+        }
         if ($flatten && count($items) === 1) {
             $out = $items[0];
         } else {
@@ -1496,41 +1572,58 @@ class Parser
      * Parse expression
      *
      * @param array $out
+     * @param bool $listOnly
+     * @param bool $lookForExp
      *
      * @return boolean
      */
-    protected function expression(&$out)
+    protected function expression(&$out, $listOnly = false, $lookForExp = true)
     {
         $s = $this->count;
         $discard = $this->discardComments;
         $this->discardComments = true;
+        $allowedTypes = ($listOnly ? [Type::T_LIST] : [Type::T_LIST, Type::T_MAP]);
 
         if ($this->matchChar('(')) {
-            if ($this->parenExpression($out, $s, ")")) {
+            if ($this->enclosedExpression($lhs, $s, ")", $allowedTypes)) {
+                if ($lookForExp) {
+                    $out = $this->expHelper($lhs, 0);
+                } else {
+                    $out = $lhs;
+                }
+
                 $this->discardComments = $discard;
+
                 return true;
             }
 
             $this->seek($s);
         }
 
-        if ($this->matchChar('[')) {
-            if ($this->parenExpression($out, $s, "]", [Type::T_LIST, Type::T_KEYWORD])) {
-                if ($out[0] !== Type::T_LIST && $out[0] !== Type::T_MAP) {
-                    $out = [Type::T_STRING, '', [ '[', $out, ']' ]];
+        if (in_array(Type::T_LIST, $allowedTypes) && $this->matchChar('[')) {
+            if ($this->enclosedExpression($lhs, $s, "]", [Type::T_LIST])) {
+                if ($lookForExp) {
+                    $out = $this->expHelper($lhs, 0);
+                } else {
+                    $out = $lhs;
                 }
-
                 $this->discardComments = $discard;
+
                 return true;
             }
 
             $this->seek($s);
         }
 
-        if ($this->value($lhs)) {
-            $out = $this->expHelper($lhs, 0);
+        if (!$listOnly && $this->value($lhs)) {
+            if ($lookForExp) {
+                $out = $this->expHelper($lhs, 0);
+            } else {
+                $out = $lhs;
+            }
 
             $this->discardComments = $discard;
+
             return true;
         }
 
@@ -1548,15 +1641,35 @@ class Parser
      *
      * @return boolean
      */
-    protected function parenExpression(&$out, $s, $closingParen = ")", $allowedTypes = [Type::T_LIST, Type::T_MAP])
+    protected function enclosedExpression(&$out, $s, $closingParen = ")", $allowedTypes = [Type::T_LIST, Type::T_MAP])
     {
-        if ($this->matchChar($closingParen)) {
+        if ($this->matchChar($closingParen) && in_array(Type::T_LIST, $allowedTypes)) {
             $out = [Type::T_LIST, '', []];
-
+            switch ($closingParen) {
+                case ")":
+                    $out['enclosing'] = 'parent'; // parenthesis list
+                    break;
+                case "]":
+                    $out['enclosing'] = 'bracket'; // bracketed list
+                    break;
+            }
             return true;
         }
 
-        if ($this->valueList($out) && $this->matchChar($closingParen) && in_array($out[0], $allowedTypes)) {
+        if ($this->valueList($out) && $this->matchChar($closingParen)
+            && in_array($out[0], [Type::T_LIST, Type::T_KEYWORD])
+            && in_array(Type::T_LIST, $allowedTypes)) {
+            if ($out[0] !== Type::T_LIST || ! empty($out['enclosing'])) {
+                $out = [Type::T_LIST, '', [$out]];
+            }
+            switch ($closingParen) {
+                case ")":
+                    $out['enclosing'] = 'parent'; // parenthesis list
+                    break;
+                case "]":
+                    $out['enclosing'] = 'bracket'; // bracketed list
+                    break;
+            }
             return true;
         }
 
@@ -1600,7 +1713,7 @@ class Parser
                 break;
             }
 
-            if (! $this->value($rhs)) {
+            if (! $this->value($rhs) && ! $this->expression($rhs, true, false)) {
                 break;
             }
 
@@ -1655,7 +1768,7 @@ class Parser
 
         $this->seek($s);
 
-        if ($this->literal('url(', 4, false) && $this->match('\s*(\/\/\S+)\s*', $m)) {
+        if ($this->literal('url(', 4, false) && $this->match('\s*(\/\/[^\s\)]+)\s*', $m)) {
             $content = 'url(' . $m[1];
 
             if ($this->matchChar(')')) {
@@ -1751,6 +1864,7 @@ class Parser
         // unicode range with wildcards
         if ($this->literal('U+', 2) && $this->match('([0-9A-F]+\?*)(-([0-9A-F]+))?', $m, false)) {
             $out = [Type::T_KEYWORD, 'U+' . $m[0]];
+
             return true;
         }
 
@@ -2040,55 +2154,16 @@ class Parser
      */
     protected function color(&$out)
     {
-        $color = [Type::T_COLOR];
-        $s     = $this->count;
+        $s = $this->count;
 
         if ($this->match('(#([0-9a-f]+))', $m)) {
-            $nofValues = strlen($m[2]);
-            $hasAlpha  = $nofValues === 4 || $nofValues === 8;
-            $channels  = $hasAlpha ? [4, 3, 2, 1] : [3, 2, 1];
-
-            switch ($nofValues) {
-                case 3:
-                case 4:
-                    $num = hexdec($m[2]);
-
-                    foreach ($channels as $i) {
-                        $t = $num & 0xf;
-                        $color[$i] = $t << 4 | $t;
-                        $num >>= 4;
-                    }
-
-                    break;
-
-                case 6:
-                case 8:
-                    $num = hexdec($m[2]);
-
-                    foreach ($channels as $i) {
-                        $color[$i] = $num & 0xff;
-                        $num >>= 8;
-                    }
-
-                    break;
-
-                default:
-                    $this->seek($s);
-
-                    return false;
-            }
-
-            if ($hasAlpha) {
-                if ($color[4] === 255) {
-                    $color[4] = 1; // fully opaque
-                } else {
-                    $color[4] = round($color[4] / 255, 3);
-                }
+            if (in_array(strlen($m[2]), [3,4,6,8])) {
+                $out = [Type::T_KEYWORD, $m[0]];
+                return true;
             }
 
-            $out = $color;
-
-            return true;
+            $this->seek($s);
+            return false;
         }
 
         return false;
@@ -2322,8 +2397,8 @@ class Parser
     /**
      * Parser interpolation
      *
-     * @param array   $out
-     * @param boolean $lookWhite save information about whitespace before and after
+     * @param string|array $out
+     * @param boolean      $lookWhite save information about whitespace before and after
      *
      * @return boolean
      */
@@ -2428,7 +2503,8 @@ class Parser
     /**
      * Parse comma separated selector list
      *
-     * @param array $out
+     * @param array   $out
+     * @param boolean $subSelector
      *
      * @return boolean
      */
@@ -2463,7 +2539,8 @@ class Parser
     /**
      * Parse whitespace separated selector list
      *
-     * @param array $out
+     * @param array   $out
+     * @param boolean $subSelector
      *
      * @return boolean
      */
@@ -2472,9 +2549,17 @@ class Parser
         $selector = [];
 
         for (;;) {
+            $s = $this->count;
+
             if ($this->match('[>+~]+', $m, true)) {
-                $selector[] = [$m[0]];
-                continue;
+                if ($subSelector && is_string($subSelector) && strpos($subSelector, 'nth-') === 0 &&
+                    $m[0] === '+' && $this->match("(\d+|n\b)", $counter)
+                ) {
+                    $this->seek($s);
+                } else {
+                    $selector[] = [$m[0]];
+                    continue;
+                }
             }
 
             if ($this->selectorSingle($part, $subSelector)) {
@@ -2507,7 +2592,8 @@ class Parser
      *     div[yes=no]#something.hello.world:nth-child(-2n+1)%placeholder
      * }}
      *
-     * @param array $out
+     * @param array   $out
+     * @param boolean $subSelector
      *
      * @return boolean
      */
@@ -2606,10 +2692,13 @@ class Parser
                     $ss = $this->count;
 
                     if ($nameParts === ['not'] || $nameParts === ['is'] ||
-                        $nameParts === ['has'] || $nameParts === ['where']
+                        $nameParts === ['has'] || $nameParts === ['where'] ||
+                        $nameParts === ['slotted'] ||
+                        $nameParts === ['nth-child'] || $nameParts == ['nth-last-child'] ||
+                        $nameParts === ['nth-of-type'] || $nameParts == ['nth-last-of-type']
                     ) {
-                        if ($this->matchChar('(') &&
-                          ($this->selectors($subs, true) || true) &&
+                        if ($this->matchChar('(', true) &&
+                          ($this->selectors($subs, reset($nameParts)) || true) &&
                           $this->matchChar(')')
                         ) {
                             $parts[] = '(';
@@ -2619,10 +2708,12 @@ class Parser
                                     foreach ($ps as &$p) {
                                         $parts[] = $p;
                                     }
+
                                     if (count($sub) && reset($sub)) {
                                         $parts[] = ' ';
                                     }
                                 }
+
                                 if (count($subs) && reset($subs)) {
                                     $parts[] = ', ';
                                 }
@@ -2655,6 +2746,17 @@ class Parser
 
             $this->seek($s);
 
+            // 2n+1
+            if ($subSelector && is_string($subSelector) && strpos($subSelector, 'nth-') === 0) {
+                if ($this->match("(\s*(\+\s*|\-\s*)?(\d+|n|\d+n))+", $counter)) {
+                    $parts[] = $counter[0];
+                    //$parts[] = str_replace(' ', '', $counter[0]);
+                    continue;
+                }
+            }
+
+            $this->seek($s);
+
             // attribute selector
             if ($char === '[' &&
                 $this->matchChar('[') &&
@@ -2769,7 +2871,7 @@ class Parser
     /**
      * Parse a placeholder
      *
-     * @param string $placeholder
+     * @param string|array $placeholder
      *
      * @return boolean
      */
@@ -2785,6 +2887,7 @@ class Parser
 
             return true;
         }
+
         if ($this->interpolation($placeholder)) {
             return true;
         }
@@ -2846,12 +2949,9 @@ class Parser
             while ($lastNode[0] === Type::T_KEYWORD && in_array($lastNode[1], ['!default', '!global'])) {
                 array_pop($token[2]);
 
-                $node = end($token[2]);
-
-                $token = $this->flattenList($token);
-
-                $flags[] = $lastNode[1];
-
+                $node     = end($token[2]);
+                $token    = $this->flattenList($token);
+                $flags[]  = $lastNode[1];
                 $lastNode = $node;
             }
         }
@@ -2869,9 +2969,8 @@ class Parser
     protected function stripOptionalFlag(&$selectors)
     {
         $optional = false;
-
         $selector = end($selectors);
-        $part = end($selector);
+        $part     = end($selector);
 
         if ($part === ['!optional']) {
             array_pop($selectors[count($selectors) - 1]);
@@ -2915,6 +3014,8 @@ class Parser
             $validChars = $allowNewline ? '.' : "[^\n]";
         }
 
+        $m = null;
+
         if (! $this->match('(' . $validChars . '*?)' . $this->pregQuote($what), $m, ! $until)) {
             return false;
         }
@@ -3014,9 +3115,10 @@ class Parser
             return;
         }
 
-        $iniDirective = 'mbstring' . '.func_overload'; // deprecated in PHP 7.2
+        // deprecated in PHP 7.2
+        $iniDirective = 'mbstring.func_overload';
 
-        if (ini_get($iniDirective) & 2) {
+        if (extension_loaded('mbstring') && ini_get($iniDirective) & 2) {
             $this->encoding = mb_internal_encoding();
 
             mb_internal_encoding('iso-8859-1');
@@ -3028,7 +3130,7 @@ class Parser
      */
     private function restoreEncoding()
     {
-        if ($this->encoding) {
+        if (extension_loaded('mbstring') && $this->encoding) {
             mb_internal_encoding($this->encoding);
         }
     }
index d6f6452..61c1d30 100644 (file)
@@ -130,8 +130,17 @@ class Base64VLQ
     private static function fromVLQSigned($value)
     {
         $negate = ($value & 1) === 1;
-        $value = $value >> 1;
+        $value = ($value >> 1) & ~(1<<(8 * PHP_INT_SIZE - 1)); // unsigned right shift
 
-        return $negate ? -$value : $value;
+        if (! $negate) {
+            return $value;
+        }
+
+        // We need to OR 0x80000000 here to ensure the 32nd bit (the sign bit) is
+        // always set for negative numbers. If `value` were 1, (meaning `negate` is
+        // true and all other bits were zeros), `value` would now be 0. -0 is just
+        // 0, and doesn't flip the 32nd bit as intended. All positive numbers will
+        // successfully flip the 32nd bit without issue, so it's a noop for them.
+        return -$value | 0x80000000;
     }
 }
diff --git a/lib/scssphp/SourceMap/Base64VLQEncoder.php b/lib/scssphp/SourceMap/Base64VLQEncoder.php
deleted file mode 100644 (file)
index 365b4d9..0000000
+++ /dev/null
@@ -1,217 +0,0 @@
-<?php
-/**
- * SCSSPHP
- *
- * @copyright 2012-2019 Leaf Corcoran
- *
- * @license http://opensource.org/licenses/MIT MIT
- *
- * @link http://scssphp.github.io/scssphp
- */
-
-namespace ScssPhp\ScssPhp\SourceMap;
-
-/**
- * Base64 VLQ Encoder
- *
- * {@internal Derivative of oyejorge/less.php's lib/SourceMap/Base64VLQ.php, relicensed with permission. }}
- *
- * @author Josh Schmidt <oyejorge@gmail.com>
- * @author Nicolas FRANÇOIS <nicolas.francois@frog-labs.com>
- */
-class Base64VLQEncoder
-{
-    /**
-     * Shift
-     *
-     * @var integer
-     */
-    private $shift = 5;
-
-    /**
-     * Mask
-     *
-     * @var integer
-     */
-    private $mask = 0x1F; // == (1 << shift) == 0b00011111
-
-    /**
-     * Continuation bit
-     *
-     * @var integer
-     */
-    private $continuationBit = 0x20; // == (mask - 1 ) == 0b00100000
-
-    /**
-     * Char to integer map
-     *
-     * @var array
-     */
-    private $charToIntMap = [
-        'A' => 0,  'B' => 1,  'C' => 2,  'D' => 3,  'E' => 4,  'F' => 5,  'G' => 6,  'H' => 7,
-        'I' => 8,  'J' => 9,  'K' => 10, 'L' => 11, 'M' => 12, 'N' => 13, 'O' => 14, 'P' => 15,
-        'Q' => 16, 'R' => 17, 'S' => 18, 'T' => 19, 'U' => 20, 'V' => 21, 'W' => 22, 'X' => 23,
-        'Y' => 24, 'Z' => 25, 'a' => 26, 'b' => 27, 'c' => 28, 'd' => 29, 'e' => 30, 'f' => 31,
-        'g' => 32, 'h' => 33, 'i' => 34, 'j' => 35, 'k' => 36, 'l' => 37, 'm' => 38, 'n' => 39,
-        'o' => 40, 'p' => 41, 'q' => 42, 'r' => 43, 's' => 44, 't' => 45, 'u' => 46, 'v' => 47,
-        'w' => 48, 'x' => 49, 'y' => 50, 'z' => 51,   0 => 52,   1 => 53,   2 => 54,   3 => 55,
-          4 => 56,   5 => 57,   6 => 58,   7 => 59,   8 => 60,   9 => 61, '+' => 62, '/' => 63,
-    ];
-
-    /**
-     * Integer to char map
-     *
-     * @var array
-     */
-    private $intToCharMap = [
-         0 => 'A',  1 => 'B',  2 => 'C',  3 => 'D',  4 => 'E',  5 => 'F',  6 => 'G',  7 => 'H',
-         8 => 'I',  9 => 'J', 10 => 'K', 11 => 'L', 12 => 'M', 13 => 'N', 14 => 'O', 15 => 'P',
-        16 => 'Q', 17 => 'R', 18 => 'S', 19 => 'T', 20 => 'U', 21 => 'V', 22 => 'W', 23 => 'X',
-        24 => 'Y', 25 => 'Z', 26 => 'a', 27 => 'b', 28 => 'c', 29 => 'd', 30 => 'e', 31 => 'f',
-        32 => 'g', 33 => 'h', 34 => 'i', 35 => 'j', 36 => 'k', 37 => 'l', 38 => 'm', 39 => 'n',
-        40 => 'o', 41 => 'p', 42 => 'q', 43 => 'r', 44 => 's', 45 => 't', 46 => 'u', 47 => 'v',
-        48 => 'w', 49 => 'x', 50 => 'y', 51 => 'z', 52 => '0', 53 => '1', 54 => '2', 55 => '3',
-        56 => '4', 57 => '5', 58 => '6', 59 => '7', 60 => '8', 61 => '9', 62 => '+', 63 => '/',
-    ];
-
-    /**
-     * Constructor
-     */
-    public function __construct()
-    {
-        // I leave it here for future reference
-        // foreach (str_split('ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/') as $i => $char)
-        // {
-        //     $this->charToIntMap[$char] = $i;
-        //     $this->intToCharMap[$i] = $char;
-        // }
-    }
-
-    /**
-     * Convert from a two-complement value to a value where the sign bit is
-     * is placed in the least significant bit. For example, as decimals:
-     *   1 becomes 2 (10 binary), -1 becomes 3 (11 binary)
-     *   2 becomes 4 (100 binary), -2 becomes 5 (101 binary)
-     * We generate the value for 32 bit machines, hence -2147483648 becomes 1, not 4294967297,
-     * even on a 64 bit machine.
-     *
-     * @param string $aValue
-     */
-    public function toVLQSigned($aValue)
-    {
-        return 0xffffffff & ($aValue < 0 ? ((-$aValue) << 1) + 1 : ($aValue << 1) + 0);
-    }
-
-    /**
-     * Convert to a two-complement value from a value where the sign bit is
-     * is placed in the least significant bit. For example, as decimals:
-     *   2 (10 binary) becomes 1, 3 (11 binary) becomes -1
-     *   4 (100 binary) becomes 2, 5 (101 binary) becomes -2
-     * We assume that the value was generated with a 32 bit machine in mind.
-     * Hence
-     *   1 becomes -2147483648
-     * even on a 64 bit machine.
-     *
-     * @param integer $aValue
-     */
-    public function fromVLQSigned($aValue)
-    {
-        return $aValue & 1 ? $this->zeroFill(~$aValue + 2, 1) | (-1 - 0x7fffffff) : $this->zeroFill($aValue, 1);
-    }
-
-    /**
-     * Return the base 64 VLQ encoded value.
-     *
-     * @param string $aValue The value to encode
-     *
-     * @return string The encoded value
-     */
-    public function encode($aValue)
-    {
-        $encoded = '';
-        $vlq = $this->toVLQSigned($aValue);
-
-        do {
-            $digit = $vlq & $this->mask;
-            $vlq = $this->zeroFill($vlq, $this->shift);
-
-            if ($vlq > 0) {
-                $digit |= $this->continuationBit;
-            }
-
-            $encoded .= $this->base64Encode($digit);
-        } while ($vlq > 0);
-
-        return $encoded;
-    }
-
-    /**
-     * Return the value decoded from base 64 VLQ.
-     *
-     * @param string $encoded The encoded value to decode
-     *
-     * @return integer The decoded value
-     */
-    public function decode($encoded)
-    {
-        $vlq = 0;
-        $i = 0;
-
-        do {
-            $digit = $this->base64Decode($encoded[$i]);
-            $vlq |= ($digit & $this->mask) << ($i * $this->shift);
-            $i++;
-        } while ($digit & $this->continuationBit);
-
-        return $this->fromVLQSigned($vlq);
-    }
-
-    /**
-     * Right shift with zero fill.
-     *
-     * @param integer $a number to shift
-     * @param integer $b number of bits to shift
-     *
-     * @return integer
-     */
-    public function zeroFill($a, $b)
-    {
-        return ($a >= 0) ? ($a >> $b) : ($a >> $b) & (PHP_INT_MAX >> ($b - 1));
-    }
-
-    /**
-     * Encode single 6-bit digit as base64.
-     *
-     * @param integer $number
-     *
-     * @return string
-     *
-     * @throws \Exception If the number is invalid
-     */
-    public function base64Encode($number)
-    {
-        if ($number < 0 || $number > 63) {
-            throw new \Exception(sprintf('Invalid number "%s" given. Must be between 0 and 63.', $number));
-        }
-
-        return $this->intToCharMap[$number];
-    }
-
-    /**
-     * Decode single 6-bit digit from base64
-     *
-     * @param string $char
-     *
-     * @return integer
-     *
-     * @throws \Exception If the number is invalid
-     */
-    public function base64Decode($char)
-    {
-        if (! array_key_exists($char, $this->charToIntMap)) {
-            throw new \Exception(sprintf('Invalid base 64 digit "%s" given.', $char));
-        }
-
-        return $this->charToIntMap[$char];
-    }
-}
index d5e6dc0..db3e7a3 100644 (file)
@@ -18,5 +18,5 @@ namespace ScssPhp\ScssPhp;
  */
 class Version
 {
-    const VERSION = 'v1.0.2';
+    const VERSION = 'v1.0.6';
 }
index f0b5311..99c47af 100644 (file)
@@ -5,16 +5,11 @@ Downloaded from: https://github.com/scssphp/scssphp
 
 Import procedure:
 
-- Copy all the files from the folder 'src' this directory.
+- Delete everything from this directory except moodle_readme.txt (this file).
+- Copy all the files from the folder 'src' to this directory.
 - Copy the license file from the project root.
 - Review the local changes defined below, if any. Reapply
   them if needed. If already available upstream, please remove
   them from the list.
 
 Licensed under MIT, Copyright (c) 2015 Leaf Corcoran.
-
-Currenly using 1.0.2 plus these local changes:
-
-- MDL-67114 : Added basic compatibility with php 7.4. This corresponds to
-      upstream commit https://github.com/scssphp/scssphp/commit/66675c1553b7e9d7c480d8aaedbf7c72374647cf
-      that is available in scssphp >= 1.0.4
index 5630e29..e762c95 100644 (file)
     <location>scssphp</location>
     <name>scssphp</name>
     <license>MIT</license>
-    <version>1.0.2</version>
+    <version>1.0.6</version>
   </library>
   <library>
     <location>spout</location>