MDL-65761 lib: Update scssphp to 0.8.3
authorMathew May <mathewm@hotmail.co.nz>
Tue, 11 Jun 2019 03:53:29 +0000 (11:53 +0800)
committerMathew May <mathewm@hotmail.co.nz>
Tue, 16 Jul 2019 01:15:21 +0000 (09:15 +0800)
15 files changed:
lib/scssphp/Block.php
lib/scssphp/Cache.php [new file with mode: 0644]
lib/scssphp/Compiler.php
lib/scssphp/Compiler/Environment.php
lib/scssphp/Formatter.php
lib/scssphp/Formatter/Compressed.php
lib/scssphp/Formatter/Crunched.php
lib/scssphp/Parser.php
lib/scssphp/SourceMap/Base64.php [new file with mode: 0644]
lib/scssphp/SourceMap/Base64VLQ.php [new file with mode: 0644]
lib/scssphp/SourceMap/Base64VLQEncoder.php
lib/scssphp/SourceMap/SourceMapGenerator.php
lib/scssphp/Util.php
lib/scssphp/Version.php
lib/thirdpartylibs.xml

index a6ef8e0..41abf01 100644 (file)
@@ -62,4 +62,9 @@ class Block
      * @var array
      */
     public $children;
+
+    /**
+     * @var \Leafo\ScssPhp\Block
+     */
+    public $selfParent;
 }
diff --git a/lib/scssphp/Cache.php b/lib/scssphp/Cache.php
new file mode 100644 (file)
index 0000000..49cf631
--- /dev/null
@@ -0,0 +1,239 @@
+<?php
+/**
+ * SCSSPHP
+ *
+ * @copyright 2012-2018 Leaf Corcoran
+ *
+ * @license http://opensource.org/licenses/MIT MIT
+ *
+ * @link http://leafo.github.io/scssphp
+ */
+
+namespace Leafo\ScssPhp;
+
+use Exception;
+
+/**
+ * The scss cache manager.
+ *
+ * In short:
+ *
+ * allow to put in cache/get from cache a generic result from a known operation on a generic dataset,
+ * taking in account options that affects the result
+ *
+ * The cache manager is agnostic about data format and only the operation is expected to be described by string
+ *
+ */
+
+/**
+ * SCSS cache
+ *
+ * @author Cedric Morin
+ */
+class Cache
+{
+    const CACHE_VERSION = 0;
+
+    // directory used for storing data
+    public static $cacheDir = false;
+
+    // prefix for the storing data
+    public static $prefix = 'scssphp_';
+
+    // force a refresh : 'once' for refreshing the first hit on a cache only, true to never use the cache in this hit
+    public static $forceFefresh = false;
+
+    // specifies the number of seconds after which data cached will be seen as 'garbage' and potentially cleaned up
+    public static $gcLifetime = 604800;
+
+    // array of already refreshed cache if $forceFefresh==='once'
+    protected static $refreshed = [];
+
+    /**
+     * Constructor
+     *
+     * @param array $options
+     */
+    public function __construct($options)
+    {
+        // check $cacheDir
+        if (isset($options['cache_dir'])) {
+            self::$cacheDir = $options['cache_dir'];
+        }
+
+        if (empty(self::$cacheDir)) {
+            throw new Exception('cache_dir not set');
+        }
+
+        if (isset($options['prefix'])) {
+            self::$prefix = $options['prefix'];
+        }
+
+        if (empty(self::$prefix)) {
+            throw new Exception('prefix not set');
+        }
+
+        if (isset($options['forceRefresh'])) {
+            self::$forceFefresh = $options['force_refresh'];
+        }
+
+        self::checkCacheDir();
+    }
+
+    /**
+     * Get the cached result of $operation on $what,
+     * which is known as dependant from the content of $options
+     *
+     * @param string  $operation    parse, compile...
+     * @param mixed   $what         content key (e.g., filename to be treated)
+     * @param array   $options      any option that affect the operation result on the content
+     * @param integer $lastModified last modified timestamp
+     *
+     * @return mixed
+     *
+     * @throws \Exception
+     */
+    public function getCache($operation, $what, $options = [], $lastModified = null)
+    {
+        $fileCache = self::$cacheDir . self::cacheName($operation, $what, $options);
+
+        if ((! self::$forceRefresh || (self::$forceRefresh === 'once' && isset(self::$refreshed[$fileCache])))
+            && file_exists($fileCache)
+        ) {
+            $cacheTime = filemtime($fileCache);
+
+            if ((is_null($lastModified) || $cacheTime > $lastModified)
+                && $cacheTime + self::$gcLifetime > time()
+            ) {
+                $c = file_get_contents($fileCache);
+                $c = unserialize($c);
+
+                if (is_array($c) && isset($c['value'])) {
+                    return $c['value'];
+                }
+            }
+        }
+
+        return null;
+    }
+
+    /**
+     * Put in cache the result of $operation on $what,
+     * which is known as dependant from the content of $options
+     *
+     * @param string $operation
+     * @param mixed  $what
+     * @param mixed  $value
+     * @param array  $options
+     */
+    public function setCache($operation, $what, $value, $options = [])
+    {
+        $fileCache = self::$cacheDir . self::cacheName($operation, $what, $options);
+
+        $c = ['value' => $value];
+        $c = serialize($c);
+        file_put_contents($fileCache, $c);
+
+        if (self::$forceRefresh === 'once') {
+            self::$refreshed[$fileCache] = true;
+        }
+    }
+
+    /**
+     * Get the cache name for the caching of $operation on $what,
+     * which is known as dependant from the content of $options
+     *
+     * @param string $operation
+     * @param mixed  $what
+     * @param array  $options
+     *
+     * @return string
+     */
+    private static function cacheName($operation, $what, $options = [])
+    {
+        $t = [
+          'version' => self::CACHE_VERSION,
+          'operation' => $operation,
+          'what' => $what,
+          'options' => $options
+        ];
+
+        $t = self::$prefix
+          . sha1(json_encode($t))
+          . ".$operation"
+          . ".scsscache";
+
+        return $t;
+    }
+
+    /**
+     * Check that the cache dir exists and is writeable
+     *
+     * @throws \Exception
+     */
+    public static function checkCacheDir()
+    {
+        self::$cacheDir = str_replace('\\', '/', self::$cacheDir);
+        self::$cacheDir = rtrim(self::$cacheDir, '/') . '/';
+
+        if (! file_exists(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)) {
+            throw new Exception('Cache directory isn\'t writable: ' . self::$cacheDir);
+        }
+    }
+
+    /**
+     * Delete unused cached files
+     */
+    public static function cleanCache()
+    {
+        static $clean = false;
+
+        if ($clean || empty(self::$cacheDir)) {
+            return;
+        }
+
+        $clean = true;
+
+        // only remove files with extensions created by SCSSPHP Cache
+        // css files removed based on the list files
+        $removeTypes = ['scsscache' => 1];
+
+        $files = scandir(self::$cacheDir);
+
+        if (! $files) {
+            return;
+        }
+
+        $checkTime = time() - self::$gcLifetime;
+
+        foreach ($files as $file) {
+            // don't delete if the file wasn't created with SCSSPHP Cache
+            if (strpos($file, self::$prefix) !== 0) {
+                continue;
+            }
+
+            $parts = explode('.', $file);
+            $type = array_pop($parts);
+
+            if (! isset($removeTypes[$type])) {
+                continue;
+            }
+
+            $fullPath = self::$cacheDir . $file;
+            $mtime = filemtime($fullPath);
+
+            // don't delete if it's a relatively new file
+            if ($mtime > $checkTime) {
+                continue;
+            }
+
+            unlink($fullPath);
+        }
+    }
+}
index 292e960..782799d 100644 (file)
@@ -13,6 +13,7 @@ namespace Leafo\ScssPhp;
 
 use Leafo\ScssPhp\Base\Range;
 use Leafo\ScssPhp\Block;
+use Leafo\ScssPhp\Cache;
 use Leafo\ScssPhp\Colors;
 use Leafo\ScssPhp\Compiler\Environment;
 use Leafo\ScssPhp\Exception\CompilerException;
@@ -98,17 +99,17 @@ class Compiler
         'function' => '^',
     ];
 
-    static public $true = [Type::T_KEYWORD, 'true'];
-    static public $false = [Type::T_KEYWORD, 'false'];
-    static public $null = [Type::T_NULL];
-    static public $nullString = [Type::T_STRING, '', []];
+    static public $true         = [Type::T_KEYWORD, 'true'];
+    static public $false        = [Type::T_KEYWORD, 'false'];
+    static public $null         = [Type::T_NULL];
+    static public $nullString   = [Type::T_STRING, '', []];
     static public $defaultValue = [Type::T_KEYWORD, ''];
     static public $selfSelector = [Type::T_SELF];
-    static public $emptyList = [Type::T_LIST, '', []];
-    static public $emptyMap = [Type::T_MAP, [], []];
-    static public $emptyString = [Type::T_STRING, '"', []];
-    static public $with = [Type::T_KEYWORD, 'with'];
-    static public $without = [Type::T_KEYWORD, 'without'];
+    static public $emptyList    = [Type::T_LIST, '', []];
+    static public $emptyMap     = [Type::T_MAP, [], []];
+    static public $emptyString  = [Type::T_STRING, '"', []];
+    static public $with         = [Type::T_KEYWORD, 'with'];
+    static public $without      = [Type::T_KEYWORD, 'without'];
 
     protected $importPaths = [''];
     protected $importCache = [];
@@ -145,26 +146,48 @@ class Compiler
     protected $charsetSeen;
     protected $sourceNames;
 
-    private $indentLevel;
-    private $commentsSeen;
-    private $extends;
-    private $extendsMap;
-    private $parsedFiles;
-    private $parser;
-    private $sourceIndex;
-    private $sourceLine;
-    private $sourceColumn;
-    private $stderr;
-    private $shouldEvaluate;
-    private $ignoreErrors;
+    protected $cache;
+
+    protected $indentLevel;
+    protected $extends;
+    protected $extendsMap;
+    protected $parsedFiles;
+    protected $parser;
+    protected $sourceIndex;
+    protected $sourceLine;
+    protected $sourceColumn;
+    protected $stderr;
+    protected $shouldEvaluate;
+    protected $ignoreErrors;
+
+    protected $callStack = [];
 
     /**
      * Constructor
      */
-    public function __construct()
+    public function __construct($cacheOptions = null)
     {
         $this->parsedFiles = [];
         $this->sourceNames = [];
+
+        if ($cacheOptions) {
+            $this->cache = new Cache($cacheOptions);
+        }
+    }
+
+    public function getCompileOptions()
+    {
+        $options = [
+            'importPaths'        => $this->importPaths,
+            'registeredVars'     => $this->registeredVars,
+            'registeredFeatures' => $this->registeredFeatures,
+            'encoding'           => $this->encoding,
+            'sourceMap'          => serialize($this->sourceMap),
+            'sourceMapOptions'   => $this->sourceMapOptions,
+            'formatter'          => $this->formatter,
+        ];
+
+        return $options;
     }
 
     /**
@@ -179,8 +202,33 @@ class Compiler
      */
     public function compile($code, $path = null)
     {
+        if ($this->cache) {
+            $cacheKey = ($path ? $path : "(stdin)") . ":" . md5($code);
+            $compileOptions = $this->getCompileOptions();
+            $cache = $this->cache->getCache("compile", $cacheKey, $compileOptions);
+
+            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
+                    ) {
+                        unset($cache);
+                        break;
+                    }
+                }
+
+                if (isset($cache)) {
+                    return $cache['out'];
+                }
+            }
+        }
+
+
         $this->indentLevel    = -1;
-        $this->commentsSeen   = [];
         $this->extends        = [];
         $this->extendsMap     = [];
         $this->sourceIndex    = null;
@@ -235,6 +283,15 @@ class Compiler
             $out .= sprintf('/*# sourceMappingURL=%s */', $sourceMapUrl);
         }
 
+        if ($this->cache && isset($cacheKey) && isset($compileOptions)) {
+            $v = [
+                'dependencies' => $this->getParsedFiles(),
+                'out' => &$out,
+            ];
+
+            $this->cache->setCache("compile", $cacheKey, $v, $compileOptions);
+        }
+
         return $out;
     }
 
@@ -247,7 +304,7 @@ class Compiler
      */
     protected function parserFactory($path)
     {
-        $parser = new Parser($path, count($this->sourceNames), $this->encoding);
+        $parser = new Parser($path, count($this->sourceNames), $this->encoding, $this->cache);
 
         $this->sourceNames[] = $path;
         $this->addParsedFile($path);
@@ -316,9 +373,16 @@ class Compiler
         $out->parent       = $this->scope;
         $out->selectors    = $selectors;
         $out->depth        = $this->env->depth;
-        $out->sourceName   = $this->env->block->sourceName;
-        $out->sourceLine   = $this->env->block->sourceLine;
-        $out->sourceColumn = $this->env->block->sourceColumn;
+
+        if ($this->env->block instanceof Block) {
+            $out->sourceName   = $this->env->block->sourceName;
+            $out->sourceLine   = $this->env->block->sourceLine;
+            $out->sourceColumn = $this->env->block->sourceColumn;
+        } else {
+            $out->sourceName   = null;
+            $out->sourceLine   = null;
+            $out->sourceColumn = null;
+        }
 
         return $out;
     }
@@ -421,6 +485,37 @@ class Compiler
         }
     }
 
+    /**
+     * Glue parts of :not( or :nth-child( ... that are in general splitted in selectors parts
+     *
+     * @param array $parts
+     *
+     * @return array
+     */
+    protected function glueFunctionSelectors($parts)
+    {
+        $new = [];
+
+        foreach ($parts as $part) {
+            if (is_array($part)) {
+                $part = $this->glueFunctionSelectors($part);
+                $new[] = $part;
+            } else {
+                // a selector part finishing with a ) is the last part of a :not( or :nth-child(
+                // and need to be joined to this
+                if (count($new) && is_string($new[count($new) - 1])
+                    && strlen($part) && substr($part, -1) === ')' && strpos($part, '(') === false
+                ) {
+                    $new[count($new) - 1] .= $part;
+                } else {
+                    $new[] = $part;
+                }
+            }
+        }
+
+        return $new;
+    }
+
     /**
      * Match extends
      *
@@ -431,14 +526,29 @@ class Compiler
      */
     protected function matchExtends($selector, &$out, $from = 0, $initial = true)
     {
+        static $partsPile = [];
+
+        $selector = $this->glueFunctionSelectors($selector);
+
         foreach ($selector as $i => $part) {
             if ($i < $from) {
                 continue;
             }
 
+            // check that we are not building an infinite loop of extensions
+            // if the new part is just including a previous part don't try to extend anymore
+            if (count($part) > 1) {
+                foreach ($partsPile as $previousPart) {
+                    if (! count(array_diff($previousPart, $part))) {
+                        continue 2;
+                    }
+                }
+            }
+
             if ($this->matchExtendsSingle($part, $origin)) {
-                $after = array_slice($selector, $i + 1);
-                $before = array_slice($selector, 0, $i);
+                $partsPile[] = $part;
+                $after       = array_slice($selector, $i + 1);
+                $before      = array_slice($selector, 0, $i);
 
                 list($before, $nonBreakableBefore) = $this->extractRelationshipFromFragment($before);
 
@@ -446,7 +556,7 @@ class Compiler
                     $k = 0;
 
                     // remove shared parts
-                    if ($initial) {
+                    if (count($new) > 1) {
                         while ($k < $i && isset($new[$k]) && $selector[$k] === $new[$k]) {
                             $k++;
                         }
@@ -456,7 +566,14 @@ class Compiler
                     $tempReplacement = $k > 0 ? array_slice($new, $k) : $new;
 
                     for ($l = count($tempReplacement) - 1; $l >= 0; $l--) {
-                        $slice = $tempReplacement[$l];
+                        $slice = [];
+
+                        foreach ($tempReplacement[$l] as $chunk) {
+                            if (! in_array($chunk, $slice)) {
+                                $slice[] = $chunk;
+                            }
+                        }
+
                         array_unshift($replacement, $slice);
 
                         if (! $this->isImmediateRelationshipCombinator(end($slice))) {
@@ -483,18 +600,19 @@ class Compiler
                     $out[] = $result;
 
                     // recursively check for more matches
-                    $this->matchExtends($result, $out, count($before) + count($mergedBefore), false);
+                    $startRecurseFrom = count($before) + min(count($nonBreakableBefore), count($mergedBefore));
+                    $this->matchExtends($result, $out, $startRecurseFrom, false);
 
                     // selector sequence merging
                     if (! empty($before) && count($new) > 1) {
-                        $sharedParts = $k > 0 ? array_slice($before, 0, $k) : [];
+                        $preSharedParts = $k > 0 ? array_slice($before, 0, $k) : [];
                         $postSharedParts = $k > 0 ? array_slice($before, $k) : $before;
 
-                        list($injectBetweenSharedParts, $nonBreakable2) = $this->extractRelationshipFromFragment($afterBefore);
+                        list($betweenSharedParts, $nonBreakable2) = $this->extractRelationshipFromFragment($afterBefore);
 
                         $result2 = array_merge(
-                            $sharedParts,
-                            $injectBetweenSharedParts,
+                            $preSharedParts,
+                            $betweenSharedParts,
                             $postSharedParts,
                             $nonBreakable2,
                             $nonBreakableBefore,
@@ -505,6 +623,8 @@ class Compiler
                         $out[] = $result2;
                     }
                 }
+
+                array_pop($partsPile);
             }
         }
     }
@@ -522,6 +642,11 @@ class Compiler
         $counts = [];
         $single = [];
 
+        // simple usual cases, no need to do the whole trick
+        if (in_array($rawSingle, [['>'],['+'],['~']])) {
+            return false;
+        }
+
         foreach ($rawSingle as $part) {
             // matches Number
             if (! is_string($part)) {
@@ -556,6 +681,8 @@ class Compiler
         foreach ($counts as $idx => $count) {
             list($target, $origin, /* $block */) = $this->extends[$idx];
 
+            $origin = $this->glueFunctionSelectors($origin);
+
             // check count
             if ($count !== count($target)) {
                 continue;
@@ -596,7 +723,6 @@ class Compiler
         return $found;
     }
 
-
     /**
      * Extract a relationship from the fragment.
      *
@@ -606,6 +732,7 @@ class Compiler
      * the rest.
      *
      * @param array $fragment The selector fragment maybe ending with a direction relationship combinator.
+     *
      * @return array The selector without the relationship fragment if any, the relationship fragment.
      */
     protected function extractRelationshipFromFragment(array $fragment)
@@ -675,13 +802,18 @@ class Compiler
     {
         $this->pushEnv($media);
 
-        $mediaQuery = $this->compileMediaQuery($this->multiplyMedia($this->env));
-
-        if (! empty($mediaQuery)) {
-            $this->scope = $this->makeOutputBlock(Type::T_MEDIA, [$mediaQuery]);
+        $mediaQueries = $this->compileMediaQuery($this->multiplyMedia($this->env));
 
+        if (! empty($mediaQueries) && $mediaQueries) {
+            $previousScope = $this->scope;
             $parentScope = $this->mediaParent($this->scope);
-            $parentScope->children[] = $this->scope;
+
+            foreach ($mediaQueries as $mediaQuery) {
+                $this->scope = $this->makeOutputBlock(Type::T_MEDIA, [$mediaQuery]);
+
+                $parentScope->children[] = $this->scope;
+                $parentScope = $this->scope;
+            }
 
             // top level properties in a media cause it to be wrapped
             $needsWrap = false;
@@ -701,21 +833,44 @@ 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;
+
+                    $file = $this->sourceNames[$media->sourceIndex];
+                    $line = $media->sourceLine;
+
+                    switch ($this->lineNumberStyle) {
+                        case static::LINE_COMMENTS:
+                            $annotation->lines[] = '/* line ' . $line
+                                                 . ($file ? ', ' . $file : '')
+                                                 . ' */';
+                            break;
+
+                        case static::DEBUG_INFO:
+                            $annotation->lines[] = '@media -sass-debug-info{'
+                                                 . ($file ? 'filename{font-family:"' . $file . '"}' : '')
+                                                 . 'line{font-family:' . $line . '}}';
+                            break;
+                    }
+
+                    $this->scope->children[] = $annotation;
+                }
             }
 
             $this->compileChildrenNoReturn($media->children, $this->scope);
 
-            $this->scope = $this->scope->parent;
+            $this->scope = $previousScope;
         }
 
         $this->popEnv();
@@ -783,18 +938,29 @@ class Compiler
             $wrapped->comments     = [];
             $wrapped->parent       = $block;
             $wrapped->children     = $block->children;
+            $wrapped->selfParent   = $block->selfParent;
 
             $block->children = [[Type::T_BLOCK, $wrapped]];
+            $block->selector = null;
+        }
+
+        $selfParent = $block->selfParent;
+
+        if (! $block->selfParent->selectors && isset($block->parent) && $block->parent &&
+            isset($block->parent->selectors) && $block->parent->selectors
+        ) {
+            $selfParent = $block->parent;
         }
 
         $this->env = $this->filterWithout($envs, $without);
-        $newBlock  = $this->spliceTree($envs, $block, $without);
 
         $saveScope   = $this->scope;
-        $this->scope = $this->rootBlock;
+        $this->scope = $this->filterScopeWithout($saveScope, $without);
 
-        $this->compileChild($newBlock, $this->scope);
+        // propagate selfParent to the children where they still can be useful
+        $this->compileChildrenNoReturn($block->children, $this->scope, $selfParent);
 
+        $this->scope = $this->completeScope($this->scope, $saveScope);
         $this->scope = $saveScope;
         $this->env   = $this->extractEnv($envs);
 
@@ -802,82 +968,119 @@ class Compiler
     }
 
     /**
-     * Splice parse tree
+     * Filter at-root scope depending of with/without option
      *
-     * @param array                $envs
-     * @param \Leafo\ScssPhp\Block $block
-     * @param integer              $without
+     * @param \Leafo\ScssPhp\Formatter\OutputBlock $scope
+     * @param mixed                                $without
      *
-     * @return array
+     * @return mixed
      */
-    private function spliceTree($envs, Block $block, $without)
+    protected function filterScopeWithout($scope, $without)
     {
-        $newBlock = null;
+        $filteredScopes = [];
 
-        foreach ($envs as $e) {
-            if (! isset($e->block)) {
-                continue;
-            }
+        if ($scope->type === TYPE::T_ROOT) {
+            return $scope;
+        }
 
-            if ($e->block === $block) {
-                continue;
-            }
+        // start from the root
+        while ($scope->parent && $scope->parent->type !== TYPE::T_ROOT) {
+            $scope = $scope->parent;
+        }
 
-            if (isset($e->block->type) && $e->block->type === Type::T_AT_ROOT) {
-                continue;
+        for (;;) {
+            if (! $scope) {
+                break;
             }
 
-            if ($e->block && $this->isWithout($without, $e->block)) {
-                continue;
-            }
+            if (! $this->isWithout($without, $scope)) {
+                $s = clone $scope;
+                $s->children = [];
+                $s->lines = [];
+                $s->parent = null;
 
-            $b = new Block;
-            $b->sourceName   = $e->block->sourceName;
-            $b->sourceIndex  = $e->block->sourceIndex;
-            $b->sourceLine   = $e->block->sourceLine;
-            $b->sourceColumn = $e->block->sourceColumn;
-            $b->selectors    = [];
-            $b->comments     = $e->block->comments;
-            $b->parent       = null;
-
-            if ($newBlock) {
-                $type = isset($newBlock->type) ? $newBlock->type : Type::T_BLOCK;
-
-                $b->children = [[$type, $newBlock]];
-
-                $newBlock->parent = $b;
-            } elseif (count($block->children)) {
-                foreach ($block->children as $child) {
-                    if ($child[0] === Type::T_BLOCK) {
-                        $child[1]->parent = $b;
-                    }
+                if ($s->type !== Type::T_MEDIA && $s->type !== Type::T_DIRECTIVE) {
+                    $s->selectors = [];
                 }
 
-                $b->children = $block->children;
+                $filteredScopes[] = $s;
             }
 
-            if (isset($e->block->type)) {
-                $b->type = $e->block->type;
+            if ($scope->children) {
+                $scope = end($scope->children);
+            } else {
+                $scope = null;
             }
+        }
 
-            if (isset($e->block->name)) {
-                $b->name = $e->block->name;
-            }
+        if (! count($filteredScopes)) {
+            return $this->rootBlock;
+        }
 
-            if (isset($e->block->queryList)) {
-                $b->queryList = $e->block->queryList;
-            }
+        $newScope = array_shift($filteredScopes);
+        $newScope->parent = $this->rootBlock;
+
+        $this->rootBlock->children[] = $newScope;
+
+        $p = &$newScope;
 
-            if (isset($e->block->value)) {
-                $b->value = $e->block->value;
+        while (count($filteredScopes)) {
+            $s = array_shift($filteredScopes);
+            $s->parent = $p;
+            $p->children[] = &$s;
+            $p = $s;
+        }
+
+        return $newScope;
+    }
+
+    /**
+     * found missing selector from a at-root compilation in the previous scope
+     * (if at-root is just enclosing a property, the selector is in the parent tree)
+     *
+     * @param \Leafo\ScssPhp\Formatter\OutputBlock $scope
+     * @param \Leafo\ScssPhp\Formatter\OutputBlock $previousScope
+     *
+     * @return mixed
+     */
+    protected function completeScope($scope, $previousScope)
+    {
+        if (! $scope->type && (! $scope->selectors || ! count($scope->selectors)) && count($scope->lines)) {
+            $scope->selectors = $this->findScopeSelectors($previousScope, $scope->depth);
+        }
+
+        if ($scope->children) {
+            foreach ($scope->children as $k => $c) {
+                $scope->children[$k] = $this->completeScope($c, $previousScope);
             }
+        }
+
+        return $scope;
+    }
 
-            $newBlock = $b;
+    /**
+     * Find a selector by the depth node in the scope
+     *
+     * @param \Leafo\ScssPhp\Formatter\OutputBlock $scope
+     * @param integer                              $depth
+     *
+     * @return array
+     */
+    protected function findScopeSelectors($scope, $depth)
+    {
+        if ($scope->depth === $depth && $scope->selectors) {
+            return $scope->selectors;
         }
 
-        $type = isset($newBlock->type) ? $newBlock->type : Type::T_BLOCK;
+        if ($scope->children) {
+            foreach (array_reverse($scope->children) as $c) {
+                if ($s = $this->findScopeSelectors($c, $depth)) {
+                    return $s;
+                }
+            }
+        }
 
-        return [$type, $newBlock];
+        return [];
     }
 
     /**
@@ -887,7 +1090,7 @@ class Compiler
      *
      * @return integer
      */
-    private function compileWith($with)
+    protected function compileWith($with)
     {
         static $mapping = [
             'rule'     => self::WITH_RULE,
@@ -938,7 +1141,7 @@ class Compiler
      *
      * @return \Leafo\ScssPhp\Compiler\Environment
      */
-    private function filterWithout($envs, $without)
+    protected function filterWithout($envs, $without)
     {
         $filtered = [];
 
@@ -956,20 +1159,30 @@ class Compiler
     /**
      * Filter WITH rules
      *
-     * @param integer              $without
-     * @param \Leafo\ScssPhp\Block $block
+     * @param integer                                                   $without
+     * @param \Leafo\ScssPhp\Block|\Leafo\ScssPhp\Formatter\OutputBlock $block
      *
      * @return boolean
      */
-    private function isWithout($without, Block $block)
+    protected function isWithout($without, $block)
     {
-        if ((($without & static::WITH_RULE) && isset($block->selectors)) ||
-            (($without & static::WITH_MEDIA) &&
-                isset($block->type) && $block->type === Type::T_MEDIA) ||
-            (($without & static::WITH_SUPPORTS) &&
-                isset($block->type) && $block->type === Type::T_DIRECTIVE &&
-                isset($block->name) && $block->name === 'supports')
-        ) {
+        if (isset($block->type)) {
+            if ($block->type === Type::T_MEDIA) {
+                return ($without & static::WITH_MEDIA) ? true : false;
+            }
+
+            if ($block->type === Type::T_DIRECTIVE) {
+                if (isset($block->name) && $block->name === 'supports') {
+                    return ($without & static::WITH_SUPPORTS) ? true : false;
+                }
+
+                if (isset($block->selectors) && strpos(serialize($block->selectors), '@supports') !== false) {
+                    return ($without & static::WITH_SUPPORTS) ? true : false;
+                }
+            }
+        }
+
+        if ((($without & static::WITH_RULE) && isset($block->selectors))) {
             return true;
         }
 
@@ -1017,6 +1230,35 @@ class Compiler
         $this->scope = $this->makeOutputBlock($block->type, $selectors);
         $this->scope->parent->children[] = $this->scope;
 
+        // wrap assign children in a block
+        // except for @font-face
+        if ($block->type !== Type::T_DIRECTIVE || $block->name !== "font-face") {
+            // need wrapping?
+            $needWrapping = false;
+
+            foreach ($block->children as $child) {
+                if ($child[0] === Type::T_ASSIGN) {
+                    $needWrapping = true;
+                    break;
+                }
+            }
+
+            if ($needWrapping) {
+                $wrapped = new Block;
+                $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;
+
+                $block->children = [[Type::T_BLOCK, $wrapped]];
+            }
+        }
+
         $this->compileChildrenNoReturn($block->children, $this->scope);
 
         $this->scope = $this->scope->parent;
@@ -1076,9 +1318,22 @@ class Compiler
         $this->scope->children[] = $out;
 
         if (count($block->children)) {
-            $out->selectors = $this->multiplySelectors($env);
+            $out->selectors = $this->multiplySelectors($env, $block->selfParent);
+
+            // propagate selfParent to the children where they still can be useful
+            $selfParentSelectors = null;
+
+            if (isset($block->selfParent->selectors)) {
+                $selfParentSelectors = $block->selfParent->selectors;
+                $block->selfParent->selectors = $out->selectors;
+            }
+
+            $this->compileChildrenNoReturn($block->children, $out, $block->selfParent);
 
-            $this->compileChildrenNoReturn($block->children, $out);
+            // and revert for the following childs of the same block
+            if ($selfParentSelectors) {
+                $block->selfParent->selectors = $selfParentSelectors;
+            }
         }
 
         $this->formatter->stripSemicolon($out->lines);
@@ -1095,6 +1350,7 @@ class Compiler
     {
         $out = $this->makeOutputBlock(Type::T_COMMENT);
         $out->lines[] = $block[1];
+
         $this->scope->children[] = $out;
     }
 
@@ -1113,6 +1369,7 @@ 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__);
 
@@ -1167,28 +1424,87 @@ class Compiler
     /**
      * Collapse selectors
      *
-     * @param array $selectors
+     * @param array   $selectors
+     * @param boolean $selectorFormat
+     *   if false return a collapsed string
+     *   if true return an array description of a structured selector
      *
      * @return string
      */
-    protected function collapseSelectors($selectors)
+    protected function collapseSelectors($selectors, $selectorFormat = false)
     {
         $parts = [];
 
         foreach ($selectors as $selector) {
-            $output = '';
+            $output = [];
+            $glueNext = false;
+
+            foreach ($selector as $node) {
+                $compound = '';
+
+                array_walk_recursive(
+                    $node,
+                    function ($value, $key) use (&$compound) {
+                        $compound .= $value;
+                    }
+                );
 
-            array_walk_recursive(
-                $selector,
-                function ($value, $key) use (&$output) {
-                    $output .= $value;
+                if ($selectorFormat && $this->isImmediateRelationshipCombinator($compound)) {
+                    if (count($output)) {
+                        $output[count($output) - 1] .= ' ' . $compound;
+                    } else {
+                        $output[] = $compound;
+                    }
+                    $glueNext = true;
+                } elseif ($glueNext) {
+                    $output[count($output) - 1] .= ' ' . $compound;
+                    $glueNext = false;
+                } else {
+                    $output[] = $compound;
                 }
-            );
+            }
+
+            if ($selectorFormat) {
+                foreach ($output as &$o) {
+                    $o = [Type::T_STRING, '', [$o]];
+                }
+                $output = [Type::T_LIST, ' ', $output];
+            } else {
+                $output = implode(' ', $output);
+            }
 
             $parts[] = $output;
         }
 
-        return implode(', ', $parts);
+        if ($selectorFormat) {
+            $parts = [Type::T_LIST, ',', $parts];
+        } else {
+            $parts = implode(', ', $parts);
+        }
+
+        return $parts;
+    }
+
+    /**
+     * Parse down the selector and revert [self] to "&" before a reparsing
+     *
+     * @param array $selectors
+     *
+     * @return array
+     */
+    protected function revertSelfSelector($selectors)
+    {
+        foreach ($selectors as &$part) {
+            if (is_array($part)) {
+                if ($part === [Type::T_SELF]) {
+                    $part = '&';
+                } else {
+                    $part = $this->revertSelfSelector($part);
+                }
+            }
+        }
+
+        return $selectors;
     }
 
     /**
@@ -1295,16 +1611,42 @@ class Compiler
         return false;
     }
 
+    protected function pushCallStack($name = '')
+    {
+        $this->callStack[] = [
+          'n' => $name,
+          Parser::SOURCE_INDEX => $this->sourceIndex,
+          Parser::SOURCE_LINE => $this->sourceLine,
+          Parser::SOURCE_COLUMN => $this->sourceColumn
+        ];
+
+        // infinite calling loop
+        if (count($this->callStack) > 25000) {
+            // not displayed but you can var_dump it to deep debug
+            $msg = $this->callStackMessage(true, 100);
+            $msg = "Infinite calling loop";
+            $this->throwError($msg);
+        }
+    }
+
+    protected function popCallStack()
+    {
+        array_pop($this->callStack);
+    }
+
     /**
      * Compile children and return result
      *
      * @param array                                $stms
      * @param \Leafo\ScssPhp\Formatter\OutputBlock $out
+     * @param string                               $traceName
      *
-     * @return array
+     * @return array|null
      */
-    protected function compileChildren($stms, OutputBlock $out)
+    protected function compileChildren($stms, OutputBlock $out, $traceName = '')
     {
+        $this->pushCallStack($traceName);
+
         foreach ($stms as $stm) {
             $ret = $this->compileChild($stm, $out);
 
@@ -1312,6 +1654,10 @@ class Compiler
                 return $ret;
             }
         }
+
+        $this->popCallStack();
+
+        return null;
     }
 
     /**
@@ -1319,13 +1665,27 @@ class Compiler
      *
      * @param array                                $stms
      * @param \Leafo\ScssPhp\Formatter\OutputBlock $out
+     * @param \Leafo\ScssPhp\Block                 $selfParent
+     * @param string                               $traceName
      *
      * @throws \Exception
      */
-    protected function compileChildrenNoReturn($stms, OutputBlock $out)
+    protected function compileChildrenNoReturn($stms, OutputBlock $out, $selfParent = null, $traceName = '')
     {
+        $this->pushCallStack($traceName);
+
         foreach ($stms as $stm) {
-            $ret = $this->compileChild($stm, $out);
+            if ($selfParent && isset($stm[1]) && is_object($stm[1]) && $stm[1] instanceof Block) {
+                $stm[1]->selfParent = $selfParent;
+                $ret = $this->compileChild($stm, $out);
+                $stm[1]->selfParent = null;
+            } elseif ($selfParent && $stm[0] === TYPE::T_INCLUDE) {
+                $stm['selfParent'] = $selfParent;
+                $ret = $this->compileChild($stm, $out);
+                unset($stm['selfParent']);
+            } else {
+                $ret = $this->compileChild($stm, $out);
+            }
 
             if (isset($ret)) {
                 $this->throwError('@return may only be used within a function');
@@ -1333,6 +1693,66 @@ class Compiler
                 return;
             }
         }
+
+        $this->popCallStack();
+    }
+
+
+    /**
+     * evaluate media query : compile internal value keeping the structure inchanged
+     *
+     * @param array $queryList
+     *
+     * @return array
+     */
+    protected function evaluateMediaQuery($queryList)
+    {
+        foreach ($queryList as $kql => $query) {
+            foreach ($query as $kq => $q) {
+                for ($i = 1; $i < count($q); $i++) {
+                    $value = $this->compileValue($q[$i]);
+
+                    // the parser had no mean to know if media type or expression if it was an interpolation
+                    if ($q[0] == Type::T_MEDIA_TYPE &&
+                        (strpos($value, '(') !== false ||
+                        strpos($value, ')') !== false ||
+                        strpos($value, ':') !== false)
+                    ) {
+                        $queryList[$kql][$kq][0] = Type::T_MEDIA_EXPRESSION;
+
+                        if (strpos($value, 'and') !== false) {
+                            $values = explode('and', $value);
+                            $value = trim(array_pop($values));
+
+                            while ($v = trim(array_pop($values))) {
+                                $type = Type::T_MEDIA_EXPRESSION;
+
+                                if (strpos($v, '(') === false &&
+                                    strpos($v, ')') === false &&
+                                    strpos($v, ':') === false
+                                ) {
+                                    $type = Type::T_MEDIA_TYPE;
+                                }
+
+                                if (substr($v, 0, 1) === '(' && substr($v, -1) === ')') {
+                                    $v = substr($v, 1, -1);
+                                }
+
+                                $queryList[$kql][] = [$type,[Type::T_KEYWORD, $v]];
+                            }
+                        }
+
+                        if (substr($value, 0, 1) === '(' && substr($value, -1) === ')') {
+                            $value = substr($value, 1, -1);
+                        }
+                    }
+
+                    $queryList[$kql][$kq][$i] = [Type::T_KEYWORD, $value];
+                }
+            }
+        }
+
+        return $queryList;
     }
 
     /**
@@ -1340,31 +1760,75 @@ class Compiler
      *
      * @param array $queryList
      *
-     * @return string
+     * @return array
      */
     protected function compileMediaQuery($queryList)
     {
-        $out = '@media';
-        $first = true;
+        $start = '@media ';
+        $default = trim($start);
+        $out = [];
+        $current = "";
 
         foreach ($queryList as $query) {
             $type = null;
             $parts = [];
 
+            $mediaTypeOnly = true;
+
+            foreach ($query as $q) {
+                if ($q[0] !== Type::T_MEDIA_TYPE) {
+                    $mediaTypeOnly = false;
+                    break;
+                }
+            }
+
             foreach ($query as $q) {
                 switch ($q[0]) {
                     case Type::T_MEDIA_TYPE:
-                        if ($type) {
-                            $type = $this->mergeMediaTypes(
-                                $type,
-                                array_map([$this, 'compileValue'], array_slice($q, 1))
-                            );
-
-                            if (empty($type)) { // merge failed
-                                return null;
+                        $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) )) {
+                                if ($type) {
+                                    array_unshift($parts, implode(' ', array_filter($type)));
+                                }
+
+                                if (! empty($parts)) {
+                                    if (strlen($current)) {
+                                        $current .= $this->formatter->tagSeparator;
+                                    }
+
+                                    $current .= implode(' and ', $parts);
+                                }
+
+                                if ($current) {
+                                    $out[] = $start . $current;
+                                }
+
+                                $current = "";
+                                $type = null;
+                                $parts = [];
+                            }
+                        }
+
+                        if ($newType === ['all'] && $default) {
+                            $default = $start . 'all';
+                        }
+
+                        // all can be safely ignored and mixed with whatever else
+                        if ($newType !== ['all']) {
+                            if ($type) {
+                                $type = $this->mergeMediaTypes($type, $newType);
+
+                                if (empty($type)) {
+                                    // merge failed : ignore this query that is not valid, skip to the next one
+                                    $parts = [];
+                                    $default = ''; // if everything fail, no @media at all
+                                    continue 3;
+                                }
+                            } else {
+                                $type = $newType;
                             }
-                        } else {
-                            $type = array_map([$this, 'compileValue'], array_slice($q, 1));
                         }
                         break;
 
@@ -1393,20 +1857,34 @@ class Compiler
             }
 
             if (! empty($parts)) {
-                if ($first) {
-                    $first = false;
-                    $out .= ' ';
-                } else {
-                    $out .= $this->formatter->tagSeparator;
+                if (strlen($current)) {
+                    $current .= $this->formatter->tagSeparator;
                 }
 
-                $out .= implode(' and ', $parts);
+                $current .= implode(' and ', $parts);
             }
         }
 
+        if ($current) {
+            $out[] = $start . $current;
+        }
+
+        // no @media type except all, and no conflict?
+        if (! $out && $default) {
+            $out[] = $default;
+        }
+
         return $out;
     }
 
+    /**
+     * Merge direct relationships between selectors
+     *
+     * @param array $selectors1
+     * @param array $selectors2
+     *
+     * @return array
+     */
     protected function mergeDirectRelationships($selectors1, $selectors2)
     {
         if (empty($selectors1) || empty($selectors2)) {
@@ -1416,7 +1894,7 @@ class Compiler
         $part1 = end($selectors1);
         $part2 = end($selectors2);
 
-        if (! $this->isImmediateRelationshipCombinator($part1[0]) || $part1 !== $part2) {
+        if (! $this->isImmediateRelationshipCombinator($part1[0]) && $part1 !== $part2) {
             return array_merge($selectors1, $selectors2);
         }
 
@@ -1426,13 +1904,18 @@ class Compiler
             $part1 = array_pop($selectors1);
             $part2 = array_pop($selectors2);
 
-            if ($this->isImmediateRelationshipCombinator($part1[0]) && $part1 !== $part2) {
-                $merged = array_merge($selectors1, [$part1], $selectors2, [$part2], $merged);
+            if (! $this->isImmediateRelationshipCombinator($part1[0]) && $part1 !== $part2) {
+                if ($this->isImmediateRelationshipCombinator(reset($merged)[0])) {
+                    array_unshift($merged, [$part1[0] . $part2[0]]);
+                    $merged = array_merge($selectors1, $selectors2, $merged);
+                } else {
+                    $merged = array_merge($selectors1, [$part1], $selectors2, [$part2], $merged);
+                }
+
                 break;
             }
 
             array_unshift($merged, $part1);
-            array_unshift($merged, [array_pop($selectors1)[0] . array_pop($selectors2)[0]]);
         } while (! empty($selectors1) && ! empty($selectors2));
 
         return $merged;
@@ -1507,13 +1990,13 @@ class Compiler
     /**
      * Compile import; returns true if the value was something that could be imported
      *
-     * @param array   $rawPath
-     * @param array   $out
-     * @param boolean $once
+     * @param array                                $rawPath
+     * @param \Leafo\ScssPhp\Formatter\OutputBlock $out
+     * @param boolean                              $once
      *
      * @return boolean
      */
-    protected function compileImport($rawPath, $out, $once = false)
+    protected function compileImport($rawPath, OutputBlock $out, $once = false)
     {
         if ($rawPath[0] === Type::T_STRING) {
             $path = $this->compileStringContent($rawPath);
@@ -1562,15 +2045,26 @@ class Compiler
      */
     protected function compileChild($child, OutputBlock $out)
     {
-        $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;
+        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->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->sourceColumn = $child[1]->sourceColumn;
+        } elseif (! empty($out->sourceLine) && ! empty($out->sourceName)) {
+            $this->sourceLine = $out->sourceLine;
+            $this->sourceIndex = array_search($out->sourceName, $this->sourceNames);
+
+            if ($this->sourceIndex === false) {
+                $this->sourceIndex = null;
+            }
+        }
 
         switch ($child[0]) {
             case Type::T_SCSSPHP_IMPORT_ONCE:
-                list(, $rawPath) = $child;
-
-                $rawPath = $this->reduce($rawPath);
+                $rawPath = $this->reduce($child[1]);
 
                 if (! $this->compileImport($rawPath, $out, true)) {
                     $out->lines[] = '@import ' . $this->compileValue($rawPath) . ';';
@@ -1578,9 +2072,7 @@ class Compiler
                 break;
 
             case Type::T_IMPORT:
-                list(, $rawPath) = $child;
-
-                $rawPath = $this->reduce($rawPath);
+                $rawPath = $this->reduce($child[1]);
 
                 if (! $this->compileImport($rawPath, $out)) {
                     $out->lines[] = '@import ' . $this->compileValue($rawPath) . ';';
@@ -1620,7 +2112,7 @@ class Compiler
                     $isGlobal = in_array('!global', $flags);
 
                     if ($isGlobal) {
-                        $this->set($name[1], $this->reduce($value), false, $this->rootEnv);
+                        $this->set($name[1], $this->reduce($value), false, $this->rootEnv, $value);
                         break;
                     }
 
@@ -1629,7 +2121,7 @@ class Compiler
                         || $result === static::$null);
 
                     if (! $isDefault || $shouldSet) {
-                        $this->set($name[1], $this->reduce($value));
+                        $this->set($name[1], $this->reduce($value), true, null, $value);
                     }
                     break;
                 }
@@ -1637,11 +2129,25 @@ class Compiler
                 $compiledName = $this->compileValue($name);
 
                 // handle shorthand syntax: size / line-height
-                if ($compiledName === 'font') {
-                    if ($value[0] === Type::T_EXPRESSION && $value[1] === '/') {
-                        $value = $this->expToString($value);
-                    } elseif ($value[0] === Type::T_LIST) {
-                        foreach ($value[2] as &$item) {
+                if ($compiledName === 'font' || $compiledName === 'grid-row' || $compiledName === 'grid-column') {
+                    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;
+
+                    if ($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];
+                    }
+
+                    if ($fontValue[0] === Type::T_EXPRESSION && $fontValue[1] === '/') {
+                        $fontValue = $this->expToString($fontValue);
+                    } elseif ($fontValue[0] === Type::T_LIST) {
+                        foreach ($fontValue[2] as &$item) {
                             if ($item[0] === Type::T_EXPRESSION && $item[1] === '/') {
                                 $item = $this->expToString($item);
                             }
@@ -1684,9 +2190,7 @@ class Compiler
                 break;
 
             case Type::T_EXTEND:
-                list(, $selectors) = $child;
-
-                foreach ($selectors as $sel) {
+                foreach ($child[1] as $sel) {
                     $results = $this->evalSelectors([$sel]);
 
                     foreach ($results as $result) {
@@ -1862,10 +2366,34 @@ class Compiler
                 $storeEnv = $this->storeEnv;
                 $this->storeEnv = $this->env;
 
+                // Find the parent selectors in the env to be able to know what '&' refers to in the mixin
+                // and assign this fake parent to childs
+                $selfParent = null;
+
+                if (isset($child['selfParent']) && isset($child['selfParent']->selectors)) {
+                    $selfParent = $child['selfParent'];
+                } else {
+                    $parentSelectors = $this->multiplySelectors($this->env);
+
+                    if ($parentSelectors) {
+                        $parent = new Block();
+                        $parent->selectors = $parentSelectors;
+
+                        foreach ($mixin->children as $k => $child) {
+                            if (isset($child[1]) && is_object($child[1]) && $child[1] instanceof Block) {
+                                $mixin->children[$k][1]->parent = $parent;
+                            }
+                        }
+                    }
+                }
+
+                // clone the stored content to not have its scope spoiled by a further call to the same mixin
+                // i.e., recursive @include of the same mixin
                 if (isset($content)) {
-                    $content->scope = $callingScope;
+                    $copyContent = clone $content;
+                    $copyContent->scope = $callingScope;
 
-                    $this->setRaw(static::$namespaces['special'] . 'content', $content, $this->env);
+                    $this->setRaw(static::$namespaces['special'] . 'content', $copyContent, $this->env);
                 }
 
                 if (isset($mixin->args)) {
@@ -1874,7 +2402,7 @@ class Compiler
 
                 $this->env->marker = 'mixin';
 
-                $this->compileChildrenNoReturn($mixin->children, $out);
+                $this->compileChildrenNoReturn($mixin->children, $out, $selfParent, $this->env->marker . " " . $name);
 
                 $this->storeEnv = $storeEnv;
 
@@ -1882,8 +2410,8 @@ class Compiler
                 break;
 
             case Type::T_MIXIN_CONTENT:
-                $content = $this->get(static::$namespaces['special'] . 'content', false, $this->getStoreEnv())
-                         ?: $this->get(static::$namespaces['special'] . 'content', false, $this->env);
+                $env = isset($this->storeEnv) ? $this->storeEnv : $this->env;
+                $content = $this->get(static::$namespaces['special'] . 'content', false, $env);
 
                 if (! $content) {
                     $content = new \stdClass();
@@ -1894,7 +2422,6 @@ class Compiler
 
                 $storeEnv = $this->storeEnv;
                 $this->storeEnv = $content->scope;
-
                 $this->compileChildrenNoReturn($content->children, $out);
 
                 $this->storeEnv = $storeEnv;
@@ -1903,25 +2430,28 @@ class Compiler
             case Type::T_DEBUG:
                 list(, $value) = $child;
 
+                $fname = $this->sourceNames[$this->sourceIndex];
                 $line = $this->sourceLine;
                 $value = $this->compileValue($this->reduce($value, true));
-                fwrite($this->stderr, "Line $line DEBUG: $value\n");
+                fwrite($this->stderr, "File $fname on line $line DEBUG: $value\n");
                 break;
 
             case Type::T_WARN:
                 list(, $value) = $child;
 
+                $fname = $this->sourceNames[$this->sourceIndex];
                 $line = $this->sourceLine;
                 $value = $this->compileValue($this->reduce($value, true));
-                fwrite($this->stderr, "Line $line WARN: $value\n");
+                fwrite($this->stderr, "File $fname on line $line WARN: $value\n");
                 break;
 
             case Type::T_ERROR:
                 list(, $value) = $child;
 
+                $fname = $this->sourceNames[$this->sourceIndex];
                 $line = $this->sourceLine;
                 $value = $this->compileValue($this->reduce($value, true));
-                $this->throwError("Line $line ERROR: $value\n");
+                $this->throwError("File $fname on line $line ERROR: $value\n");
                 break;
 
             case Type::T_CONTROL:
@@ -1966,7 +2496,7 @@ class Compiler
      *
      * @param array $value
      *
-     * @return array
+     * @return boolean
      */
     protected function isTruthy($value)
     {
@@ -1997,7 +2527,7 @@ class Compiler
         switch ($value[0]) {
             case Type::T_EXPRESSION:
                 if ($value[1] === '/') {
-                    return $this->shouldEval($value[2]$value[3]);
+                    return $this->shouldEval($value[2]) || $this->shouldEval($value[3]);
                 }
 
                 // fall-thru
@@ -2019,9 +2549,8 @@ class Compiler
      */
     protected function reduce($value, $inExp = false)
     {
-        list($type) = $value;
 
-        switch ($type) {
+        switch ($value[0]) {
             case Type::T_EXPRESSION:
                 list(, $op, $left, $right, $inParens) = $value;
 
@@ -2154,9 +2683,7 @@ class Compiler
                 return [Type::T_STRING, '', [$op, $exp]];
 
             case Type::T_VARIABLE:
-                list(, $name) = $value;
-
-                return $this->reduce($this->get($name));
+                return $this->reduce($this->get($value[1]));
 
             case Type::T_LIST:
                 foreach ($value[2] as &$item) {
@@ -2187,13 +2714,19 @@ class Compiler
 
             case Type::T_INTERPOLATE:
                 $value[1] = $this->reduce($value[1]);
+                if ($inExp) {
+                    return $value[1];
+                }
 
                 return $value;
 
             case Type::T_FUNCTION_CALL:
-                list(, $name, $argValues) = $value;
+                return $this->fncall($value[1], $value[2]);
 
-                return $this->fncall($name, $argValues);
+            case Type::T_SELF:
+                $selfSelector = $this->multiplySelectors($this->env);
+                $selfSelector = $this->collapseSelectors($selfSelector, true);
+                return $selfSelector;
 
             default:
                 return $value;
@@ -2208,7 +2741,7 @@ class Compiler
      *
      * @return array|null
      */
-    private function fncall($name, $argValues)
+    protected function fncall($name, $argValues)
     {
         // SCSS @function
         if ($this->callScssFunction($name, $argValues, $returnValue)) {
@@ -2254,9 +2787,8 @@ class Compiler
     public function normalizeValue($value)
     {
         $value = $this->coerceForExpression($this->reduce($value));
-        list($type) = $value;
 
-        switch ($type) {
+        switch ($value[0]) {
             case Type::T_LIST:
                 $value = $this->extractInterpolation($value);
 
@@ -2271,7 +2803,7 @@ class Compiler
                 return $value;
 
             case Type::T_STRING:
-                return [$type, '"', [$this->compileStringContent($value)]];
+                return [$value[0], '"', [$this->compileStringContent($value)]];
 
             case Type::T_NUMBER:
                 return $value->normalize();
@@ -2359,7 +2891,7 @@ class Compiler
      * @param array $left
      * @param array $right
      *
-     * @return array
+     * @return array|null
      */
     protected function opAdd($left, $right)
     {
@@ -2382,6 +2914,8 @@ class Compiler
 
             return $strRight;
         }
+
+        return null;
     }
 
     /**
@@ -2391,15 +2925,15 @@ class Compiler
      * @param array   $right
      * @param boolean $shouldEval
      *
-     * @return array
+     * @return array|null
      */
     protected function opAnd($left, $right, $shouldEval)
     {
         if (! $shouldEval) {
-            return;
+            return null;
         }
 
-        if ($left !== static::$false and $left !== static::$null) {
+        if ($left !== static::$false && $left !== static::$null) {
             return $this->reduce($right, true);
         }
 
@@ -2413,15 +2947,15 @@ class Compiler
      * @param array   $right
      * @param boolean $shouldEval
      *
-     * @return array
+     * @return array|null
      */
     protected function opOr($left, $right, $shouldEval)
     {
         if (! $shouldEval) {
-            return;
+            return null;
         }
 
-        if ($left !== static::$false and $left !== static::$null) {
+        if ($left !== static::$false && $left !== static::$null) {
             return $left;
         }
 
@@ -2676,9 +3210,7 @@ class Compiler
     {
         $value = $this->reduce($value);
 
-        list($type) = $value;
-
-        switch ($type) {
+        switch ($value[0]) {
             case Type::T_KEYWORD:
                 return $value[1];
 
@@ -2694,7 +3226,9 @@ class Compiler
                 $b = round($b);
 
                 if (count($value) === 5 && $value[4] !== 1) { // rgba
-                    return 'rgba(' . $r . ', ' . $g . ', ' . $b . ', ' . $value[4] . ')';
+                    $a = new Node\Number($value[4], '');
+
+                    return 'rgba(' . $r . ', ' . $g . ', ' . $b . ', ' . $a . ')';
                 }
 
                 $h = sprintf('#%02x%02x%02x', $r, $g, $b);
@@ -2771,11 +3305,8 @@ class Compiler
                 return $left . $this->compileValue($interpolate) . $right;
 
             case Type::T_INTERPOLATE:
-                // raw parse node
-                list(, $exp) = $value;
-
                 // strip quotes if it's a string
-                $reduced = $this->reduce($exp);
+                $reduced = $this->reduce($value[1]);
 
                 switch ($reduced[0]) {
                     case Type::T_LIST:
@@ -2825,7 +3356,7 @@ class Compiler
                 return 'null';
 
             default:
-                $this->throwError("unknown value type: $type");
+                $this->throwError("unknown value type: $value[0]");
         }
     }
 
@@ -2890,43 +3421,69 @@ class Compiler
      * Find the final set of selectors
      *
      * @param \Leafo\ScssPhp\Compiler\Environment $env
+     * @param \Leafo\ScssPhp\Block                $selfParent
      *
      * @return array
      */
-    protected function multiplySelectors(Environment $env)
+    protected function multiplySelectors(Environment $env, $selfParent = null)
     {
         $envs            = $this->compactEnv($env);
         $selectors       = [];
         $parentSelectors = [[]];
 
+        $selfParentSelectors = null;
+
+        if (! is_null($selfParent) && $selfParent->selectors) {
+            $selfParentSelectors = $this->evalSelectors($selfParent->selectors);
+        }
+
         while ($env = array_pop($envs)) {
             if (empty($env->selectors)) {
                 continue;
             }
 
-            $selectors = [];
+            $selectors = $env->selectors;
+
+            do {
+                $stillHasSelf = false;
+                $prevSelectors = $selectors;
+                $selectors = [];
 
-            foreach ($env->selectors as $selector) {
-                foreach ($parentSelectors as $parent) {
-                    $selectors[] = $this->joinSelectors($parent, $selector);
+                foreach ($prevSelectors as $selector) {
+                    foreach ($parentSelectors as $parent) {
+                        if ($selfParentSelectors) {
+                            foreach ($selfParentSelectors as $selfParent) {
+                                // if no '&' in the selector, each call will give same result, only add once
+                                $s = $this->joinSelectors($parent, $selector, $stillHasSelf, $selfParent);
+                                $selectors[serialize($s)] = $s;
+                            }
+                        } else {
+                            $s = $this->joinSelectors($parent, $selector, $stillHasSelf);
+                            $selectors[serialize($s)] = $s;
+                        }
+                    }
                 }
-            }
+            } while ($stillHasSelf);
 
             $parentSelectors = $selectors;
         }
 
+        $selectors = array_values($selectors);
+
         return $selectors;
     }
 
     /**
      * Join selectors; looks for & to replace, or append parent before child
      *
-     * @param array $parent
-     * @param array $child
-     *
+     * @param array   $parent
+     * @param array   $child
+     * @param boolean &$stillHasSelf
+     * @param array   $selfParentSelectors
+
      * @return array
      */
-    protected function joinSelectors($parent, $child)
+    protected function joinSelectors($parent, $child, &$stillHasSelf, $selfParentSelectors = null)
     {
         $setSelf = false;
         $out = [];
@@ -2935,16 +3492,33 @@ class Compiler
             $newPart = [];
 
             foreach ($part as $p) {
-                if ($p === static::$selfSelector) {
+                // only replace & once and should be recalled to be able to make combinations
+                if ($p === static::$selfSelector && $setSelf) {
+                    $stillHasSelf = true;
+                }
+
+                if ($p === static::$selfSelector && ! $setSelf) {
                     $setSelf = true;
 
-                    foreach ($parent as $i => $parentPart) {
+                    if (is_null($selfParentSelectors)) {
+                        $selfParentSelectors = $parent;
+                    }
+
+                    foreach ($selfParentSelectors as $i => $parentPart) {
                         if ($i > 0) {
                             $out[] = $newPart;
                             $newPart = [];
                         }
 
                         foreach ($parentPart as $pp) {
+                            if (is_array($pp)) {
+                                $flatten = [];
+                                array_walk_recursive($pp, function ($a) use (&$flatten) {
+                                    $flatten[] = $a;
+                                });
+                                $pp = implode($flatten);
+                            }
+
                             $newPart[] = $pp;
                         }
                     }
@@ -2984,6 +3558,12 @@ class Compiler
             ? $env->block->queryList
             : [[[Type::T_MEDIA_VALUE, $env->block->value]]];
 
+        $store = [$this->env, $this->storeEnv];
+        $this->env = $env;
+        $this->storeEnv = null;
+        $parentQueries = $this->evaluateMediaQuery($parentQueries);
+        list($this->env, $this->storeEnv) = $store;
+
         if ($childQueries === null) {
             $childQueries = $parentQueries;
         } else {
@@ -2992,7 +3572,11 @@ class Compiler
 
             foreach ($parentQueries as $parentQuery) {
                 foreach ($originalQueries as $childQuery) {
-                    $childQueries []= array_merge($parentQuery, $childQuery);
+                    $childQueries[] = array_merge(
+                        $parentQuery,
+                        [[Type::T_MEDIA_TYPE, [Type::T_KEYWORD, 'all']]],
+                        $childQuery
+                    );
                 }
             }
         }
@@ -3007,7 +3591,7 @@ class Compiler
      *
      * @return array
      */
-    private function compactEnv(Environment $env)
+    protected function compactEnv(Environment $env)
     {
         for ($envs = []; $env; $env = $env->parent) {
             $envs[] = $env;
@@ -3023,7 +3607,7 @@ class Compiler
      *
      * @return \Leafo\ScssPhp\Compiler\Environment
      */
-    private function extractEnv($envs)
+    protected function extractEnv($envs)
     {
         for ($env = null; $e = array_pop($envs);) {
             $e->parent = $env;
@@ -3078,8 +3662,9 @@ class Compiler
      * @param mixed                               $value
      * @param boolean                             $shadow
      * @param \Leafo\ScssPhp\Compiler\Environment $env
+     * @param mixed                               $valueUnreduced
      */
-    protected function set($name, $value, $shadow = false, Environment $env = null)
+    protected function set($name, $value, $shadow = false, Environment $env = null, $valueUnreduced = null)
     {
         $name = $this->normalizeName($name);
 
@@ -3088,9 +3673,9 @@ class Compiler
         }
 
         if ($shadow) {
-            $this->setRaw($name, $value, $env);
+            $this->setRaw($name, $value, $env, $valueUnreduced);
         } else {
-            $this->setExisting($name, $value, $env);
+            $this->setExisting($name, $value, $env, $valueUnreduced);
         }
     }
 
@@ -3100,8 +3685,9 @@ class Compiler
      * @param string                              $name
      * @param mixed                               $value
      * @param \Leafo\ScssPhp\Compiler\Environment $env
+     * @param mixed                               $valueUnreduced
      */
-    protected function setExisting($name, $value, Environment $env)
+    protected function setExisting($name, $value, Environment $env, $valueUnreduced = null)
     {
         $storeEnv = $env;
 
@@ -3126,6 +3712,10 @@ class Compiler
         }
 
         $env->store[$name] = $value;
+
+        if ($valueUnreduced) {
+            $env->storeUnreduced[$name] = $valueUnreduced;
+        }
     }
 
     /**
@@ -3134,10 +3724,15 @@ class Compiler
      * @param string                              $name
      * @param mixed                               $value
      * @param \Leafo\ScssPhp\Compiler\Environment $env
+     * @param mixed                               $valueUnreduced
      */
-    protected function setRaw($name, $value, Environment $env)
+    protected function setRaw($name, $value, Environment $env, $valueUnreduced = null)
     {
         $env->store[$name] = $value;
+
+        if ($valueUnreduced) {
+            $env->storeUnreduced[$name] = $valueUnreduced;
+        }
     }
 
     /**
@@ -3148,10 +3743,11 @@ class Compiler
      * @param string                              $name
      * @param boolean                             $shouldThrow
      * @param \Leafo\ScssPhp\Compiler\Environment $env
+     * @param boolean                             $unreduced
      *
-     * @return mixed
+     * @return mixed|null
      */
-    public function get($name, $shouldThrow = true, Environment $env = null)
+    public function get($name, $shouldThrow = true, Environment $env = null, $unreduced = false)
     {
         $normalizedName = $this->normalizeName($name);
         $specialContentKey = static::$namespaces['special'] . 'content';
@@ -3163,15 +3759,24 @@ class Compiler
         $nextIsRoot = false;
         $hasNamespace = $normalizedName[0] === '^' || $normalizedName[0] === '@' || $normalizedName[0] === '%';
 
+        $maxDepth = 10000;
+
         for (;;) {
+            if ($maxDepth-- <= 0) {
+                break;
+            }
+
             if (array_key_exists($normalizedName, $env->store)) {
+                if ($unreduced && isset($env->storeUnreduced[$normalizedName])) {
+                    return $env->storeUnreduced[$normalizedName];
+                }
+
                 return $env->store[$normalizedName];
             }
 
             if (! $hasNamespace && isset($env->marker)) {
                 if (! $nextIsRoot && ! empty($env->store[$specialContentKey])) {
                     $env = $env->store[$specialContentKey]->scope;
-                    $nextIsRoot = true;
                     continue;
                 }
 
@@ -3187,10 +3792,11 @@ class Compiler
         }
 
         if ($shouldThrow) {
-            $this->throwError("Undefined variable \$$name");
+            $this->throwError("Undefined variable \$$name" . ($maxDepth<=0 ? " (infinite recursion)" : ""));
         }
 
         // found nothing
+        return null;
     }
 
     /**
@@ -3299,7 +3905,7 @@ class Compiler
      *
      * @api
      *
-     * @param string $path
+     * @param string|callable $path
      */
     public function addImportPath($path)
     {
@@ -3421,10 +4027,10 @@ class Compiler
     /**
      * Import file
      *
-     * @param string $path
-     * @param array  $out
+     * @param string                               $path
+     * @param \Leafo\ScssPhp\Formatter\OutputBlock $out
      */
-    protected function importFile($path, $out)
+    protected function importFile($path, OutputBlock $out)
     {
         // see if tree is cached
         $realPath = realpath($path);
@@ -3472,9 +4078,12 @@ class Compiler
             if (is_string($dir)) {
                 // check urls for normal import paths
                 foreach ($urls as $full) {
-                    $full = $dir
-                        . (! empty($dir) && substr($dir, -1) !== '/' ? '/' : '')
-                        . $full;
+                    $separator = (
+                        ! empty($dir) &&
+                        substr($dir, -1) !== '/' &&
+                        substr($full, 0, 1) !== '/'
+                    ) ? '/' : '';
+                    $full = $dir . $separator . $full;
 
                     if ($this->fileExists($file = $full . '.scss') ||
                         ($hasExtension && $this->fileExists($file = $full))
@@ -3519,6 +4128,8 @@ class Compiler
     public function setIgnoreErrors($ignoreErrors)
     {
         $this->ignoreErrors = $ignoreErrors;
+
+        return $this;
     }
 
     /**
@@ -3541,11 +4152,53 @@ class Compiler
         }
 
         $line = $this->sourceLine;
-        $msg = "$msg: line: $line";
+        $loc = isset($this->sourceNames[$this->sourceIndex])
+             ? $this->sourceNames[$this->sourceIndex] . " on line $line"
+             : "line: $line";
+        $msg = "$msg: $loc";
+
+        $callStackMsg = $this->callStackMessage();
+
+        if ($callStackMsg) {
+            $msg .= "\nCall Stack:\n" . $callStackMsg;
+        }
 
         throw new CompilerException($msg);
     }
 
+    /**
+     * Beautify call stack for output
+     *
+     * @param boolean $all
+     * @param null    $limit
+     *
+     * @return string
+     */
+    protected function callStackMessage($all = false, $limit = null)
+    {
+        $callStackMsg = [];
+        $ncall = 0;
+
+        if ($this->callStack) {
+            foreach (array_reverse($this->callStack) as $call) {
+                if ($all || (isset($call['n']) && $call['n'])) {
+                    $msg = "#" . $ncall++ . " " . $call['n'] . " ";
+                    $msg .= (isset($this->sourceNames[$call[Parser::SOURCE_INDEX]])
+                          ? $this->sourceNames[$call[Parser::SOURCE_INDEX]]
+                          : '(unknown file)');
+                    $msg .= " on line " . $call[Parser::SOURCE_LINE];
+                    $callStackMsg[] = $msg;
+
+                    if (! is_null($limit) && $ncall>$limit) {
+                        break;
+                    }
+                }
+            }
+        }
+
+        return implode("\n", $callStackMsg);
+    }
+
     /**
      * Handle import loop
      *
@@ -3611,7 +4264,7 @@ class Compiler
 
         $this->env->marker = 'function';
 
-        $ret = $this->compileChildren($func->children, $tmp);
+        $ret = $this->compileChildren($func->children, $tmp, $this->env->marker . " " . $name);
 
         $this->storeEnv = $storeEnv;
 
@@ -3646,7 +4299,7 @@ class Compiler
             return false;
         }
 
-        list($sorted, $kwargs) = $this->sortArgs($prototype, $args);
+        @list($sorted, $kwargs) = $this->sortArgs($prototype, $args);
 
         if ($name !== 'if' && $name !== 'call') {
             foreach ($sorted as &$val) {
@@ -3705,7 +4358,7 @@ class Compiler
             $key = $key[1];
 
             if (empty($key)) {
-                $posArgs[] = $value;
+                $posArgs[] = empty($arg[2]) ? $value : $arg;
             } else {
                 $keyArgs[$key] = $value;
             }
@@ -3855,7 +4508,7 @@ class Compiler
      *
      * @return array|\Leafo\ScssPhp\Node\Number
      */
-    private function coerceValue($value)
+    protected function coerceValue($value)
     {
         if (is_array($value) || $value instanceof \ArrayAccess) {
             return $value;
@@ -4057,7 +4710,7 @@ class Compiler
         $value = $this->coerceMap($value);
 
         if ($value[0] !== Type::T_MAP) {
-            $this->throwError('expecting map');
+            $this->throwError('expecting map, %s received', $value[0]);
         }
 
         return $value;
@@ -4077,7 +4730,7 @@ class Compiler
     public function assertList($value)
     {
         if ($value[0] !== Type::T_LIST) {
-            $this->throwError('expecting list');
+            $this->throwError('expecting list, %s received', $value[0]);
         }
 
         return $value;
@@ -4100,7 +4753,7 @@ class Compiler
             return $color;
         }
 
-        $this->throwError('expecting color');
+        $this->throwError('expecting color, %s received', $value[0]);
     }
 
     /**
@@ -4117,7 +4770,7 @@ class Compiler
     public function assertNumber($value)
     {
         if ($value[0] !== Type::T_NUMBER) {
-            $this->throwError('expecting number');
+            $this->throwError('expecting number, %s received', $value[0]);
         }
 
         return $value[1];
@@ -4194,7 +4847,7 @@ class Compiler
      *
      * @return float
      */
-    private function hueToRGB($m1, $m2, $h)
+    protected function hueToRGB($m1, $m2, $h)
     {
         if ($h < 0) {
             $h += 1;
@@ -4257,20 +4910,38 @@ class Compiler
     {
         $name = $this->compileStringContent($this->coerceString($this->reduce(array_shift($args), true)));
 
-        $args = array_map(
-            function ($a) {
-                return [null, $a, false];
-            },
-            $args
-        );
+        $posArgs = [];
+
+        foreach ($args as $arg) {
+            if (empty($arg[0])) {
+                if ($arg[2] === true) {
+                    $tmp = $this->reduce($arg[1]);
+
+                    if ($tmp[0] === Type::T_LIST) {
+                        foreach ($tmp[2] as $item) {
+                            $posArgs[] = [null, $item, false];
+                        }
+                    } else {
+                        $posArgs[] = [null, $tmp, true];
+                    }
+
+                    continue;
+                }
+
+                $posArgs[] = [null, $this->reduce($arg), false];
+                continue;
+            }
+
+            $posArgs[] = [null, $arg, false];
+        }
 
         if (count($kwargs)) {
             foreach ($kwargs as $key => $value) {
-                $args[] = [[Type::T_VARIABLE, $key], $value, false];
+                $posArgs[] = [[Type::T_VARIABLE, $key], $value, false];
             }
         }
 
-        return $this->reduce([Type::T_FUNCTION_CALL, $name, $args]);
+        return $this->reduce([Type::T_FUNCTION_CALL, $name, $posArgs]);
     }
 
     protected static $libIf = ['condition', 'if-true', 'if-false'];
@@ -4331,7 +5002,7 @@ class Compiler
     protected function libRgba($args)
     {
         if ($color = $this->coerceColor($args[0])) {
-            $num = ! isset($args[1]) ? $args[3] : $args[1];
+            $num = isset($args[3]) ? $args[3] : $args[1];
             $alpha = $this->assertNumber($num);
             $color[4] = $alpha;
 
@@ -4526,7 +5197,7 @@ class Compiler
         ];
 
         if ($firstAlpha != 1.0 || $secondAlpha != 1.0) {
-            $new[] = $firstAlpha * $weight + $secondAlpha * ($weight - 1);
+            $new[] = $firstAlpha * $weight + $secondAlpha * (1 - $weight);
         }
 
         return $this->fixColor($new);
@@ -4829,7 +5500,7 @@ class Compiler
             if (null === $unit) {
                 $unit = $number[2];
                 $originalUnit = $item->unitStr();
-            } elseif ($unit !== $number[2]) {
+            } elseif ($number[1] && $unit !== $number[2]) {
                 $this->throwError('Incompatible units: "%s" and "%s".', $originalUnit, $item->unitStr());
                 break;
             }
@@ -4898,7 +5569,7 @@ class Compiler
         if (! isset($list[2][$n])) {
             $this->throwError('Invalid argument for "n"');
 
-            return;
+            return null;
         }
 
         $list[2][$n] = $args[2];
@@ -4976,7 +5647,21 @@ class Compiler
         $map1 = $this->assertMap($args[0]);
         $map2 = $this->assertMap($args[1]);
 
-        return [Type::T_MAP, array_merge($map1[1], $map2[1]), array_merge($map1[2], $map2[2])];
+        foreach ($map2[1] as $i2 => $key2) {
+            $key = $this->compileStringContent($this->coerceString($key2));
+
+            foreach ($map1[1] as $i1 => $key1) {
+                if ($key === $this->compileStringContent($this->coerceString($key1))) {
+                    $map1[2][$i1] = $map2[2][$i2];
+                    continue 2;
+                }
+            }
+
+            $map1[1][] = $map2[1][$i2];
+            $map1[2][] = $map2[2][$i2];
+        }
+
+        return $map1;
     }
 
     protected static $libKeywords = ['args'];
@@ -5122,7 +5807,7 @@ class Compiler
         ) {
             $this->throwError('Invalid argument(s) for "comparable"');
 
-            return;
+            return null;
         }
 
         $number1 = $number1->normalize();
@@ -5302,7 +5987,7 @@ class Compiler
             if ($n < 1) {
                 $this->throwError("limit must be greater than or equal to 1");
 
-                return;
+                return null;
             }
 
             return new Node\Number(mt_rand(1, $n), '');
index fe309dd..99231f3 100644 (file)
@@ -33,6 +33,11 @@ class Environment
      */
     public $store;
 
+    /**
+     * @var array
+     */
+    public $storeUnreduced;
+
     /**
      * @var integer
      */
index b4f90aa..1403859 100644 (file)
@@ -256,7 +256,8 @@ abstract class Formatter
                 $this->currentLine,
                 $this->currentColumn,
                 $this->currentBlock->sourceLine,
-                $this->currentBlock->sourceColumn - 1, //columns from parser are off by one
+                //columns from parser are off by one
+                $this->currentBlock->sourceColumn > 0 ? $this->currentBlock->sourceColumn - 1 : 0,
                 $this->currentBlock->sourceName
             );
 
index 1faa7e1..ab38529 100644 (file)
@@ -59,4 +59,23 @@ class Compressed extends Formatter
             $this->write($this->break);
         }
     }
+
+    /**
+     * Output block selectors
+     *
+     * @param \Leafo\ScssPhp\Formatter\OutputBlock $block
+     */
+    protected function blockSelectors(OutputBlock $block)
+    {
+        $inner = $this->indentStr();
+
+        $this->write(
+            $inner
+            . implode(
+                $this->tagSeparator,
+                str_replace([' > ', ' + ', ' ~ '], ['>', '+', '~'], $block->selectors)
+            )
+            . $this->open . $this->break
+        );
+    }
 }
index 42d77b5..da740cc 100644 (file)
@@ -57,4 +57,23 @@ class Crunched extends Formatter
             $this->write($this->break);
         }
     }
+
+    /**
+     * Output block selectors
+     *
+     * @param \Leafo\ScssPhp\Formatter\OutputBlock $block
+     */
+    protected function blockSelectors(OutputBlock $block)
+    {
+        $inner = $this->indentStr();
+
+        $this->write(
+            $inner
+            . implode(
+                $this->tagSeparator,
+                str_replace([' > ', ' + ', ' ~ '], ['>', '+', '~'], $block->selectors)
+            )
+            . $this->open . $this->break
+        );
+    }
 }
index 6fdea3e..9f8cb2e 100644 (file)
@@ -12,6 +12,7 @@
 namespace Leafo\ScssPhp;
 
 use Leafo\ScssPhp\Block;
+use Leafo\ScssPhp\Cache;
 use Leafo\ScssPhp\Compiler;
 use Leafo\ScssPhp\Exception\ParserException;
 use Leafo\ScssPhp\Node;
@@ -53,6 +54,8 @@ class Parser
     protected static $operatorPattern;
     protected static $whitePattern;
 
+    protected $cache;
+
     private $sourceName;
     private $sourceIndex;
     private $sourcePositions;
@@ -65,23 +68,26 @@ class Parser
     private $utf8;
     private $encoding;
     private $patternModifiers;
+    private $commentsSeen;
 
     /**
      * Constructor
      *
      * @api
      *
-     * @param string  $sourceName
-     * @param integer $sourceIndex
-     * @param string  $encoding
+     * @param string               $sourceName
+     * @param integer              $sourceIndex
+     * @param string               $encoding
+     * @param \Leafo\ScssPhp\Cache $cache
      */
-    public function __construct($sourceName, $sourceIndex = 0, $encoding = 'utf-8')
+    public function __construct($sourceName, $sourceIndex = 0, $encoding = 'utf-8', $cache = null)
     {
         $this->sourceName       = $sourceName ?: '(stdin)';
         $this->sourceIndex      = $sourceIndex;
         $this->charset          = null;
         $this->utf8             = ! $encoding || strtolower($encoding) === 'utf-8';
         $this->patternModifiers = $this->utf8 ? 'Aisu' : 'Ais';
+        $this->commentsSeen     = [];
 
         if (empty(static::$operatorPattern)) {
             static::$operatorPattern = '([*\/%+-]|[!=]\=|\>\=?|\<\=\>|\<\=?|and|or)';
@@ -95,6 +101,10 @@ class Parser
                 ? '/' . $commentSingle . '[^\n]*\s*|(' . static::$commentPattern . ')\s*|\s+/AisuS'
                 : '/' . $commentSingle . '[^\n]*\s*|(' . static::$commentPattern . ')\s*|\s+/AisS';
         }
+
+        if ($cache) {
+            $this->cache = $cache;
+        }
     }
 
     /**
@@ -142,6 +152,19 @@ class Parser
      */
     public function parse($buffer)
     {
+        if ($this->cache) {
+            $cacheKey = $this->sourceName . ":" . md5($buffer);
+            $parseOptions = [
+                'charset' => $this->charset,
+                'utf8' => $this->utf8,
+            ];
+            $v = $this->cache->getCache("parse", $cacheKey, $parseOptions);
+
+            if (! is_null($v)) {
+                return $v;
+            }
+        }
+
         // strip BOM (byte order marker)
         if (substr($buffer, 0, 3) === "\xef\xbb\xbf") {
             $buffer = substr($buffer, 3);
@@ -181,6 +204,10 @@ class Parser
 
         $this->restoreEncoding();
 
+        if ($this->cache) {
+            $this->cache->setCache("parse", $cacheKey, $this->env, $parseOptions);
+        }
+
         return $this->env;
     }
 
@@ -271,7 +298,7 @@ class Parser
      * the buffer position will be left at an invalid state. In order to
      * avoid this, Compiler::seek() is used to remember and set buffer positions.
      *
-     * Before parsing a chain, use $s = $this->seek() to remember the current
+     * Before parsing a chain, use $s = $this->count to remember the current
      * position into $s. Then if a chain fails, use $this->seek($s) to
      * go back where we started.
      *
@@ -279,14 +306,14 @@ class Parser
      */
     protected function parseChunk()
     {
-        $s = $this->seek();
+        $s = $this->count;
 
         // the directives
         if (isset($this->buffer[$this->count]) && $this->buffer[$this->count] === '@') {
-            if ($this->literal('@at-root') &&
+            if ($this->literal('@at-root', 8) &&
                 ($this->selectors($selector) || true) &&
                 ($this->map($with) || true) &&
-                $this->literal('{')
+                $this->matchChar('{')
             ) {
                 $atRoot = $this->pushSpecialBlock(Type::T_AT_ROOT, $s);
                 $atRoot->selector = $selector;
@@ -297,7 +324,7 @@ class Parser
 
             $this->seek($s);
 
-            if ($this->literal('@media') && $this->mediaQueryList($mediaQueryList) && $this->literal('{')) {
+            if ($this->literal('@media', 6) && $this->mediaQueryList($mediaQueryList) && $this->matchChar('{')) {
                 $media = $this->pushSpecialBlock(Type::T_MEDIA, $s);
                 $media->queryList = $mediaQueryList[2];
 
@@ -306,10 +333,10 @@ class Parser
 
             $this->seek($s);
 
-            if ($this->literal('@mixin') &&
+            if ($this->literal('@mixin', 6) &&
                 $this->keyword($mixinName) &&
                 ($this->argumentDef($args) || true) &&
-                $this->literal('{')
+                $this->matchChar('{')
             ) {
                 $mixin = $this->pushSpecialBlock(Type::T_MIXIN, $s);
                 $mixin->name = $mixinName;
@@ -320,13 +347,13 @@ class Parser
 
             $this->seek($s);
 
-            if ($this->literal('@include') &&
+            if ($this->literal('@include', 8) &&
                 $this->keyword($mixinName) &&
-                ($this->literal('(') &&
+                ($this->matchChar('(') &&
                     ($this->argValues($argValues) || true) &&
-                    $this->literal(')') || true) &&
+                    $this->matchChar(')') || true) &&
                 ($this->end() ||
-                    $this->literal('{') && $hasBlock = true)
+                    $this->matchChar('{') && $hasBlock = true)
             ) {
                 $child = [Type::T_INCLUDE, $mixinName, isset($argValues) ? $argValues : null, null];
 
@@ -342,7 +369,7 @@ class Parser
 
             $this->seek($s);
 
-            if ($this->literal('@scssphp-import-once') &&
+            if ($this->literal('@scssphp-import-once', 20) &&
                 $this->valueList($importPath) &&
                 $this->end()
             ) {
@@ -353,7 +380,7 @@ class Parser
 
             $this->seek($s);
 
-            if ($this->literal('@import') &&
+            if ($this->literal('@import', 7) &&
                 $this->valueList($importPath) &&
                 $this->end()
             ) {
@@ -364,7 +391,7 @@ class Parser
 
             $this->seek($s);
 
-            if ($this->literal('@import') &&
+            if ($this->literal('@import', 7) &&
                 $this->url($importPath) &&
                 $this->end()
             ) {
@@ -375,7 +402,7 @@ class Parser
 
             $this->seek($s);
 
-            if ($this->literal('@extend') &&
+            if ($this->literal('@extend', 7) &&
                 $this->selectors($selectors) &&
                 $this->end()
             ) {
@@ -388,10 +415,10 @@ class Parser
 
             $this->seek($s);
 
-            if ($this->literal('@function') &&
+            if ($this->literal('@function', 9) &&
                 $this->keyword($fnName) &&
                 $this->argumentDef($args) &&
-                $this->literal('{')
+                $this->matchChar('{')
             ) {
                 $func = $this->pushSpecialBlock(Type::T_FUNCTION, $s);
                 $func->name = $fnName;
@@ -402,7 +429,7 @@ class Parser
 
             $this->seek($s);
 
-            if ($this->literal('@break') && $this->end()) {
+            if ($this->literal('@break', 6) && $this->end()) {
                 $this->append([Type::T_BREAK], $s);
 
                 return true;
@@ -410,7 +437,7 @@ class Parser
 
             $this->seek($s);
 
-            if ($this->literal('@continue') && $this->end()) {
+            if ($this->literal('@continue', 9) && $this->end()) {
                 $this->append([Type::T_CONTINUE], $s);
 
                 return true;
@@ -418,8 +445,7 @@ class Parser
 
             $this->seek($s);
 
-
-            if ($this->literal('@return') && ($this->valueList($retVal) || true) && $this->end()) {
+            if ($this->literal('@return', 7) && ($this->valueList($retVal) || true) && $this->end()) {
                 $this->append([Type::T_RETURN, isset($retVal) ? $retVal : [Type::T_NULL]], $s);
 
                 return true;
@@ -427,11 +453,11 @@ class Parser
 
             $this->seek($s);
 
-            if ($this->literal('@each') &&
+            if ($this->literal('@each', 5) &&
                 $this->genericList($varNames, 'variable', ',', false) &&
-                $this->literal('in') &&
+                $this->literal('in', 2) &&
                 $this->valueList($list) &&
-                $this->literal('{')
+                $this->matchChar('{')
             ) {
                 $each = $this->pushSpecialBlock(Type::T_EACH, $s);
 
@@ -446,9 +472,9 @@ class Parser
 
             $this->seek($s);
 
-            if ($this->literal('@while') &&
+            if ($this->literal('@while', 6) &&
                 $this->expression($cond) &&
-                $this->literal('{')
+                $this->matchChar('{')
             ) {
                 $while = $this->pushSpecialBlock(Type::T_WHILE, $s);
                 $while->cond = $cond;
@@ -458,14 +484,14 @@ class Parser
 
             $this->seek($s);
 
-            if ($this->literal('@for') &&
+            if ($this->literal('@for', 4) &&
                 $this->variable($varName) &&
-                $this->literal('from') &&
+                $this->literal('from', 4) &&
                 $this->expression($start) &&
-                ($this->literal('through') ||
-                    ($forUntil = true && $this->literal('to'))) &&
+                ($this->literal('through', 7) ||
+                    ($forUntil = true && $this->literal('to', 2))) &&
                 $this->expression($end) &&
-                $this->literal('{')
+                $this->matchChar('{')
             ) {
                 $for = $this->pushSpecialBlock(Type::T_FOR, $s);
                 $for->var = $varName[1];
@@ -478,7 +504,7 @@ class Parser
 
             $this->seek($s);
 
-            if ($this->literal('@if') && $this->valueList($cond) && $this->literal('{')) {
+            if ($this->literal('@if', 3) && $this->valueList($cond) && $this->matchChar('{')) {
                 $if = $this->pushSpecialBlock(Type::T_IF, $s);
                 $if->cond = $cond;
                 $if->cases = [];
@@ -488,7 +514,7 @@ class Parser
 
             $this->seek($s);
 
-            if ($this->literal('@debug') &&
+            if ($this->literal('@debug', 6) &&
                 $this->valueList($value) &&
                 $this->end()
             ) {
@@ -499,7 +525,7 @@ class Parser
 
             $this->seek($s);
 
-            if ($this->literal('@warn') &&
+            if ($this->literal('@warn', 5) &&
                 $this->valueList($value) &&
                 $this->end()
             ) {
@@ -510,7 +536,7 @@ class Parser
 
             $this->seek($s);
 
-            if ($this->literal('@error') &&
+            if ($this->literal('@error', 6) &&
                 $this->valueList($value) &&
                 $this->end()
             ) {
@@ -521,7 +547,7 @@ class Parser
 
             $this->seek($s);
 
-            if ($this->literal('@content') && $this->end()) {
+            if ($this->literal('@content', 8) && $this->end()) {
                 $this->append([Type::T_MIXIN_CONTENT], $s);
 
                 return true;
@@ -534,10 +560,10 @@ class Parser
             if (isset($last) && $last[0] === Type::T_IF) {
                 list(, $if) = $last;
 
-                if ($this->literal('@else')) {
-                    if ($this->literal('{')) {
+                if ($this->literal('@else', 5)) {
+                    if ($this->matchChar('{')) {
                         $else = $this->pushSpecialBlock(Type::T_ELSE, $s);
-                    } elseif ($this->literal('if') && $this->valueList($cond) && $this->literal('{')) {
+                    } elseif ($this->literal('if', 2) && $this->valueList($cond) && $this->matchChar('{')) {
                         $else = $this->pushSpecialBlock(Type::T_ELSEIF, $s);
                         $else->cond = $cond;
                     }
@@ -554,7 +580,7 @@ class Parser
             }
 
             // only retain the first @charset directive encountered
-            if ($this->literal('@charset') &&
+            if ($this->literal('@charset', 8) &&
                 $this->valueList($charset) &&
                 $this->end()
             ) {
@@ -576,10 +602,10 @@ class Parser
             $this->seek($s);
 
             // doesn't match built in directive, do generic one
-            if ($this->literal('@', false) &&
+            if ($this->matchChar('@', false) &&
                 $this->keyword($dirName) &&
                 ($this->variable($dirValue) || $this->openString('{', $dirValue) || true) &&
-                $this->literal('{')
+                $this->matchChar('{')
             ) {
                 if ($dirName === 'media') {
                     $directive = $this->pushSpecialBlock(Type::T_MEDIA, $s);
@@ -603,7 +629,7 @@ class Parser
         // property shortcut
         // captures most properties before having to parse a selector
         if ($this->keyword($name, false) &&
-            $this->literal(': ') &&
+            $this->literal(': ', 2) &&
             $this->valueList($value) &&
             $this->end()
         ) {
@@ -617,7 +643,7 @@ class Parser
 
         // variable assigns
         if ($this->variable($name) &&
-            $this->literal(':') &&
+            $this->matchChar(':') &&
             $this->valueList($value) &&
             $this->end()
         ) {
@@ -631,12 +657,12 @@ class Parser
         $this->seek($s);
 
         // misc
-        if ($this->literal('-->')) {
+        if ($this->literal('-->', 3)) {
             return true;
         }
 
         // opening css block
-        if ($this->selectors($selectors) && $this->literal('{')) {
+        if ($this->selectors($selectors) && $this->matchChar('{')) {
             $this->pushBlock($selectors, $s);
 
             return true;
@@ -645,7 +671,7 @@ class Parser
         $this->seek($s);
 
         // property assign, or nested assign
-        if ($this->propertyName($name) && $this->literal(':')) {
+        if ($this->propertyName($name) && $this->matchChar(':')) {
             $foundSomething = false;
 
             if ($this->valueList($value)) {
@@ -653,7 +679,7 @@ class Parser
                 $foundSomething = true;
             }
 
-            if ($this->literal('{')) {
+            if ($this->matchChar('{')) {
                 $propBlock = $this->pushSpecialBlock(Type::T_NESTED_PROPERTY, $s);
                 $propBlock->prefix = $name;
                 $foundSomething = true;
@@ -669,7 +695,7 @@ class Parser
         $this->seek($s);
 
         // closing a block
-        if ($this->literal('}')) {
+        if ($this->matchChar('}')) {
             $block = $this->popBlock();
 
             if (isset($block->type) && $block->type === Type::T_INCLUDE) {
@@ -686,8 +712,8 @@ class Parser
         }
 
         // extra stuff
-        if ($this->literal(';') ||
-            $this->literal('<!--')
+        if ($this->matchChar(';') ||
+            $this->literal('<!--', 4)
         ) {
             return true;
         }
@@ -763,11 +789,18 @@ class Parser
             $this->throwParseError('unexpected }');
         }
 
+        if ($block->type == Type::T_AT_ROOT) {
+            // keeps the parent in case of self selector &
+            $block->selfParent = $block->parent;
+        }
+
         $this->env = $block->parent;
+
         unset($block->parent);
 
         $comments = $block->comments;
-        if (count($comments)) {
+
+        if ($comments) {
             $this->env->comments = $comments;
             unset($block->comments);
         }
@@ -800,18 +833,10 @@ class Parser
      * Seek to position in input stream (or return current position in input stream)
      *
      * @param integer $where
-     *
-     * @return integer
      */
-    protected function seek($where = null)
+    protected function seek($where)
     {
-        if ($where === null) {
-            return $this->count;
-        }
-
         $this->count = $where;
-
-        return true;
     }
 
     /**
@@ -866,52 +891,78 @@ class Parser
      */
     protected function match($regex, &$out, $eatWhitespace = null)
     {
+        $r = '/' . $regex . '/' . $this->patternModifiers;
+
+        if (! preg_match($r, $this->buffer, $out, null, $this->count)) {
+            return false;
+        }
+
+        $this->count += strlen($out[0]);
+
         if (! isset($eatWhitespace)) {
             $eatWhitespace = $this->eatWhiteDefault;
         }
 
-        $r = '/' . $regex . '/' . $this->patternModifiers;
+        if ($eatWhitespace) {
+            $this->whitespace();
+        }
 
-        if (preg_match($r, $this->buffer, $out, null, $this->count)) {
-            $this->count += strlen($out[0]);
+        return true;
+    }
 
-            if ($eatWhitespace) {
-                $this->whitespace();
-            }
+    /**
+     * Match a single string
+     *
+     * @param string  $char
+     * @param boolean $eatWhitespace
+     *
+     * @return boolean
+     */
+    protected function matchChar($char, $eatWhitespace = null)
+    {
+        if (! isset($this->buffer[$this->count]) || $this->buffer[$this->count] !== $char) {
+            return false;
+        }
 
-            return true;
+        $this->count++;
+
+        if (! isset($eatWhitespace)) {
+            $eatWhitespace = $this->eatWhiteDefault;
         }
 
-        return false;
+        if ($eatWhitespace) {
+            $this->whitespace();
+        }
+
+        return true;
     }
 
     /**
      * Match literal string
      *
      * @param string  $what
+     * @param integer $len
      * @param boolean $eatWhitespace
      *
      * @return boolean
      */
-    protected function literal($what, $eatWhitespace = null)
+    protected function literal($what, $len, $eatWhitespace = null)
     {
-        if (! isset($eatWhitespace)) {
-            $eatWhitespace = $this->eatWhiteDefault;
+        if (strcasecmp(substr($this->buffer, $this->count, $len), $what) !== 0) {
+            return false;
         }
 
-        $len = strlen($what);
-
-        if (strcasecmp(substr($this->buffer, $this->count, $len), $what) === 0) {
-            $this->count += $len;
+        $this->count += $len;
 
-            if ($eatWhitespace) {
-                $this->whitespace();
-            }
+        if (! isset($eatWhitespace)) {
+            $eatWhitespace = $this->eatWhiteDefault;
+        }
 
-            return true;
+        if ($eatWhitespace) {
+            $this->whitespace();
         }
 
-        return false;
+        return true;
     }
 
     /**
@@ -969,7 +1020,7 @@ class Parser
 
         $comments = $this->env->comments;
 
-        if (count($comments)) {
+        if ($comments) {
             $this->env->children = array_merge($this->env->children, $comments);
             $this->env->comments = [];
         }
@@ -1013,7 +1064,7 @@ class Parser
         $expressions = null;
         $parts = [];
 
-        if (($this->literal('only') && ($only = true) || $this->literal('not') && ($not = true) || true) &&
+        if (($this->literal('only', 4) && ($only = true) || $this->literal('not', 3) && ($not = true) || true) &&
             $this->mixedKeyword($mediaType)
         ) {
             $prop = [Type::T_MEDIA_TYPE];
@@ -1040,7 +1091,7 @@ class Parser
             $parts[] = $prop;
         }
 
-        if (empty($parts) || $this->literal('and')) {
+        if (empty($parts) || $this->literal('and', 3)) {
             $this->genericList($expressions, 'mediaExpression', 'and', false);
 
             if (is_array($expressions)) {
@@ -1062,13 +1113,13 @@ class Parser
      */
     protected function mediaExpression(&$out)
     {
-        $s = $this->seek();
+        $s = $this->count;
         $value = null;
 
-        if ($this->literal('(') &&
+        if ($this->matchChar('(') &&
             $this->expression($feature) &&
-            ($this->literal(':') && $this->expression($value) || true) &&
-            $this->literal(')')
+            ($this->matchChar(':') && $this->expression($value) || true) &&
+            $this->matchChar(')')
         ) {
             $out = [Type::T_MEDIA_EXPRESSION, $feature];
 
@@ -1111,20 +1162,20 @@ class Parser
      */
     protected function argValue(&$out)
     {
-        $s = $this->seek();
+        $s = $this->count;
 
         $keyword = null;
 
-        if (! $this->variable($keyword) || ! $this->literal(':')) {
+        if (! $this->variable($keyword) || ! $this->matchChar(':')) {
             $this->seek($s);
             $keyword = null;
         }
 
         if ($this->genericList($value, 'expression')) {
             $out = [$keyword, $value, false];
-            $s = $this->seek();
+            $s = $this->count;
 
-            if ($this->literal('...')) {
+            if ($this->literal('...', 3)) {
                 $out[2] = true;
             } else {
                 $this->seek($s);
@@ -1139,7 +1190,7 @@ class Parser
     /**
      * Parse comma separated value list
      *
-     * @param string $out
+     * @param array $out
      *
      * @return boolean
      */
@@ -1172,20 +1223,21 @@ class Parser
      */
     protected function genericList(&$out, $parseItem, $delim = '', $flatten = true)
     {
-        $s = $this->seek();
+        $s = $this->count;
         $items = [];
+        $value = null;
 
         while ($this->$parseItem($value)) {
             $items[] = $value;
 
             if ($delim) {
-                if (! $this->literal($delim)) {
+                if (! $this->literal($delim, strlen($delim))) {
                     break;
                 }
             }
         }
 
-        if (count($items) === 0) {
+        if (! $items) {
             $this->seek($s);
 
             return false;
@@ -1209,22 +1261,22 @@ class Parser
      */
     protected function expression(&$out)
     {
-        $s = $this->seek();
-
-        if ($this->literal('(')) {
-            if ($this->literal(')')) {
-                $out = [Type::T_LIST, '', []];
+        $s = $this->count;
 
-                return true;
-            }
-
-            if ($this->valueList($out) && $this->literal(')') && $out[0] === Type::T_LIST) {
+        if ($this->matchChar('(')) {
+            if ($this->parenExpression($out, $s, ")")) {
                 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 ($this->map($out)) {
                 return true;
             }
 
@@ -1240,6 +1292,37 @@ class Parser
         return false;
     }
 
+    /**
+     * Parse expression specifically checking for lists in parenthesis or brackets
+     *
+     * @param array   $out
+     * @param integer $s
+     * @param string  $closingParen
+     * @param array   $allowedTypes
+     *
+     * @return boolean
+     */
+    protected function parenExpression(&$out, $s, $closingParen = ")", $allowedTypes = [Type::T_LIST, Type::T_MAP])
+    {
+        if ($this->matchChar($closingParen)) {
+            $out = [Type::T_LIST, '', []];
+
+            return true;
+        }
+
+        if ($this->valueList($out) && $this->matchChar($closingParen) && in_array($out[0], $allowedTypes)) {
+            return true;
+        }
+
+        $this->seek($s);
+
+        if (in_array(Type::T_MAP, $allowedTypes) && $this->map($out)) {
+            return true;
+        }
+
+        return false;
+    }
+
     /**
      * Parse left-hand side of subexpression
      *
@@ -1252,7 +1335,7 @@ class Parser
     {
         $operators = static::$operatorPattern;
 
-        $ss = $this->seek();
+        $ss = $this->count;
         $whiteBefore = isset($this->buffer[$this->count - 1]) &&
             ctype_space($this->buffer[$this->count - 1]);
 
@@ -1281,7 +1364,7 @@ class Parser
             }
 
             $lhs = [Type::T_EXPRESSION, $op, $lhs, $rhs, $this->inParens, $whiteBefore, $whiteAfter];
-            $ss = $this->seek();
+            $ss = $this->count;
             $whiteBefore = isset($this->buffer[$this->count - 1]) &&
                 ctype_space($this->buffer[$this->count - 1]);
         }
@@ -1300,58 +1383,119 @@ class Parser
      */
     protected function value(&$out)
     {
-        $s = $this->seek();
+        if (! isset($this->buffer[$this->count])) {
+            return false;
+        }
 
-        if ($this->literal('not', false) && $this->whitespace() && $this->value($inner)) {
-            $out = [Type::T_UNARY, 'not', $inner, $this->inParens];
+        $s = $this->count;
+        $char = $this->buffer[$this->count];
 
-            return true;
+        if ($this->literal('url(', 4) && $this->match('data:([a-z]+)\/([a-z0-9.+-]+);base64,', $m, false)) {
+            $len = strspn(
+                $this->buffer,
+                'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwyxz0123456789+/=',
+                $this->count
+            );
+
+            $this->count += $len;
+
+            if ($this->matchChar(')')) {
+                $content = substr($this->buffer, $s, $this->count - $s);
+                $out = [Type::T_KEYWORD, $content];
+
+                return true;
+            }
         }
 
         $this->seek($s);
 
-        if ($this->literal('not', false) && $this->parenValue($inner)) {
-            $out = [Type::T_UNARY, 'not', $inner, $this->inParens];
+        // not
+        if ($char === 'n' && $this->literal('not', 3, false)) {
+            if ($this->whitespace() && $this->value($inner)) {
+                $out = [Type::T_UNARY, 'not', $inner, $this->inParens];
 
-            return true;
+                return true;
+            }
+
+            $this->seek($s);
+
+            if ($this->parenValue($inner)) {
+                $out = [Type::T_UNARY, 'not', $inner, $this->inParens];
+
+                return true;
+            }
+
+            $this->seek($s);
         }
 
-        $this->seek($s);
+        // addition
+        if ($char === '+') {
+            $this->count++;
+
+            if ($this->value($inner)) {
+                $out = [Type::T_UNARY, '+', $inner, $this->inParens];
+
+                return true;
+            }
 
-        if ($this->literal('+') && $this->value($inner)) {
-            $out = [Type::T_UNARY, '+', $inner, $this->inParens];
+            $this->count--;
+
+            return false;
+        }
+
+        // negation
+        if ($char === '-') {
+            $this->count++;
+
+            if ($this->variable($inner) || $this->unit($inner) || $this->parenValue($inner)) {
+                $out = [Type::T_UNARY, '-', $inner, $this->inParens];
+
+                return true;
+            }
 
+            $this->count--;
+        }
+
+        // paren
+        if ($char === '(' && $this->parenValue($out)) {
             return true;
         }
 
-        $this->seek($s);
+        if ($char === '#') {
+            if ($this->interpolation($out) || $this->color($out)) {
+                return true;
+            }
+        }
 
-        // negation
-        if ($this->literal('-', false) &&
-            ($this->variable($inner) ||
-            $this->unit($inner) ||
-            $this->parenValue($inner))
-        ) {
-            $out = [Type::T_UNARY, '-', $inner, $this->inParens];
+        if ($this->matchChar('&', true)) {
+            $out = [Type::T_SELF];
 
             return true;
         }
 
-        $this->seek($s);
+        if ($char === '$' && $this->variable($out)) {
+            return true;
+        }
 
-        if ($this->parenValue($out) ||
-            $this->interpolation($out) ||
-            $this->variable($out) ||
-            $this->color($out) ||
-            $this->unit($out) ||
-            $this->string($out) ||
-            $this->func($out) ||
-            $this->progid($out)
-        ) {
+        if ($char === 'p' && $this->progid($out)) {
+            return true;
+        }
+
+        if (($char === '"' || $char === "'") && $this->string($out)) {
+            return true;
+        }
+
+        if ($this->unit($out)) {
             return true;
         }
 
-        if ($this->keyword($keyword)) {
+        if ($this->keyword($keyword, false)) {
+            if ($this->func($keyword, $out)) {
+                return true;
+            }
+
+            $this->whitespace();
+
             if ($keyword === 'null') {
                 $out = [Type::T_NULL];
             } else {
@@ -1373,12 +1517,12 @@ class Parser
      */
     protected function parenValue(&$out)
     {
-        $s = $this->seek();
+        $s = $this->count;
 
         $inParens = $this->inParens;
 
-        if ($this->literal('(')) {
-            if ($this->literal(')')) {
+        if ($this->matchChar('(')) {
+            if ($this->matchChar(')')) {
                 $out = [Type::T_LIST, '', []];
 
                 return true;
@@ -1386,7 +1530,7 @@ class Parser
 
             $this->inParens = true;
 
-            if ($this->expression($exp) && $this->literal(')')) {
+            if ($this->expression($exp) && $this->matchChar(')')) {
                 $out = $exp;
                 $this->inParens = $inParens;
 
@@ -1409,15 +1553,15 @@ class Parser
      */
     protected function progid(&$out)
     {
-        $s = $this->seek();
+        $s = $this->count;
 
-        if ($this->literal('progid:', false) &&
+        if ($this->literal('progid:', 7, false) &&
             $this->openString('(', $fn) &&
-            $this->literal('(')
+            $this->matchChar('(')
         ) {
             $this->openString(')', $args, '(');
 
-            if ($this->literal(')')) {
+            if ($this->matchChar(')')) {
                 $out = [Type::T_STRING, '', [
                     'progid:', $fn, '(', $args, ')'
                 ]];
@@ -1434,17 +1578,16 @@ class Parser
     /**
      * Parse function call
      *
-     * @param array $out
+     * @param string $name
+     * @param array  $func
      *
      * @return boolean
      */
-    protected function func(&$func)
+    protected function func($name, &$func)
     {
-        $s = $this->seek();
+        $s = $this->count;
 
-        if ($this->keyword($name, false) &&
-            $this->literal('(')
-        ) {
+        if ($this->matchChar('(')) {
             if ($name === 'alpha' && $this->argumentList($args)) {
                 $func = [Type::T_FUNCTION, $name, [Type::T_STRING, '', $args]];
 
@@ -1452,9 +1595,9 @@ class Parser
             }
 
             if ($name !== 'expression' && ! preg_match('/^(-[a-z]+-)?calc$/', $name)) {
-                $ss = $this->seek();
+                $ss = $this->count;
 
-                if ($this->argValues($args) && $this->literal(')')) {
+                if ($this->argValues($args) && $this->matchChar(')')) {
                     $func = [Type::T_FUNCTION_CALL, $name, $args];
 
                     return true;
@@ -1464,7 +1607,7 @@ class Parser
             }
 
             if (($this->openString(')', $str, '(') || true) &&
-                $this->literal(')')
+                $this->matchChar(')')
             ) {
                 $args = [];
 
@@ -1492,13 +1635,13 @@ class Parser
      */
     protected function argumentList(&$out)
     {
-        $s = $this->seek();
-        $this->literal('(');
+        $s = $this->count;
+        $this->matchChar('(');
 
         $args = [];
 
         while ($this->keyword($var)) {
-            if ($this->literal('=') && $this->expression($exp)) {
+            if ($this->matchChar('=') && $this->expression($exp)) {
                 $args[] = [Type::T_STRING, '', [$var . '=']];
                 $arg = $exp;
             } else {
@@ -1507,14 +1650,14 @@ class Parser
 
             $args[] = $arg;
 
-            if (! $this->literal(',')) {
+            if (! $this->matchChar(',')) {
                 break;
             }
 
             $args[] = [Type::T_STRING, '', [', ']];
         }
 
-        if (! $this->literal(')') || ! count($args)) {
+        if (! $this->matchChar(')') || ! $args) {
             $this->seek($s);
 
             return false;
@@ -1534,28 +1677,28 @@ class Parser
      */
     protected function argumentDef(&$out)
     {
-        $s = $this->seek();
-        $this->literal('(');
+        $s = $this->count;
+        $this->matchChar('(');
 
         $args = [];
 
         while ($this->variable($var)) {
             $arg = [$var[1], null, false];
 
-            $ss = $this->seek();
+            $ss = $this->count;
 
-            if ($this->literal(':') && $this->genericList($defaultVal, 'expression')) {
+            if ($this->matchChar(':') && $this->genericList($defaultVal, 'expression')) {
                 $arg[1] = $defaultVal;
             } else {
                 $this->seek($ss);
             }
 
-            $ss = $this->seek();
+            $ss = $this->count;
 
-            if ($this->literal('...')) {
-                $sss = $this->seek();
+            if ($this->literal('...', 3)) {
+                $sss = $this->count;
 
-                if (! $this->literal(')')) {
+                if (! $this->matchChar(')')) {
                     $this->throwParseError('... has to be after the final argument');
                 }
 
@@ -1567,12 +1710,12 @@ class Parser
 
             $args[] = $arg;
 
-            if (! $this->literal(',')) {
+            if (! $this->matchChar(',')) {
                 break;
             }
         }
 
-        if (! $this->literal(')')) {
+        if (! $this->matchChar(')')) {
             $this->seek($s);
 
             return false;
@@ -1592,27 +1735,27 @@ class Parser
      */
     protected function map(&$out)
     {
-        $s = $this->seek();
+        $s = $this->count;
 
-        if (! $this->literal('(')) {
+        if (! $this->matchChar('(')) {
             return false;
         }
 
         $keys = [];
         $values = [];
 
-        while ($this->genericList($key, 'expression') && $this->literal(':') &&
+        while ($this->genericList($key, 'expression') && $this->matchChar(':') &&
             $this->genericList($value, 'expression')
         ) {
             $keys[] = $key;
             $values[] = $value;
 
-            if (! $this->literal(',')) {
+            if (! $this->matchChar(',')) {
                 break;
             }
         }
 
-        if (! count($keys) || ! $this->literal(')')) {
+        if (! $keys || ! $this->matchChar(')')) {
             $this->seek($s);
 
             return false;
@@ -1633,22 +1776,48 @@ class Parser
     protected function color(&$out)
     {
         $color = [Type::T_COLOR];
+        $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;
+                    }
 
-        if ($this->match('(#([0-9a-f]{6})|#([0-9a-f]{3}))', $m)) {
-            if (isset($m[3])) {
-                $num = hexdec($m[3]);
+                    break;
 
-                foreach ([3, 2, 1] as $i) {
-                    $t = $num & 0xf;
-                    $color[$i] = $t << 4 | $t;
-                    $num >>= 4;
-                }
-            } else {
-                $num = hexdec($m[2]);
+                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;
+            }
 
-                foreach ([3, 2, 1] as $i) {
-                    $color[$i] = $num & 0xff;
-                    $num >>= 8;
+            if ($hasAlpha) {
+                if ($color[4] === 255) {
+                    $color[4] = 1; // fully opaque
+                } else {
+                    $color[4] = round($color[4] / 255, 3);
                 }
             }
 
@@ -1663,16 +1832,24 @@ class Parser
     /**
      * Parse number with unit
      *
-     * @param array $out
+     * @param array $unit
      *
      * @return boolean
      */
     protected function unit(&$unit)
     {
-        if ($this->match('([0-9]*(\.)?[0-9]+)([%a-zA-Z]+)?', $m)) {
-            $unit = new Node\Number($m[1], empty($m[3]) ? '' : $m[3]);
+        $s = $this->count;
 
-            return true;
+        if ($this->match('([0-9]*(\.)?[0-9]+)([%a-zA-Z]+)?', $m, false)) {
+            if (strlen($this->buffer) === $this->count || ! ctype_digit($this->buffer[$this->count])) {
+                $this->whitespace();
+
+                $unit = new Node\Number($m[1], empty($m[3]) ? '' : $m[3]);
+
+                return true;
+            }
+
+            $this->seek($s);
         }
 
         return false;
@@ -1687,11 +1864,11 @@ class Parser
      */
     protected function string(&$out)
     {
-        $s = $this->seek();
+        $s = $this->count;
 
-        if ($this->literal('"', false)) {
+        if ($this->matchChar('"', false)) {
             $delim = '"';
-        } elseif ($this->literal("'", false)) {
+        } elseif ($this->matchChar("'", false)) {
             $delim = "'";
         } else {
             return false;
@@ -1718,10 +1895,12 @@ class Parser
                     $content[] = '#{'; // ignore it
                 }
             } elseif ($m[2] === '\\') {
-                if ($this->literal('"', false)) {
+                if ($this->matchChar('"', false)) {
                     $content[] = $m[2] . '"';
-                } elseif ($this->literal("'", false)) {
+                } elseif ($this->matchChar("'", false)) {
                     $content[] = $m[2] . "'";
+                } elseif ($this->literal("\\", 1, false)) {
+                    $content[] = $m[2] . "\\";
                 } else {
                     $content[] = $m[2];
                 }
@@ -1733,12 +1912,14 @@ class Parser
 
         $this->eatWhiteDefault = $oldWhite;
 
-        if ($this->literal($delim)) {
+        if ($this->literal($delim, strlen($delim))) {
             if ($hasInterpolation) {
                 $delim = '"';
 
                 foreach ($content as &$string) {
-                    if ($string === "\\'") {
+                    if ($string === "\\\\") {
+                        $string = "\\";
+                    } elseif ($string === "\\'") {
                         $string = "'";
                     } elseif ($string === '\\"') {
                         $string = '"';
@@ -1786,7 +1967,7 @@ class Parser
 
         $this->eatWhiteDefault = $oldWhite;
 
-        if (count($parts) === 0) {
+        if (! $parts) {
             return false;
         }
 
@@ -1852,7 +2033,7 @@ class Parser
 
         $this->eatWhiteDefault = $oldWhite;
 
-        if (count($content) === 0) {
+        if (! $content) {
             return false;
         }
 
@@ -1879,17 +2060,22 @@ class Parser
         $oldWhite = $this->eatWhiteDefault;
         $this->eatWhiteDefault = true;
 
-        $s = $this->seek();
+        $s = $this->count;
 
-        if ($this->literal('#{') && $this->valueList($value) && $this->literal('}', false)) {
-            if ($lookWhite) {
-                $left = preg_match('/\s/', $this->buffer[$s - 1]) ? ' ' : '';
-                $right = preg_match('/\s/', $this->buffer[$this->count]) ? ' ': '';
+        if ($this->literal('#{', 2) && $this->valueList($value) && $this->matchChar('}', false)) {
+            if ($value === [Type::T_SELF]) {
+                $out = $value;
             } else {
-                $left = $right = false;
+                if ($lookWhite) {
+                    $left = preg_match('/\s/', $this->buffer[$s - 1]) ? ' ' : '';
+                    $right = preg_match('/\s/', $this->buffer[$this->count]) ? ' ': '';
+                } else {
+                    $left = $right = false;
+                }
+
+                $out = [Type::T_INTERPOLATE, $value, $left, $right];
             }
 
-            $out = [Type::T_INTERPOLATE, $value, $left, $right];
             $this->eatWhiteDefault = $oldWhite;
 
             if ($this->eatWhiteDefault) {
@@ -1900,6 +2086,7 @@ class Parser
         }
 
         $this->seek($s);
+
         $this->eatWhiteDefault = $oldWhite;
 
         return false;
@@ -1930,7 +2117,7 @@ class Parser
                 continue;
             }
 
-            if (count($parts) === 0 && $this->match('[:.#]', $m, false)) {
+            if (! $parts && $this->match('[:.#]', $m, false)) {
                 // css hacks
                 $parts[] = $m[0];
                 continue;
@@ -1941,7 +2128,7 @@ class Parser
 
         $this->eatWhiteDefault = $oldWhite;
 
-        if (count($parts) === 0) {
+        if (! $parts) {
             return false;
         }
 
@@ -1973,24 +2160,24 @@ class Parser
      *
      * @return boolean
      */
-    protected function selectors(&$out)
+    protected function selectors(&$out, $subSelector = false)
     {
-        $s = $this->seek();
+        $s = $this->count;
         $selectors = [];
 
-        while ($this->selector($sel)) {
+        while ($this->selector($sel, $subSelector)) {
             $selectors[] = $sel;
 
-            if (! $this->literal(',')) {
+            if (! $this->matchChar(',', true)) {
                 break;
             }
 
-            while ($this->literal(',')) {
+            while ($this->matchChar(',', true)) {
                 ; // ignore extra
             }
         }
 
-        if (count($selectors) === 0) {
+        if (! $selectors) {
             $this->seek($s);
 
             return false;
@@ -2008,23 +2195,23 @@ class Parser
      *
      * @return boolean
      */
-    protected function selector(&$out)
+    protected function selector(&$out, $subSelector = false)
     {
         $selector = [];
 
         for (;;) {
-            if ($this->match('[>+~]+', $m)) {
+            if ($this->match('[>+~]+', $m, true)) {
                 $selector[] = [$m[0]];
                 continue;
             }
 
-            if ($this->selectorSingle($part)) {
+            if ($this->selectorSingle($part, $subSelector)) {
                 $selector[] = $part;
                 $this->match('\s+', $m);
                 continue;
             }
 
-            if ($this->match('\/[^\/]+\/', $m)) {
+            if ($this->match('\/[^\/]+\/', $m, true)) {
                 $selector[] = [$m[0]];
                 continue;
             }
@@ -2032,11 +2219,12 @@ class Parser
             break;
         }
 
-        if (count($selector) === 0) {
+        if (! $selector) {
             return false;
         }
 
         $out = $selector;
+
         return true;
     }
 
@@ -2051,108 +2239,155 @@ class Parser
      *
      * @return boolean
      */
-    protected function selectorSingle(&$out)
+    protected function selectorSingle(&$out, $subSelector = false)
     {
         $oldWhite = $this->eatWhiteDefault;
         $this->eatWhiteDefault = false;
 
         $parts = [];
 
-        if ($this->literal('*', false)) {
+        if ($this->matchChar('*', false)) {
             $parts[] = '*';
         }
 
         for (;;) {
-            // see if we can stop early
-            if ($this->match('\s*[{,]', $m)) {
-                $this->count--;
+            if (! isset($this->buffer[$this->count])) {
                 break;
             }
 
-            $s = $this->seek();
+            $s = $this->count;
+            $char = $this->buffer[$this->count];
 
-            // self
-            if ($this->literal('&', false)) {
-                $parts[] = Compiler::$selfSelector;
-                continue;
+            // see if we can stop early
+            if ($char === '{' || $char === ',' || $char === ';' || $char === '}' || $char === '@') {
+                break;
             }
 
-            if ($this->literal('.', false)) {
-                $parts[] = '.';
-                continue;
+            // parsing a sub selector in () stop with the closing )
+            if ($subSelector && $char === ')') {
+                break;
             }
 
-            if ($this->literal('|', false)) {
-                $parts[] = '|';
-                continue;
+            //self
+            switch ($char) {
+                case '&':
+                    $parts[] = Compiler::$selfSelector;
+                    $this->count++;
+                    continue 2;
+
+                case '.':
+                    $parts[] = '.';
+                    $this->count++;
+                    continue 2;
+
+                case '|':
+                    $parts[] = '|';
+                    $this->count++;
+                    continue 2;
             }
 
-            if ($this->match('\\\\\S', $m)) {
+            if ($char === '\\' && $this->match('\\\\\S', $m)) {
                 $parts[] = $m[0];
                 continue;
             }
 
-            // for keyframes
-            if ($this->unit($unit)) {
-                $parts[] = $unit;
-                continue;
-            }
+            if ($char === '%') {
+                $this->count++;
 
-            if ($this->keyword($name)) {
-                $parts[] = $name;
-                continue;
-            }
+                if ($this->placeholder($placeholder)) {
+                    $parts[] = '%';
+                    $parts[] = $placeholder;
+                    continue;
+                }
 
-            if ($this->interpolation($inter)) {
-                $parts[] = $inter;
-                continue;
+                break;
             }
 
-            if ($this->literal('%', false) && $this->placeholder($placeholder)) {
-                $parts[] = '%';
-                $parts[] = $placeholder;
-                continue;
-            }
+            if ($char === '#') {
+                if ($this->interpolation($inter)) {
+                    $parts[] = $inter;
+                    continue;
+                }
 
-            if ($this->literal('#', false)) {
                 $parts[] = '#';
+                $this->count++;
                 continue;
             }
 
             // a pseudo selector
-            if ($this->match('::?', $m) && $this->mixedKeyword($nameParts)) {
-                $parts[] = $m[0];
-
-                foreach ($nameParts as $sub) {
-                    $parts[] = $sub;
+            if ($char === ':') {
+                if ($this->buffer[$this->count + 1] === ':') {
+                    $this->count += 2;
+                    $part = '::';
+                } else {
+                    $this->count++;
+                    $part = ':';
                 }
 
-                $ss = $this->seek();
+                if ($this->mixedKeyword($nameParts)) {
+                    $parts[] = $part;
 
-                if ($this->literal('(') &&
-                    ($this->openString(')', $str, '(') || true) &&
-                    $this->literal(')')
-                ) {
-                    $parts[] = '(';
+                    foreach ($nameParts as $sub) {
+                        $parts[] = $sub;
+                    }
 
-                    if (! empty($str)) {
-                        $parts[] = $str;
+                    $ss = $this->count;
+
+                    if ($nameParts === ['not'] || $nameParts === ['is'] ||
+                        $nameParts === ['has'] || $nameParts === ['where']
+                    ) {
+                        if ($this->matchChar('(') &&
+                          ($this->selectors($subs, true) || true) &&
+                          $this->matchChar(')')
+                        ) {
+                            $parts[] = '(';
+
+                            while ($sub = array_shift($subs)) {
+                                while ($ps = array_shift($sub)) {
+                                    foreach ($ps as &$p) {
+                                        $parts[] = $p;
+                                    }
+                                    if (count($sub) && reset($sub)) {
+                                        $parts[] = ' ';
+                                    }
+                                }
+                                if (count($subs) && reset($subs)) {
+                                    $parts[] = ', ';
+                                }
+                            }
+
+                            $parts[] = ')';
+                        } else {
+                            $this->seek($ss);
+                        }
+                    } else {
+                        if ($this->matchChar('(') &&
+                          ($this->openString(')', $str, '(') || true) &&
+                          $this->matchChar(')')
+                        ) {
+                            $parts[] = '(';
+
+                            if (! empty($str)) {
+                                $parts[] = $str;
+                            }
+
+                            $parts[] = ')';
+                        } else {
+                            $this->seek($ss);
+                        }
                     }
 
-                    $parts[] = ')';
-                } else {
-                    $this->seek($ss);
+                    continue;
                 }
-
-                continue;
             }
 
             $this->seek($s);
 
             // attribute selector
-            if ($this->literal('[') &&
-               ($this->openString(']', $str, '[') || true) &&
-               $this->literal(']')
+            if ($char === '[' &&
+                $this->matchChar('[') &&
+                ($this->openString(']', $str, '[') || true) &&
+                $this->matchChar(']')
             ) {
                 $parts[] = '[';
 
@@ -2161,18 +2396,28 @@ class Parser
                 }
 
                 $parts[] = ']';
-
                 continue;
             }
 
             $this->seek($s);
 
+            // for keyframes
+            if ($this->unit($unit)) {
+                $parts[] = $unit;
+                continue;
+            }
+
+            if ($this->keyword($name)) {
+                $parts[] = $name;
+                continue;
+            }
+
             break;
         }
 
         $this->eatWhiteDefault = $oldWhite;
 
-        if (count($parts) === 0) {
+        if (! $parts) {
             return false;
         }
 
@@ -2190,9 +2435,9 @@ class Parser
      */
     protected function variable(&$out)
     {
-        $s = $this->seek();
+        $s = $this->count;
 
-        if ($this->literal('$', false) && $this->keyword($name)) {
+        if ($this->matchChar('$', false) && $this->keyword($name)) {
             $out = [Type::T_VARIABLE, $name];
 
             return true;
@@ -2239,14 +2484,17 @@ class Parser
     {
         if ($this->match(
             $this->utf8
-                ? '([\pL\w\-_]+|#[{][$][\pL\w\-_]+[}])'
-                : '([\w\-_]+|#[{][$][\w\-_]+[}])',
+                ? '([\pL\w\-_]+)'
+                : '([\w\-_]+)',
             $m
         )) {
             $placeholder = $m[1];
 
             return true;
         }
+        if ($this->interpolation($placeholder)) {
+            return true;
+        }
 
         return false;
     }
@@ -2276,7 +2524,7 @@ class Parser
      */
     protected function end()
     {
-        if ($this->literal(';')) {
+        if ($this->matchChar(';')) {
             return true;
         }
 
@@ -2438,7 +2686,7 @@ class Parser
      *
      * @param integer $pos
      *
-     * @return integer
+     * @return array
      */
     private function getSourcePosition($pos)
     {
diff --git a/lib/scssphp/SourceMap/Base64.php b/lib/scssphp/SourceMap/Base64.php
new file mode 100644 (file)
index 0000000..54015bb
--- /dev/null
@@ -0,0 +1,184 @@
+<?php
+/**
+ * SCSSPHP
+ *
+ * @copyright 2012-2015 Leaf Corcoran
+ *
+ * @license http://opensource.org/licenses/MIT MIT
+ *
+ * @link http://leafo.github.io/scssphp
+ */
+
+namespace Leafo\ScssPhp\SourceMap;
+
+/**
+ * Base 64 Encode/Decode
+ *
+ * @author Anthon Pang <anthon.pang@gmail.com>
+ */
+class Base64
+{
+    /**
+     * @var array
+     */
+    private static $encodingMap = [
+        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 => '/',
+    ];
+
+    /**
+     * @var array
+     */
+    private static $decodingMap = [
+        '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,
+    ];
+
+    /**
+     * Convert to base64
+     *
+     * @param integer $value
+     *
+     * @return string
+     */
+    public static function encode($value)
+    {
+        return self::$encodingMap[$value];
+    }
+
+    /**
+     * Convert from base64
+     *
+     * @param string $value
+     *
+     * @return integer
+     */
+    public static function decode($value)
+    {
+        return self::$decodingMap[$value];
+    }
+}
diff --git a/lib/scssphp/SourceMap/Base64VLQ.php b/lib/scssphp/SourceMap/Base64VLQ.php
new file mode 100644 (file)
index 0000000..6de3b5e
--- /dev/null
@@ -0,0 +1,137 @@
+<?php
+/**
+ * SCSSPHP
+ *
+ * @copyright 2012-2015 Leaf Corcoran
+ *
+ * @license http://opensource.org/licenses/MIT MIT
+ *
+ * @link http://leafo.github.io/scssphp
+ */
+
+namespace Leafo\ScssPhp\SourceMap;
+
+use Leafo\ScssPhp\SourceMap\Base64;
+
+/**
+ * Base 64 VLQ
+ *
+ * Based on the Base 64 VLQ implementation in Closure Compiler:
+ * https://github.com/google/closure-compiler/blob/master/src/com/google/debugging/sourcemap/Base64VLQ.java
+ *
+ * Copyright 2011 The Closure Compiler Authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ * @author John Lenz <johnlenz@google.com>
+ * @author Anthon Pang <anthon.pang@gmail.com>
+ */
+class Base64VLQ
+{
+    // A Base64 VLQ digit can represent 5 bits, so it is base-32.
+    const VLQ_BASE_SHIFT = 5;
+
+    // A mask of bits for a VLQ digit (11111), 31 decimal.
+    const VLQ_BASE_MASK = 31;
+
+    // The continuation bit is the 6th bit.
+    const VLQ_CONTINUATION_BIT = 32;
+
+    /**
+     * Returns the VLQ encoded value.
+     *
+     * @param integer $value
+     *
+     * @return string
+     */
+    public static function encode($value)
+    {
+        $encoded = '';
+        $vlq = self::toVLQSigned($value);
+
+        do {
+            $digit = $vlq & self::VLQ_BASE_MASK;
+            $vlq >>= self::VLQ_BASE_SHIFT;
+
+            if ($vlq > 0) {
+                $digit |= self::VLQ_CONTINUATION_BIT;
+            }
+
+            $encoded .= Base64::encode($digit);
+        } while ($vlq > 0);
+
+        return $encoded;
+    }
+
+    /**
+     * Decodes VLQValue.
+     *
+     * @param string $str
+     * @param integer $index
+     *
+     * @return integer
+     */
+    public static function decode($str, &$index)
+    {
+        $result = 0;
+        $shift = 0;
+
+        do {
+            $c = $str[$index++];
+            $digit = Base64::decode($c);
+            $continuation = ($digit & self::VLQ_CONTINUATION_BIT) != 0;
+            $digit &= self::VLQ_BASE_MASK;
+            $result = $result + ($digit << $shift);
+            $shift = $shift + self::VLQ_BASE_SHIFT;
+        } while ($continuation);
+
+        return self::fromVLQSigned($result);
+    }
+
+    /**
+     * Converts 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)
+     *
+     * @param integer $value
+     *
+     * @return integer
+     */
+    private static function toVLQSigned($value)
+    {
+        if ($value < 0) {
+            return ((-$value) << 1) + 1;
+        }
+
+        return ($value << 1) + 0;
+    }
+
+    /**
+     * Converts 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
+     *
+     * @param integer $value
+     *
+     * @return integer
+     */
+    private static function fromVLQSigned($value)
+    {
+        $negate = ($value & 1) === 1;
+        $value = $value >> 1;
+
+        return $negate ? -$value : $value;
+    }
+}
index 1189ce0..caf56c7 100644 (file)
@@ -47,7 +47,7 @@ class Base64VLQEncoder
      *
      * @var array
      */
-    private $charToIntMap = 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,
@@ -56,14 +56,14 @@ class Base64VLQEncoder
         '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 = 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',
@@ -72,7 +72,7 @@ class Base64VLQEncoder
         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
index 70b47cd..fb11a0b 100644 (file)
@@ -33,7 +33,7 @@ class SourceMapGenerator
      *
      * @var array
      */
-    protected $defaultOptions = array(
+    protected $defaultOptions = [
         // an optional source root, useful for relocating source files
         // on a server or removing repeated values in the 'sources' entry.
         // This value is prepended to the individual entries in the 'source' field.
@@ -56,12 +56,12 @@ class SourceMapGenerator
 
         // base path for filename normalization
         'sourceMapBasepath' => ''
-    );
+    ];
 
     /**
      * The base64 VLQ encoder
      *
-     * @var \Leafo\ScssPhp\SourceMap\Base64VLQEncoder
+     * @var \Leafo\ScssPhp\SourceMap\Base64VLQ
      */
     protected $encoder;
 
@@ -70,22 +70,22 @@ class SourceMapGenerator
      *
      * @var array
      */
-    protected $mappings = array();
+    protected $mappings = [];
 
     /**
      * Array of contents map
      *
      * @var array
      */
-    protected $contentsMap = array();
+    protected $contentsMap = [];
 
     /**
      * File to content map
      *
      * @var array
      */
-    protected $sources = array();
-    protected $source_keys = array();
+    protected $sources = [];
+    protected $sourceKeys = [];
 
     /**
      * @var array
@@ -95,7 +95,7 @@ class SourceMapGenerator
     public function __construct(array $options = [])
     {
         $this->options = array_merge($this->defaultOptions, $options);
-        $this->encoder = new Base64VLQEncoder();
+        $this->encoder = new Base64VLQ();
     }
 
     /**
@@ -109,13 +109,13 @@ class SourceMapGenerator
      */
     public function addMapping($generatedLine, $generatedColumn, $originalLine, $originalColumn, $sourceFile)
     {
-        $this->mappings[] = array(
-            'generated_line' => $generatedLine,
+        $this->mappings[] = [
+            'generated_line'   => $generatedLine,
             'generated_column' => $generatedColumn,
-            'original_line' => $originalLine,
-            'original_column' => $originalColumn,
-            'source_file' => $sourceFile
-        );
+            'original_line'    => $originalLine,
+            'original_column'  => $originalColumn,
+            'source_file'      => $sourceFile
+        ];
 
         $this->sources[$sourceFile] = $sourceFile;
     }
@@ -123,9 +123,10 @@ class SourceMapGenerator
     /**
      * Saves the source map to a file
      *
-     * @param string $file    The absolute path to a file
      * @param string $content The content to write
      *
+     * @return string
+     *
      * @throws \Leafo\ScssPhp\Exception\CompilerException If the file could not be saved
      */
     public function saveMap($content)
@@ -136,7 +137,9 @@ class SourceMapGenerator
         // directory does not exist
         if (! is_dir($dir)) {
             // FIXME: create the dir automatically?
-            throw new CompilerException(sprintf('The directory "%s" does not exist. Cannot save the source map.', $dir));
+            throw new CompilerException(
+                sprintf('The directory "%s" does not exist. Cannot save the source map.', $dir)
+            );
         }
 
         // FIXME: proper saving, with dir write check!
@@ -156,7 +159,7 @@ class SourceMapGenerator
      */
     public function generateJson()
     {
-        $sourceMap = array();
+        $sourceMap = [];
         $mappings  = $this->generateMappings();
 
         // File version (always the first entry in the object) and must be a positive integer.
@@ -178,14 +181,14 @@ class SourceMapGenerator
         }
 
         // A list of original sources used by the 'mappings' entry.
-        $sourceMap['sources'] = array();
+        $sourceMap['sources'] = [];
 
-        foreach ($this->sources as $source_uri => $source_filename) {
-            $sourceMap['sources'][] = $this->normalizeFilename($source_filename);
+        foreach ($this->sources as $sourceUri => $sourceFilename) {
+            $sourceMap['sources'][] = $this->normalizeFilename($sourceFilename);
         }
 
         // A list of symbol names used by the 'mappings' entry.
-        $sourceMap['names'] = array();
+        $sourceMap['names'] = [];
 
         // A string with the encoded mapping data.
         $sourceMap['mappings'] = $mappings;
@@ -202,7 +205,7 @@ class SourceMapGenerator
             unset($sourceMap['sourceRoot']);
         }
 
-        return json_encode($sourceMap);
+        return json_encode($sourceMap, JSON_UNESCAPED_SLASHES);
     }
 
     /**
@@ -216,7 +219,7 @@ class SourceMapGenerator
             return null;
         }
 
-        $content = array();
+        $content = [];
 
         foreach ($this->sources as $sourceFile) {
             $content[] = file_get_contents($sourceFile);
@@ -236,10 +239,10 @@ class SourceMapGenerator
             return '';
         }
 
-        $this->source_keys = array_flip(array_keys($this->sources));
+        $this->sourceKeys = array_flip(array_keys($this->sources));
 
         // group mappings by generated line number.
-        $groupedMap = $groupedMapEncoded = array();
+        $groupedMap = $groupedMapEncoded = [];
 
         foreach ($this->mappings as $m) {
             $groupedMap[$m['generated_line']][] = $m;
@@ -248,15 +251,15 @@ class SourceMapGenerator
         ksort($groupedMap);
         $lastGeneratedLine = $lastOriginalIndex = $lastOriginalLine = $lastOriginalColumn = 0;
 
-        foreach ($groupedMap as $lineNumber => $line_map) {
+        foreach ($groupedMap as $lineNumber => $lineMap) {
             while (++$lastGeneratedLine < $lineNumber) {
                 $groupedMapEncoded[] = ';';
             }
 
-            $lineMapEncoded = array();
+            $lineMapEncoded = [];
             $lastGeneratedColumn = 0;
 
-            foreach ($line_map as $m) {
+            foreach ($lineMap as $m) {
                 $mapEncoded = $this->encoder->encode($m['generated_column'] - $lastGeneratedColumn);
                 $lastGeneratedColumn = $m['generated_column'];
 
@@ -293,9 +296,16 @@ class SourceMapGenerator
      */
     protected function findFileIndex($filename)
     {
-        return $this->source_keys[$filename];
+        return $this->sourceKeys[$filename];
     }
 
+    /**
+     * Normalize filename
+     *
+     * @param string $filename
+     *
+     * @return string
+     */
     protected function normalizeFilename($filename)
     {
         $filename = $this->fixWindowsPath($filename);
@@ -303,7 +313,7 @@ class SourceMapGenerator
         $basePath = $this->options['sourceMapBasepath'];
 
         // "Trim" the 'sourceMapBasepath' from the output filename.
-        if (strpos($filename, $basePath) === 0) {
+        if (strlen($basePath) && strpos($filename, $basePath) === 0) {
             $filename = substr($filename, strlen($basePath));
         }
 
index 7526e02..a1e5065 100644 (file)
@@ -63,7 +63,7 @@ class Util
      */
     public static function encodeURIComponent($string)
     {
-        $revert = array('%21' => '!', '%2A' => '*', '%27' => "'", '%28' => '(', '%29' => ')');
+        $revert = ['%21' => '!', '%2A' => '*', '%27' => "'", '%28' => '(', '%29' => ')'];
 
         return strtr(rawurlencode($string), $revert);
     }
index 3b8dcd0..46ffedf 100644 (file)
@@ -18,5 +18,5 @@ namespace Leafo\ScssPhp;
  */
 class Version
 {
-    const VERSION = 'v0.7.5';
+    const VERSION = 'v0.8.3';
 }
index 8623c2e..1b19dd3 100644 (file)
     <location>scssphp</location>
     <name>scssphp</name>
     <license>MIT</license>
-    <version>0.7.5</version>
+    <version>0.8.3</version>
   </library>
   <library>
     <location>spout</location>