Update to scssphp/scssphp 1.3
authorTim Düsterhus <duesterhus@woltlab.com>
Tue, 3 Nov 2020 14:44:50 +0000 (15:44 +0100)
committerTim Düsterhus <duesterhus@woltlab.com>
Tue, 3 Nov 2020 14:44:50 +0000 (15:44 +0100)
38 files changed:
wcfsetup/install/files/lib/system/api/composer.json
wcfsetup/install/files/lib/system/api/composer.lock
wcfsetup/install/files/lib/system/api/composer/ClassLoader.php
wcfsetup/install/files/lib/system/api/composer/installed.json
wcfsetup/install/files/lib/system/api/scssphp/scssphp/README.md
wcfsetup/install/files/lib/system/api/scssphp/scssphp/bin/pscss
wcfsetup/install/files/lib/system/api/scssphp/scssphp/composer.json
wcfsetup/install/files/lib/system/api/scssphp/scssphp/phpcs.xml.dist [new file with mode: 0644]
wcfsetup/install/files/lib/system/api/scssphp/scssphp/scss.inc.php
wcfsetup/install/files/lib/system/api/scssphp/scssphp/src/Base/Range.php
wcfsetup/install/files/lib/system/api/scssphp/scssphp/src/Block.php
wcfsetup/install/files/lib/system/api/scssphp/scssphp/src/Cache.php
wcfsetup/install/files/lib/system/api/scssphp/scssphp/src/Colors.php
wcfsetup/install/files/lib/system/api/scssphp/scssphp/src/Compiler.php
wcfsetup/install/files/lib/system/api/scssphp/scssphp/src/Compiler/Environment.php
wcfsetup/install/files/lib/system/api/scssphp/scssphp/src/Exception/CompilerException.php
wcfsetup/install/files/lib/system/api/scssphp/scssphp/src/Exception/ParserException.php
wcfsetup/install/files/lib/system/api/scssphp/scssphp/src/Exception/RangeException.php
wcfsetup/install/files/lib/system/api/scssphp/scssphp/src/Exception/SassException.php [new file with mode: 0644]
wcfsetup/install/files/lib/system/api/scssphp/scssphp/src/Exception/SassScriptException.php [new file with mode: 0644]
wcfsetup/install/files/lib/system/api/scssphp/scssphp/src/Exception/ServerException.php
wcfsetup/install/files/lib/system/api/scssphp/scssphp/src/Formatter.php
wcfsetup/install/files/lib/system/api/scssphp/scssphp/src/Formatter/Compact.php
wcfsetup/install/files/lib/system/api/scssphp/scssphp/src/Formatter/Compressed.php
wcfsetup/install/files/lib/system/api/scssphp/scssphp/src/Formatter/Crunched.php
wcfsetup/install/files/lib/system/api/scssphp/scssphp/src/Formatter/Debug.php
wcfsetup/install/files/lib/system/api/scssphp/scssphp/src/Formatter/Expanded.php
wcfsetup/install/files/lib/system/api/scssphp/scssphp/src/Formatter/Nested.php
wcfsetup/install/files/lib/system/api/scssphp/scssphp/src/Formatter/OutputBlock.php
wcfsetup/install/files/lib/system/api/scssphp/scssphp/src/Node.php
wcfsetup/install/files/lib/system/api/scssphp/scssphp/src/Node/Number.php
wcfsetup/install/files/lib/system/api/scssphp/scssphp/src/Parser.php
wcfsetup/install/files/lib/system/api/scssphp/scssphp/src/SourceMap/Base64.php
wcfsetup/install/files/lib/system/api/scssphp/scssphp/src/SourceMap/Base64VLQ.php
wcfsetup/install/files/lib/system/api/scssphp/scssphp/src/SourceMap/SourceMapGenerator.php
wcfsetup/install/files/lib/system/api/scssphp/scssphp/src/Type.php
wcfsetup/install/files/lib/system/api/scssphp/scssphp/src/Util.php
wcfsetup/install/files/lib/system/api/scssphp/scssphp/src/Version.php

index 0ea471ca44b64a5d6f5d14bbf5824a118fe6b58a..243f7c55cfe5350260d4f1ae88d6b2887799c8ff 100644 (file)
@@ -14,7 +14,7 @@
         "chrisjean/php-ico": "1.0.*",
         "true/punycode": "~2.0",
         "pear/net_idna2": "^0.2.0",
-        "scssphp/scssphp": "^1.1",
+        "scssphp/scssphp": "^1.3",
         "guzzlehttp/guzzle": "dev-6.5-windows"
     },
     "repositories": [
index 86b3664373f24331c47658c9ced69f610d789680..399e67d366c32fce294b27d11c0ab856cd224cbe 100644 (file)
@@ -4,7 +4,7 @@
         "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
         "This file is @generated automatically"
     ],
-    "content-hash": "84035248a681a0748790233bd2a4189f",
+    "content-hash": "596e3570b2867b29d0b01279d2e862d6",
     "packages": [
         {
             "name": "chrisjean/php-ico",
         },
         {
             "name": "scssphp/scssphp",
-            "version": "1.1.1",
+            "version": "1.3",
             "source": {
                 "type": "git",
                 "url": "https://github.com/scssphp/scssphp.git",
-                "reference": "824e4cec10b2bfa88eec5dac23991cb9106622c1"
+                "reference": "261cd018025d5790e135a1e5b694d6af186e6bca"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/scssphp/scssphp/zipball/824e4cec10b2bfa88eec5dac23991cb9106622c1",
-                "reference": "824e4cec10b2bfa88eec5dac23991cb9106622c1",
+                "url": "https://api.github.com/repos/scssphp/scssphp/zipball/261cd018025d5790e135a1e5b694d6af186e6bca",
+                "reference": "261cd018025d5790e135a1e5b694d6af186e6bca",
                 "shasum": ""
             },
             "require": {
                 "php": ">=5.6.0"
             },
             "require-dev": {
-                "phpunit/phpunit": "^5.7 || ^6.5 || ^7.5 || ^8.3",
+                "phpunit/phpunit": "^5.7 || ^6.5 || ^7.5 || ^8.3 || ^9.4",
+                "sass/sass-spec": "2020.08.10",
                 "squizlabs/php_codesniffer": "~3.5",
                 "twbs/bootstrap": "~4.3",
                 "zurb/foundation": "~6.5"
                 "scss",
                 "stylesheet"
             ],
-            "time": "2020-06-04T17:30:40+00:00"
+            "time": "2020-10-29T11:09:57+00:00"
         },
         {
             "name": "symfony/css-selector",
index fce8549f0781bafdc7da2301b84d048286757445..03b9bb9c40cb86c2c2bbec2ce6ff0ddce9ad586c 100644 (file)
@@ -60,7 +60,7 @@ class ClassLoader
     public function getPrefixes()
     {
         if (!empty($this->prefixesPsr0)) {
-            return call_user_func_array('array_merge', $this->prefixesPsr0);
+            return call_user_func_array('array_merge', array_values($this->prefixesPsr0));
         }
 
         return array();
index 9b678851b0529c933670709d24ca5fdc00ac6315..885dba7ccbea4040d1a9633603cafc085fd9a5de 100644 (file)
     },
     {
         "name": "scssphp/scssphp",
-        "version": "1.1.1",
-        "version_normalized": "1.1.1.0",
+        "version": "1.3",
+        "version_normalized": "1.3.0.0",
         "source": {
             "type": "git",
             "url": "https://github.com/scssphp/scssphp.git",
-            "reference": "824e4cec10b2bfa88eec5dac23991cb9106622c1"
+            "reference": "261cd018025d5790e135a1e5b694d6af186e6bca"
         },
         "dist": {
             "type": "zip",
-            "url": "https://api.github.com/repos/scssphp/scssphp/zipball/824e4cec10b2bfa88eec5dac23991cb9106622c1",
-            "reference": "824e4cec10b2bfa88eec5dac23991cb9106622c1",
+            "url": "https://api.github.com/repos/scssphp/scssphp/zipball/261cd018025d5790e135a1e5b694d6af186e6bca",
+            "reference": "261cd018025d5790e135a1e5b694d6af186e6bca",
             "shasum": ""
         },
         "require": {
             "php": ">=5.6.0"
         },
         "require-dev": {
-            "phpunit/phpunit": "^5.7 || ^6.5 || ^7.5 || ^8.3",
+            "phpunit/phpunit": "^5.7 || ^6.5 || ^7.5 || ^8.3 || ^9.4",
+            "sass/sass-spec": "2020.08.10",
             "squizlabs/php_codesniffer": "~3.5",
             "twbs/bootstrap": "~4.3",
             "zurb/foundation": "~6.5"
         },
-        "time": "2020-06-04T17:30:40+00:00",
+        "time": "2020-10-29T11:09:57+00:00",
         "bin": [
             "bin/pscss"
         ],
index 86eba25cadb447e6778cb613c25c1253212443a2..0b485195e3fedc730721fd4dd73b27112c952bd5 100644 (file)
@@ -1,12 +1,12 @@
 # scssphp
-### <http://scssphp.github.io/scssphp>
+### <https://scssphp.github.io/scssphp>
 
-[![Build](https://travis-ci.org/scssphp/scssphp.svg?branch=master)](http://travis-ci.org/scssphp/scssphp)
+[![Build](https://travis-ci.com/scssphp/scssphp.svg?branch=master)](https://travis-ci.com/scssphp/scssphp)
 [![License](https://poser.pugx.org/scssphp/scssphp/license)](https://packagist.org/packages/scssphp/scssphp)
 
 `scssphp` is a compiler for SCSS written in PHP.
 
-Checkout the homepage, <http://scssphp.github.io/scssphp>, for directions on how to use.
+Checkout the homepage, <https://scssphp.github.io/scssphp>, for directions on how to use.
 
 ## Running Tests
 
@@ -39,8 +39,8 @@ To enable the `scss` compatibility tests:
 
 ## Coding Standard
 
-`scssphp` source conforms to [PSR2](http://www.php-fig.org/psr/psr-2/).
+`scssphp` source conforms to [PSR12](https://www.php-fig.org/psr/psr-12/).
 
 Run the following command from the root directory to check the code for "sniffs".
 
-    vendor/bin/phpcs --standard=PSR2 bin src tests
+    vendor/bin/phpcs --standard=PSR12 --extensions=php bin src tests *.php
index 56bf48fdc6d69d5d917fc13a39dd4b842e46b2c0..b3c8a735d6e6196778b934fcb255a81d7130a5c9 100644 (file)
@@ -1,5 +1,6 @@
 #!/usr/bin/env php
 <?php
+
 /**
  * SCSSPHP
  *
@@ -24,13 +25,9 @@ use ScssPhp\ScssPhp\Version;
 
 $style = null;
 $loadPaths = null;
-$precision = null;
 $dumpTree = false;
 $inputFile = null;
 $changeDir = false;
-$debugInfo = false;
-$lineNumbers = false;
-$ignoreErrors = false;
 $encoding = false;
 $sourceMap = false;
 
@@ -71,13 +68,13 @@ Usage: $exe [options] [input-file]
 Options include:
 
     --help              Show this message [-h, -?]
-    --continue-on-error Continue compilation (as best as possible) when error encountered
-    --debug-info        Annotate selectors with CSS referring to the source file and line number [-g]
+    --continue-on-error [deprecated] Ignored
+    --debug-info        [deprecated] Ignored [-g]
     --dump-tree         Dump formatted parse tree [-T]
     --iso8859-1         Use iso8859-1 encoding instead of default utf-8
-    --line-numbers      Annotate selectors with comments referring to the source file and line number [--line-comments]
+    --line-numbers      [deprecated] Ignored [--line-comments]
     --load-path=PATH    Set import path [-I]
-    --precision=N       Set decimal number precision (default 10) [-p]
+    --precision=N       [deprecated] Ignored. (default 10) [-p]
     --sourcemap         Create source map file
     --style=FORMAT      Set the output format (compact, compressed, crunched, expanded, or nested) [-s, -t]
     --version           Print the version [-v]
@@ -90,13 +87,15 @@ EOT;
         exit(Version::VERSION . "\n");
     }
 
+    // Keep parsing --continue-on-error to avoid BC breaks for scripts using it
     if ($argv[$i] === '--continue-on-error') {
-        $ignoreErrors = true;
+        // TODO report it as a warning ?
         continue;
     }
 
+    // Keep parsing it to avoid BC breaks for scripts using it
     if ($argv[$i] === '-g' || $argv[$i] === '--debug-info') {
-        $debugInfo = true;
+        // TODO report it as a warning ?
         continue;
     }
 
@@ -105,8 +104,9 @@ EOT;
         continue;
     }
 
+    // Keep parsing it to avoid BC breaks for scripts using it
     if ($argv[$i] === '--line-numbers' || $argv[$i] === '--line-comments') {
-        $lineNumbers = true;
+        // TODO report it as a warning ?
         continue;
     }
 
@@ -134,10 +134,11 @@ EOT;
         continue;
     }
 
+    // Keep parsing --precision to avoid BC breaks for scripts using it
     $value = parseArgument($i, array('-p', '--precision'));
 
     if (isset($value)) {
-        $precision = $value;
+        // TODO report it as a warning ?
         continue;
     }
 
@@ -176,26 +177,10 @@ if ($dumpTree) {
 
 $scss = new Compiler();
 
-if ($debugInfo) {
-    $scss->setLineNumberStyle(Compiler::DEBUG_INFO);
-}
-
-if ($lineNumbers) {
-    $scss->setLineNumberStyle(Compiler::LINE_COMMENTS);
-}
-
-if ($ignoreErrors) {
-    $scss->setIgnoreErrors($ignoreErrors);
-}
-
 if ($loadPaths) {
     $scss->setImportPaths(explode(PATH_SEPARATOR, $loadPaths));
 }
 
-if ($precision) {
-    $scss->setNumberPrecision($precision);
-}
-
 if ($style) {
     $scss->setFormatter('ScssPhp\\ScssPhp\\Formatter\\' . ucfirst($style));
 }
index 43f55653707bab4b32dc1c939917d332339725ee..6ac13125a8626b8cdf01bc93f3ef805592b97970 100644 (file)
         "ext-ctype": "*"
     },
     "require-dev": {
+        "phpunit/phpunit": "^5.7 || ^6.5 || ^7.5 || ^8.3 || ^9.4",
+        "sass/sass-spec": "2020.08.10",
         "squizlabs/php_codesniffer": "~3.5",
-        "phpunit/phpunit": "^5.7 || ^6.5 || ^7.5 || ^8.3",
         "twbs/bootstrap": "~4.3",
         "zurb/foundation": "~6.5"
     },
-    "minimum-stability": "dev",
+    "repositories": [
+        {
+            "type": "package",
+            "package": {
+                "name": "sass/sass-spec",
+                "version": "2020.08.10",
+                "source": {
+                    "type": "git",
+                    "url": "https://github.com/sass/sass-spec.git",
+                    "reference": "73222792c42a516d62e2e25c3f5d9e35f5567030"
+                },
+                "dist": {
+                    "type": "zip",
+                    "url": "https://api.github.com/repos/sass/sass-spec/zipball/73222792c42a516d62e2e25c3f5d9e35f5567030",
+                    "reference": "73222792c42a516d62e2e25c3f5d9e35f5567030",
+                    "shasum": ""
+                }
+            }
+        }
+    ],
     "bin": ["bin/pscss"],
-    "archive": {
-        "exclude": [
-            "/Makefile",
-            "/.gitattributes",
-            "/.gitignore",
-            "/.travis.yml",
-            "/phpunit.xml.dist",
-            "/tests"
-        ]
+    "config": {
+        "sort-packages": true
     }
 }
diff --git a/wcfsetup/install/files/lib/system/api/scssphp/scssphp/phpcs.xml.dist b/wcfsetup/install/files/lib/system/api/scssphp/scssphp/phpcs.xml.dist
new file mode 100644 (file)
index 0000000..b162dbd
--- /dev/null
@@ -0,0 +1,12 @@
+<?xml version="1.0"?>
+<ruleset name="PSR12 (adapted for PHP 5.6+)">
+    <rule ref="PSR12">
+        <!-- Ignore this PHP 7.1+ sniff as long as we support PHP 5.6+ -->
+        <exclude name="PSR12.Properties.ConstantVisibility.NotFound"/>
+
+        <!-- This sniff doesn't ignore comment blocks -->
+<!--
+        <exclude name="Generic.Files.LineLength"/>
+-->
+    </rule>
+</ruleset>
index e4ec7f181597054aaa9375b248f64aeb8aa4940b..96532aa6f49d04f34fde236195ea130c939ed1dd 100644 (file)
@@ -1,4 +1,5 @@
 <?php
+
 if (version_compare(PHP_VERSION, '5.6') < 0) {
     throw new \Exception('scssphp requires PHP 5.6 or above');
 }
@@ -10,6 +11,8 @@ if (! class_exists('ScssPhp\ScssPhp\Version', false)) {
     include_once __DIR__ . '/src/Colors.php';
     include_once __DIR__ . '/src/Compiler.php';
     include_once __DIR__ . '/src/Compiler/Environment.php';
+    include_once __DIR__ . '/src/Exception/SassException.php';
+    include_once __DIR__ . '/src/Exception/SassScriptException.php';
     include_once __DIR__ . '/src/Exception/CompilerException.php';
     include_once __DIR__ . '/src/Exception/ParserException.php';
     include_once __DIR__ . '/src/Exception/RangeException.php';
index 91a2d74866870422dcf602f8014d8ecb624d2759..02d47ac8a7777a3f80f7b197f8516bf869c81c4b 100644 (file)
@@ -1,4 +1,5 @@
 <?php
+
 /**
  * SCSSPHP
  *
@@ -96,12 +97,14 @@ class Cache
     {
         $fileCache = self::$cacheDir . self::cacheName($operation, $what, $options);
 
-        if (((self::$forceRefresh === false) || (self::$forceRefresh === 'once' &&
+        if (
+            ((self::$forceRefresh === false) || (self::$forceRefresh === 'once' &&
             isset(self::$refreshed[$fileCache]))) && file_exists($fileCache)
         ) {
             $cacheTime = filemtime($fileCache);
 
-            if ((\is_null($lastModified) || $cacheTime > $lastModified) &&
+            if (
+                (\is_null($lastModified) || $cacheTime > $lastModified) &&
                 $cacheTime + self::$gcLifetime > time()
             ) {
                 $c = file_get_contents($fileCache);
@@ -177,9 +180,7 @@ class Cache
         self::$cacheDir = rtrim(self::$cacheDir, '/') . '/';
 
         if (! is_dir(self::$cacheDir)) {
-            if (! mkdir(self::$cacheDir)) {
-                throw new Exception('Cache directory couldn\'t be created: ' . self::$cacheDir);
-            }
+            throw new Exception('Cache directory doesn\'t exist: ' . self::$cacheDir);
         }
 
         if (! is_writable(self::$cacheDir)) {
index c3ca1bfb2083d2b92cbc2245c3db7f4361a16dd4..4b62c361c52f79ac398286f0f63262cb04b51718 100644 (file)
@@ -1,4 +1,5 @@
 <?php
+
 /**
  * SCSSPHP
  *
@@ -28,6 +29,7 @@ class Colors
     protected static $cssColors = [
         'aliceblue' => '240,248,255',
         'antiquewhite' => '250,235,215',
+        'cyan' => '0,255,255',
         'aqua' => '0,255,255',
         'aquamarine' => '127,255,212',
         'azure' => '240,255,255',
@@ -46,13 +48,12 @@ class Colors
         'cornflowerblue' => '100,149,237',
         'cornsilk' => '255,248,220',
         'crimson' => '220,20,60',
-        'cyan' => '0,255,255',
         'darkblue' => '0,0,139',
         'darkcyan' => '0,139,139',
         'darkgoldenrod' => '184,134,11',
         'darkgray' => '169,169,169',
-        'darkgreen' => '0,100,0',
         'darkgrey' => '169,169,169',
+        'darkgreen' => '0,100,0',
         'darkkhaki' => '189,183,107',
         'darkmagenta' => '139,0,139',
         'darkolivegreen' => '85,107,47',
@@ -74,15 +75,16 @@ class Colors
         'firebrick' => '178,34,34',
         'floralwhite' => '255,250,240',
         'forestgreen' => '34,139,34',
+        'magenta' => '255,0,255',
         'fuchsia' => '255,0,255',
         'gainsboro' => '220,220,220',
         'ghostwhite' => '248,248,255',
         'gold' => '255,215,0',
         'goldenrod' => '218,165,32',
         'gray' => '128,128,128',
+        'grey' => '128,128,128',
         'green' => '0,128,0',
         'greenyellow' => '173,255,47',
-        'grey' => '128,128,128',
         'honeydew' => '240,255,240',
         'hotpink' => '255,105,180',
         'indianred' => '205,92,92',
@@ -98,8 +100,8 @@ class Colors
         'lightcyan' => '224,255,255',
         'lightgoldenrodyellow' => '250,250,210',
         'lightgray' => '211,211,211',
-        'lightgreen' => '144,238,144',
         'lightgrey' => '211,211,211',
+        'lightgreen' => '144,238,144',
         'lightpink' => '255,182,193',
         'lightsalmon' => '255,160,122',
         'lightseagreen' => '32,178,170',
@@ -111,7 +113,6 @@ class Colors
         'lime' => '0,255,0',
         'limegreen' => '50,205,50',
         'linen' => '250,240,230',
-        'magenta' => '255,0,255',
         'maroon' => '128,0,0',
         'mediumaquamarine' => '102,205,170',
         'mediumblue' => '0,0,205',
@@ -145,7 +146,6 @@ class Colors
         'plum' => '221,160,221',
         'powderblue' => '176,224,230',
         'purple' => '128,0,128',
-        'rebeccapurple' => '102,51,153',
         'red' => '255,0,0',
         'rosybrown' => '188,143,143',
         'royalblue' => '65,105,225',
@@ -167,7 +167,6 @@ class Colors
         'teal' => '0,128,128',
         'thistle' => '216,191,216',
         'tomato' => '255,99,71',
-        'transparent' => '0,0,0,0',
         'turquoise' => '64,224,208',
         'violet' => '238,130,238',
         'wheat' => '245,222,179',
@@ -175,6 +174,8 @@ class Colors
         'whitesmoke' => '245,245,245',
         'yellow' => '255,255,0',
         'yellowgreen' => '154,205,50',
+        'rebeccapurple' => '102,51,153',
+        'transparent' => '0,0,0,0',
     ];
 
     /**
@@ -226,7 +227,10 @@ class Colors
             foreach (static::$cssColors as $name => $rgb_str) {
                 $rgb_str = explode(',', $rgb_str);
 
-                if (\count($rgb_str) == 3) {
+                if (
+                    \count($rgb_str) == 3 &&
+                    ! isset($reverseColorTable[\intval($rgb_str[0])][\intval($rgb_str[1])][\intval($rgb_str[2])])
+                ) {
                     $reverseColorTable[\intval($rgb_str[0])][\intval($rgb_str[1])][\intval($rgb_str[2])] = $name;
                 }
             }
index eef6970a6ab9427f71280de1e9ad9528f53106a3..2840cd5db40240e98d500b7a25b4e91a2676a6a9 100644 (file)
@@ -1,4 +1,5 @@
 <?php
+
 /**
  * SCSSPHP
  *
@@ -17,8 +18,10 @@ use ScssPhp\ScssPhp\Cache;
 use ScssPhp\ScssPhp\Colors;
 use ScssPhp\ScssPhp\Compiler\Environment;
 use ScssPhp\ScssPhp\Exception\CompilerException;
+use ScssPhp\ScssPhp\Exception\SassScriptException;
 use ScssPhp\ScssPhp\Formatter\OutputBlock;
 use ScssPhp\ScssPhp\Node;
+use ScssPhp\ScssPhp\Node\Number;
 use ScssPhp\ScssPhp\SourceMap\SourceMapGenerator;
 use ScssPhp\ScssPhp\Type;
 use ScssPhp\ScssPhp\Parser;
@@ -58,12 +61,30 @@ use ScssPhp\ScssPhp\Util;
  */
 class Compiler
 {
+    /**
+     * @deprecated
+     */
     const LINE_COMMENTS = 1;
+    /**
+     * @deprecated
+     */
     const DEBUG_INFO    = 2;
 
+    /**
+     * @deprecated
+     */
     const WITH_RULE     = 1;
+    /**
+     * @deprecated
+     */
     const WITH_MEDIA    = 2;
+    /**
+     * @deprecated
+     */
     const WITH_SUPPORTS = 4;
+    /**
+     * @deprecated
+     */
     const WITH_ALL      = 7;
 
     const SOURCE_MAP_NONE   = 0;
@@ -87,7 +108,6 @@ class Compiler
 
         '<='  => 'lte',
         '>='  => 'gte',
-        '<=>' => 'cmp',
     ];
 
     /**
@@ -101,7 +121,9 @@ class Compiler
 
     public static $true         = [Type::T_KEYWORD, 'true'];
     public static $false        = [Type::T_KEYWORD, 'false'];
+    /** @deprecated */
     public static $NaN          = [Type::T_KEYWORD, 'NaN'];
+    /** @deprecated */
     public static $Infinity     = [Type::T_KEYWORD, 'Infinity'];
     public static $null         = [Type::T_NULL];
     public static $nullString   = [Type::T_STRING, '', []];
@@ -126,6 +148,9 @@ class Compiler
     ];
 
     protected $encoding = null;
+    /**
+     * @deprecated
+     */
     protected $lineNumberStyle = null;
 
     protected $sourceMap = self::SOURCE_MAP_NONE;
@@ -225,9 +250,9 @@ class Compiler
     public function compile($code, $path = null)
     {
         if ($this->cache) {
-            $cacheKey       = ($path ? $path : "(stdin)") . ":" . md5($code);
+            $cacheKey       = ($path ? $path : '(stdin)') . ':' . md5($code);
             $compileOptions = $this->getCompileOptions();
-            $cache          = $this->cache->getCache("compile", $cacheKey, $compileOptions);
+            $cache          = $this->cache->getCache('compile', $cacheKey, $compileOptions);
 
             if (\is_array($cache) && isset($cache['dependencies']) && isset($cache['out'])) {
                 // check if any dependency file changed before accepting the cache
@@ -258,46 +283,50 @@ class Compiler
         $this->shouldEvaluate = null;
         $this->ignoreCallStackMessage = false;
 
-        $this->parser = $this->parserFactory($path);
-        $tree         = $this->parser->parse($code);
-        $this->parser = null;
+        try {
+            $this->parser = $this->parserFactory($path);
+            $tree         = $this->parser->parse($code);
+            $this->parser = null;
 
-        $this->formatter = new $this->formatter();
-        $this->rootBlock = null;
-        $this->rootEnv   = $this->pushEnv($tree);
+            $this->formatter = new $this->formatter();
+            $this->rootBlock = null;
+            $this->rootEnv   = $this->pushEnv($tree);
 
-        $this->injectVariables($this->registeredVars);
-        $this->compileRoot($tree);
-        $this->popEnv();
+            $this->injectVariables($this->registeredVars);
+            $this->compileRoot($tree);
+            $this->popEnv();
 
-        $sourceMapGenerator = null;
+            $sourceMapGenerator = null;
 
-        if ($this->sourceMap) {
-            if (\is_object($this->sourceMap) && $this->sourceMap instanceof SourceMapGenerator) {
-                $sourceMapGenerator = $this->sourceMap;
-                $this->sourceMap = self::SOURCE_MAP_FILE;
-            } elseif ($this->sourceMap !== self::SOURCE_MAP_NONE) {
-                $sourceMapGenerator = new SourceMapGenerator($this->sourceMapOptions);
+            if ($this->sourceMap) {
+                if (\is_object($this->sourceMap) && $this->sourceMap instanceof SourceMapGenerator) {
+                    $sourceMapGenerator = $this->sourceMap;
+                    $this->sourceMap = self::SOURCE_MAP_FILE;
+                } elseif ($this->sourceMap !== self::SOURCE_MAP_NONE) {
+                    $sourceMapGenerator = new SourceMapGenerator($this->sourceMapOptions);
+                }
             }
-        }
 
-        $out = $this->formatter->format($this->scope, $sourceMapGenerator);
+            $out = $this->formatter->format($this->scope, $sourceMapGenerator);
 
-        if (! empty($out) && $this->sourceMap && $this->sourceMap !== self::SOURCE_MAP_NONE) {
-            $sourceMap    = $sourceMapGenerator->generateJson();
-            $sourceMapUrl = null;
+            if (! empty($out) && $this->sourceMap && $this->sourceMap !== self::SOURCE_MAP_NONE) {
+                $sourceMap    = $sourceMapGenerator->generateJson();
+                $sourceMapUrl = null;
 
-            switch ($this->sourceMap) {
-                case self::SOURCE_MAP_INLINE:
-                    $sourceMapUrl = sprintf('data:application/json,%s', Util::encodeURIComponent($sourceMap));
-                    break;
+                switch ($this->sourceMap) {
+                    case self::SOURCE_MAP_INLINE:
+                        $sourceMapUrl = sprintf('data:application/json,%s', Util::encodeURIComponent($sourceMap));
+                        break;
 
-                case self::SOURCE_MAP_FILE:
-                    $sourceMapUrl = $sourceMapGenerator->saveMap($sourceMap);
-                    break;
-            }
+                    case self::SOURCE_MAP_FILE:
+                        $sourceMapUrl = $sourceMapGenerator->saveMap($sourceMap);
+                        break;
+                }
 
-            $out .= sprintf('/*# sourceMappingURL=%s */', $sourceMapUrl);
+                $out .= sprintf('/*# sourceMappingURL=%s */', $sourceMapUrl);
+            }
+        } catch (SassScriptException $e) {
+            throw $this->error($e->getMessage());
         }
 
         if ($this->cache && isset($cacheKey) && isset($compileOptions)) {
@@ -306,7 +335,13 @@ class Compiler
                 'out' => &$out,
             ];
 
-            $this->cache->setCache("compile", $cacheKey, $v, $compileOptions);
+            $this->cache->setCache('compile', $cacheKey, $v, $compileOptions);
+        }
+
+        if (!$this->charsetSeen && function_exists('mb_strlen')) {
+            if (strlen($out) !== mb_strlen($out)) {
+                $out = '@charset "UTF-8";' . "\n" . $out;
+            }
         }
 
         return $out;
@@ -390,7 +425,7 @@ class Compiler
      */
     protected function makeOutputBlock($type, $selectors = null)
     {
-        $out = new OutputBlock;
+        $out = new OutputBlock();
         $out->type      = $type;
         $out->lines     = [];
         $out->children  = [];
@@ -446,7 +481,7 @@ class Compiler
             $origin = $this->collapseSelectors($origin);
 
             $this->sourceLine = $block[Parser::SOURCE_LINE];
-            $this->throwError("\"$origin\" failed to @extend \"$target\". The selector \"$target\" was not found.");
+            throw $this->error("\"$origin\" failed to @extend \"$target\". The selector \"$target\" was not found.");
         }
     }
 
@@ -527,10 +562,11 @@ class Compiler
             } else {
                 // a selector part finishing with a ) is the last part of a :not( or :nth-child(
                 // and need to be joined to this
-                if (\count($new) && \is_string($new[\count($new) - 1]) &&
+                if (
+                    \count($new) && \is_string($new[\count($new) - 1]) &&
                     \strlen($part) && substr($part, -1) === ')' && strpos($part, '(') === false
                 ) {
-                    while (\count($new)>1 && substr($new[\count($new) - 1], -1) !== '(') {
+                    while (\count($new) > 1 && substr($new[\count($new) - 1], -1) !== '(') {
                         $part = array_pop($new) . $part;
                     }
                     $new[\count($new) - 1] .= $part;
@@ -594,7 +630,7 @@ class Compiler
                         }
                     }
 
-                    if (\count($nonBreakableBefore) and $k == \count($new)) {
+                    if (\count($nonBreakableBefore) && $k === \count($new)) {
                         $k--;
                     }
 
@@ -684,8 +720,9 @@ class Compiler
      */
     protected function isPseudoSelector($part, &$matches)
     {
-        if (strpos($part, ":") === 0
-            && preg_match(",^::?([\w-]+)\((.+)\)$,", $part, $matches)
+        if (
+            strpos($part, ':') === 0 &&
+            preg_match(",^::?([\w-]+)\((.+)\)$,", $part, $matches)
         ) {
             return true;
         }
@@ -709,7 +746,8 @@ class Compiler
             $single = reset($extended);
             $part = reset($single);
 
-            if ($this->isPseudoSelector($part, $matchesExtended) &&
+            if (
+                $this->isPseudoSelector($part, $matchesExtended) &&
                 \in_array($matchesExtended[1], [ 'slotted' ])
             ) {
                 $prev = end($out);
@@ -719,11 +757,12 @@ class Compiler
                     $single = reset($prev);
                     $part = reset($single);
 
-                    if ($this->isPseudoSelector($part, $matchesPrev) &&
+                    if (
+                        $this->isPseudoSelector($part, $matchesPrev) &&
                         $matchesPrev[1] === $matchesExtended[1]
                     ) {
                         $extended = explode($matchesExtended[1] . '(', $matchesExtended[0], 2);
-                        $extended[1] = $matchesPrev[2] . ", " . $extended[1];
+                        $extended[1] = $matchesPrev[2] . ', ' . $extended[1];
                         $extended = implode($matchesExtended[1] . '(', $extended);
                         $extended = [ [ $extended ]];
                         array_pop($out);
@@ -783,14 +822,15 @@ class Compiler
                 }
             }
 
-            if ($initial &&
+            if (
+                $initial &&
                 $this->isPseudoSelector($part, $matches) &&
                 ! \in_array($matches[1], [ 'not' ])
             ) {
                 $buffer    = $matches[2];
                 $parser    = $this->parserFactory(__METHOD__);
 
-                if ($parser->parseSelector($buffer, $subSelectors)) {
+                if ($parser->parseSelector($buffer, $subSelectors, false)) {
                     foreach ($subSelectors as $ksub => $subSelector) {
                         $subExtended = [];
                         $this->matchExtends($subSelector, $subExtended, 0, false);
@@ -805,7 +845,7 @@ class Compiler
 
                             $subSelectorsExtended = implode(', ', $subSelectorsExtended);
                             $singleExtended = $single;
-                            $singleExtended[$k] = str_replace("(".$buffer.")", "($subSelectorsExtended)", $part);
+                            $singleExtended[$k] = str_replace('(' . $buffer . ')', "($subSelectorsExtended)", $part);
                             $outOrigin[] = [ $singleExtended ];
                             $found = true;
                         }
@@ -830,14 +870,15 @@ class Compiler
 
             foreach ($origin as $j => $new) {
                 // prevent infinite loop when target extends itself
-                if ($this->isSelfExtend($single, $origin) and !$initial) {
+                if ($this->isSelfExtend($single, $origin) && ! $initial) {
                     return false;
                 }
 
                 $replacement = end($new);
 
                 // Extending a decorated tag with another tag is not possible.
-                if ($extendingDecoratedTag && $replacement[0] != $extendingDecoratedTag &&
+                if (
+                    $extendingDecoratedTag && $replacement[0] != $extendingDecoratedTag &&
                     preg_match('/^[a-z0-9]+$/i', $replacement[0])
                 ) {
                     unset($origin[$j]);
@@ -908,12 +949,13 @@ class Compiler
         $wasTag = false;
         $pseudo = [];
 
-        while (\count($other) && strpos(end($other), ':')===0) {
+        while (\count($other) && strpos(end($other), ':') === 0) {
             array_unshift($pseudo, array_pop($other));
         }
 
         foreach ([array_reverse($base), array_reverse($other)] as $single) {
             $rang = count($single);
+
             foreach ($single as $part) {
                 if (preg_match('/^[\[:]/', $part)) {
                     $out[] = $part;
@@ -921,7 +963,7 @@ class Compiler
                 } elseif (preg_match('/^[\.#]/', $part)) {
                     array_unshift($out, $part);
                     $wasTag = false;
-                } elseif (preg_match('/^[^_-]/', $part) and $rang==1) {
+                } elseif (preg_match('/^[^_-]/', $part) && $rang === 1) {
                     $tag[] = $part;
                     $wasTag = true;
                 } elseif ($wasTag) {
@@ -972,7 +1014,8 @@ class Compiler
             foreach ($media->children as $child) {
                 $type = $child[0];
 
-                if ($type !== Type::T_BLOCK &&
+                if (
+                    $type !== Type::T_BLOCK &&
                     $type !== Type::T_MEDIA &&
                     $type !== Type::T_DIRECTIVE &&
                     $type !== Type::T_IMPORT
@@ -983,7 +1026,7 @@ class Compiler
             }
 
             if ($needsWrap) {
-                $wrapped = new Block;
+                $wrapped = new Block();
                 $wrapped->sourceName   = $media->sourceName;
                 $wrapped->sourceIndex  = $media->sourceIndex;
                 $wrapped->sourceLine   = $media->sourceLine;
@@ -994,30 +1037,6 @@ class Compiler
                 $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);
@@ -1057,14 +1076,25 @@ class Compiler
     protected function compileDirective($directive, OutputBlock $out)
     {
         if (\is_array($directive)) {
-            $s = '@' . $directive[0];
+            $directiveName = $this->compileDirectiveName($directive[0]);
+            $s = '@' . $directiveName;
 
             if (! empty($directive[1])) {
                 $s .= ' ' . $this->compileValue($directive[1]);
             }
+            // sass-spec compliance on newline after directives, a bit tricky :/
+            $appendNewLine = (! empty($directive[2]) || strpos($s, "\n")) ? "\n" : "";
+            if (\is_array($directive[0]) && empty($directive[1])) {
+                $appendNewLine = "\n";
+            }
 
-            $this->appendRootDirective($s . ';', $out);
+            if (empty($directive[3])) {
+                $this->appendRootDirective($s . ';' . $appendNewLine, $out, [Type::T_COMMENT, Type::T_DIRECTIVE]);
+            } else {
+                $this->appendOutputLine($out, Type::T_DIRECTIVE, $s . ';');
+            }
         } else {
+            $directive->name = $this->compileDirectiveName($directive->name);
             $s = '@' . $directive->name;
 
             if (! empty($directive->value)) {
@@ -1079,6 +1109,22 @@ class Compiler
         }
     }
 
+    /**
+     * directive names can include some interpolation
+     *
+     * @param string|array $directiveName
+     * @return array|string
+     * @throws CompilerException
+     */
+    protected function compileDirectiveName($directiveName)
+    {
+        if (is_string($directiveName)) {
+            return $directiveName;
+        }
+
+        return $this->compileValue($directiveName);
+    }
+
     /**
      * Compile at-root
      *
@@ -1092,7 +1138,7 @@ class Compiler
 
         // wrap inline selector
         if ($block->selector) {
-            $wrapped = new Block;
+            $wrapped = new Block();
             $wrapped->sourceName   = $block->sourceName;
             $wrapped->sourceIndex  = $block->sourceIndex;
             $wrapped->sourceLine   = $block->sourceLine;
@@ -1109,7 +1155,9 @@ class Compiler
 
         $selfParent = $block->selfParent;
 
-        if (! $block->selfParent->selectors && isset($block->parent) && $block->parent &&
+        if (
+            ! $block->selfParent->selectors &&
+            isset($block->parent) && $block->parent &&
             isset($block->parent->selectors) && $block->parent->selectors
         ) {
             $selfParent = $block->parent;
@@ -1266,6 +1314,17 @@ class Compiler
         $without = ['rule' => true];
 
         if ($withCondition) {
+            if ($withCondition[0] === Type::T_INTERPOLATE) {
+                $w = $this->compileValue($withCondition);
+
+                $buffer = "($w)";
+                $parser = $this->parserFactory(__METHOD__);
+
+                if ($parser->parseValue($buffer, $reParsedWith)) {
+                    $withCondition = $reParsedWith;
+                }
+            }
+
             if ($this->libMapHasKey([$withCondition, static::$with])) {
                 $without = []; // cancel the default
                 $list = $this->coerceList($this->libMapGet([$withCondition, static::$with]));
@@ -1338,7 +1397,7 @@ class Compiler
 
             if ($block->type === Type::T_DIRECTIVE) {
                 if (isset($block->name)) {
-                    return $this->testWithWithout($block->name, $with, $without);
+                    return $this->testWithWithout($this->compileDirectiveName($block->name), $with, $without);
                 } elseif (isset($block->selectors) && preg_match(',@(\w+),ims', json_encode($block->selectors), $m)) {
                     return $this->testWithWithout($m[1], $with, $without);
                 } else {
@@ -1354,7 +1413,7 @@ class Compiler
                     $s = reset($s);
                 }
 
-                if (\is_object($s) && $s instanceof Node\Number) {
+                if (\is_object($s) && $s instanceof Number) {
                     return $this->testWithWithout('keyframes', $with, $without);
                 }
             }
@@ -1377,7 +1436,6 @@ class Compiler
      */
     protected function testWithWithout($what, $with, $without)
     {
-
         // if without, reject only if in the list (or 'all' is in the list)
         if (\count($without)) {
             return (isset($without[$what]) || isset($without['all'])) ? false : true;
@@ -1465,7 +1523,7 @@ class Compiler
 
         // wrap assign children in a block
         // except for @font-face
-        if ($block->type !== Type::T_DIRECTIVE || $block->name !== "font-face") {
+        if ($block->type !== Type::T_DIRECTIVE || $this->compileDirectiveName($block->name) !== 'font-face') {
             // need wrapping?
             $needWrapping = false;
 
@@ -1477,7 +1535,7 @@ class Compiler
             }
 
             if ($needWrapping) {
-                $wrapped = new Block;
+                $wrapped = new Block();
                 $wrapped->sourceName   = $block->sourceName;
                 $wrapped->sourceIndex  = $block->sourceIndex;
                 $wrapped->sourceLine   = $block->sourceLine;
@@ -1524,30 +1582,6 @@ class Compiler
 
         $out = $this->makeOutputBlock(null);
 
-        if (isset($this->lineNumberStyle) && \count($env->selectors) && \count($block->children)) {
-            $annotation = $this->makeOutputBlock(Type::T_COMMENT);
-            $annotation->depth = 0;
-
-            $file = $this->sourceNames[$block->sourceIndex];
-            $line = $block->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->scope->children[] = $out;
 
         if (\count($block->children)) {
@@ -1637,11 +1671,11 @@ class Compiler
 
         // after evaluating interpolates, we might need a second pass
         if ($this->shouldEvaluate) {
-            $selectors = $this->revertSelfSelector($selectors);
+            $selectors = $this->replaceSelfSelector($selectors, '&');
             $buffer    = $this->collapseSelectors($selectors);
             $parser    = $this->parserFactory(__METHOD__);
 
-            if ($parser->parseSelector($buffer, $newSelectors)) {
+            if ($parser->parseSelector($buffer, $newSelectors, true)) {
                 $selectors = array_map([$this, 'evalSelector'], $newSelectors);
             }
         }
@@ -1674,11 +1708,12 @@ class Compiler
             if (\is_array($p) && ($p[0] === Type::T_INTERPOLATE || $p[0] === Type::T_STRING)) {
                 $p = $this->compileValue($p);
 
-                // force re-evaluation
-                if (strpos($p, '&') !== false || strpos($p, ',') !== false) {
+                // force re-evaluation if self char or non standard char
+                if (preg_match(',[^\w-],', $p)) {
                     $this->shouldEvaluate = true;
                 }
-            } elseif (\is_string($p) && \strlen($p) >= 2 &&
+            } elseif (
+                \is_string($p) && \strlen($p) >= 2 &&
                 ($first = $p[0]) && ($first === '"' || $first === "'") &&
                 substr($p, -1) === $first
             ) {
@@ -1762,14 +1797,18 @@ class Compiler
      *
      * @return array
      */
-    protected function revertSelfSelector($selectors)
+    protected function replaceSelfSelector($selectors, $replace = null)
     {
         foreach ($selectors as &$part) {
             if (\is_array($part)) {
                 if ($part === [Type::T_SELF]) {
-                    $part = '&';
+                    if (\is_null($replace)) {
+                        $replace = $this->reduce([Type::T_SELF]);
+                        $replace = $this->compileValue($replace);
+                    }
+                    $part = $replace;
                 } else {
-                    $part = $this->revertSelfSelector($part);
+                    $part = $this->replaceSelfSelector($part, $replace);
                 }
             }
         }
@@ -1789,7 +1828,8 @@ class Compiler
         $joined = [];
 
         foreach ($single as $part) {
-            if (empty($joined) ||
+            if (
+                empty($joined) ||
                 ! \is_string($part) ||
                 preg_match('/[\[.:#%]/', $part)
             ) {
@@ -1894,9 +1934,9 @@ class Compiler
         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";
+            $msg = 'Infinite calling loop';
 
-            $this->throwError($msg);
+            throw $this->error($msg);
         }
     }
 
@@ -1961,10 +2001,7 @@ class Compiler
             }
 
             if (isset($ret)) {
-                $this->throwError('@return may only be used within a function');
-                $this->popCallStack();
-
-                return;
+                throw $this->error('@return may only be used within a function');
             }
         }
 
@@ -1994,7 +2031,8 @@ class Compiler
 
                     // the parser had no mean to know if media type or expression if it was an interpolation
                     // so you need to reparse if the T_MEDIA_TYPE looks like anything else a media type
-                    if ($q[0] == Type::T_MEDIA_TYPE &&
+                    if (
+                        $q[0] == Type::T_MEDIA_TYPE &&
                         (strpos($value, '(') !== false ||
                         strpos($value, ')') !== false ||
                         strpos($value, ':') !== false ||
@@ -2049,7 +2087,7 @@ class Compiler
         $start   = '@media ';
         $default = trim($start);
         $out     = [];
-        $current = "";
+        $current = '';
 
         foreach ($queryList as $query) {
             $type = null;
@@ -2088,7 +2126,7 @@ class Compiler
                                     $out[] = $start . $current;
                                 }
 
-                                $current = "";
+                                $current = '';
                                 $type    = null;
                                 $parts   = [];
                             }
@@ -2263,7 +2301,7 @@ class Compiler
         }
 
         // t1 == t2, neither m1 nor m2 are "not"
-        return [empty($m1)? $m2 : $m1, $t1];
+        return [empty($m1) ? $m2 : $m1, $t1];
     }
 
     /**
@@ -2280,7 +2318,7 @@ class Compiler
         if ($rawPath[0] === Type::T_STRING) {
             $path = $this->compileStringContent($rawPath);
 
-            if ($path = $this->findImport($path)) {
+            if (strpos($path, 'url(') !== 0 && $path = $this->findImport($path)) {
                 if (! $once || ! \in_array($path, $this->importedFiles)) {
                     $this->importFile($path, $out);
                     $this->importedFiles[] = $path;
@@ -2289,7 +2327,7 @@ class Compiler
                 return true;
             }
 
-            $this->appendRootDirective('@import ' . $this->compileValue($rawPath). ';', $out);
+            $this->appendRootDirective('@import ' . $this->compileImportPath($rawPath) . ';', $out);
 
             return false;
         }
@@ -2302,7 +2340,7 @@ class Compiler
 
             foreach ($rawPath[2] as $path) {
                 if ($path[0] !== Type::T_STRING) {
-                    $this->appendRootDirective('@import ' . $this->compileValue($rawPath) . ';', $out);
+                    $this->appendRootDirective('@import ' . $this->compileImportPath($rawPath) . ';', $out);
 
                     return false;
                 }
@@ -2315,11 +2353,58 @@ class Compiler
             return true;
         }
 
-        $this->appendRootDirective('@import ' . $this->compileValue($rawPath) . ';', $out);
+        $this->appendRootDirective('@import ' . $this->compileImportPath($rawPath) . ';', $out);
 
         return false;
     }
 
+    /**
+     * @param $rawPath
+     * @return string
+     * @throws CompilerException
+     */
+    protected function compileImportPath($rawPath)
+    {
+        $path = $this->compileValue($rawPath);
+
+        // case url() without quotes : supress \r \n remaining in the path
+        // if this is a real string there can not be CR or LF char
+        if (strpos($path, 'url(') === 0) {
+            $path = str_replace(array("\r", "\n"), array('', ' '), $path);
+        } else {
+            // if this is a file name in a string, spaces shoudl be escaped
+            $path = $this->reduce($rawPath);
+            $path = $this->escapeImportPathString($path);
+            $path = $this->compileValue($path);
+        }
+
+        return $path;
+    }
+
+    /**
+     * @param array $path
+     * @return array
+     * @throws CompilerException
+     */
+    protected function escapeImportPathString($path)
+    {
+        switch ($path[0]) {
+            case Type::T_LIST:
+                foreach ($path[2] as $k => $v) {
+                    $path[2][$k] = $this->escapeImportPathString($v);
+                }
+                break;
+            case Type::T_STRING:
+                if ($path[1]) {
+                    $path = $this->compileValue($path);
+                    $path = str_replace(' ', '\\ ', $path);
+                    $path = [Type::T_KEYWORD, $path];
+                }
+                break;
+        }
+
+        return $path;
+    }
 
     /**
      * Append a root directive like @import or @charset as near as the possible from the source code
@@ -2381,19 +2466,12 @@ class Compiler
     {
         $outWrite = &$out;
 
-        if ($type === Type::T_COMMENT) {
-            $parent = $out->parent;
-
-            if (end($parent->children) !== $out) {
-                $outWrite = &$parent->children[\count($parent->children) - 1];
-            }
-        }
-
         // check if it's a flat output or not
         if (\count($out->children)) {
             $lastChild = &$out->children[\count($out->children) - 1];
 
-            if ($lastChild->depth === $out->depth &&
+            if (
+                $lastChild->depth === $out->depth &&
                 \is_null($lastChild->selectors) &&
                 ! \count($lastChild->children)
             ) {
@@ -2545,7 +2623,7 @@ class Compiler
                             break;
                     }
 
-                    if ($compiledName === 'font' and $value[0] === Type::T_LIST && $value[1]==',') {
+                    if ($compiledName === 'font' && $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
                         $shorthandValue=&$value[2][0];
@@ -2561,7 +2639,7 @@ class Compiler
                                 $divider = $this->reduce($divider, true);
                             }
 
-                            if (\intval($divider->dimension) and ! \count($divider->units)) {
+                            if ($divider instanceof Number && \intval($divider->getDimension()) && $divider->unitless()) {
                                 $revert = false;
                             }
                         }
@@ -2574,9 +2652,10 @@ class Compiler
                             if ($item[0] === Type::T_EXPRESSION && $item[1] === '/') {
                                 if ($maxShorthandDividers > 0) {
                                     $revert = true;
+
                                     // if the list of values is too long, this has to be a shorthand,
                                     // otherwise it could be a real division
-                                    if (\is_null($maxListElements) or \count($shorthandValue[2]) <= $maxListElements) {
+                                    if (\is_null($maxListElements) || \count($shorthandValue[2]) <= $maxListElements) {
                                         if ($shorthandDividerNeedsUnit) {
                                             $divider = $item[3];
 
@@ -2584,7 +2663,7 @@ class Compiler
                                                 $divider = $this->reduce($divider, true);
                                             }
 
-                                            if (\intval($divider->dimension) and ! \count($divider->units)) {
+                                            if ($divider instanceof Number && \intval($divider->getDimension()) && $divider->unitless()) {
                                                 $revert = false;
                                             }
                                         }
@@ -2642,6 +2721,7 @@ class Compiler
 
             case Type::T_EXTEND:
                 foreach ($child[1] as $sel) {
+                    $sel = $this->replaceSelfSelector($sel);
                     $results = $this->evalSelectors([$sel]);
 
                     foreach ($results as $result) {
@@ -2666,7 +2746,8 @@ class Compiler
                 }
 
                 foreach ($if->cases as $case) {
-                    if ($case->type === Type::T_ELSE ||
+                    if (
+                        $case->type === Type::T_ELSE ||
                         $case->type === Type::T_ELSEIF && $this->isTruthy($this->reduce($case->cond))
                     ) {
                         return $this->compileChildren($case->children, $out);
@@ -2695,17 +2776,11 @@ class Compiler
                     $ret = $this->compileChildren($each->children, $out);
 
                     if ($ret) {
-                        if ($ret[0] !== Type::T_CONTROL) {
-                            $store = $this->env->store;
-                            $this->popEnv();
-                            $this->backPropagateEnv($store, $each->vars);
+                        $store = $this->env->store;
+                        $this->popEnv();
+                        $this->backPropagateEnv($store, $each->vars);
 
-                            return $ret;
-                        }
-
-                        if ($ret[1]) {
-                            break;
-                        }
+                        return $ret;
                     }
                 }
                 $store = $this->env->store;
@@ -2721,13 +2796,7 @@ class Compiler
                     $ret = $this->compileChildren($while->children, $out);
 
                     if ($ret) {
-                        if ($ret[0] !== Type::T_CONTROL) {
-                            return $ret;
-                        }
-
-                        if ($ret[1]) {
-                            break;
-                        }
+                        return $ret;
                     }
                 }
                 break;
@@ -2738,43 +2807,45 @@ class Compiler
                 $start = $this->reduce($for->start, true);
                 $end   = $this->reduce($for->end, true);
 
-                if (! ($start[2] == $end[2] || $end->unitless())) {
-                    $this->throwError('Incompatible units: "%s" and "%s".', $start->unitStr(), $end->unitStr());
+                if (! $start instanceof Number) {
+                    throw $this->error('%s is not a number', $start[0]);
+                }
 
-                    break;
+                if (! $end instanceof Number) {
+                    throw $this->error('%s is not a number', $end[0]);
                 }
 
-                $unit  = $start[2];
-                $start = $start[1];
-                $end   = $end[1];
+                $start->assertSameUnitOrUnitless($end);
+
+                $numeratorUnits = $start->getNumeratorUnits();
+                $denominatorUnits = $start->getDenominatorUnits();
+
+                $start = $start->getDimension();
+                $end   = $end->getDimension();
 
                 $d = $start < $end ? 1 : -1;
 
                 $this->pushEnv();
 
                 for (;;) {
-                    if ((! $for->until && $start - $d == $end) ||
+                    if (
+                        (! $for->until && $start - $d == $end) ||
                         ($for->until && $start == $end)
                     ) {
                         break;
                     }
 
-                    $this->set($for->var, new Node\Number($start, $unit));
+                    $this->set($for->var, new Number($start, $numeratorUnits, $denominatorUnits));
                     $start += $d;
 
                     $ret = $this->compileChildren($for->children, $out);
 
                     if ($ret) {
-                        if ($ret[0] !== Type::T_CONTROL) {
-                            $store = $this->env->store;
-                            $this->popEnv();
-                            $this->backPropagateEnv($store, [$for->var]);
-                            return $ret;
-                        }
+                        $store = $this->env->store;
+                        $this->popEnv();
+                        $this->backPropagateEnv($store, [$for->var]);
 
-                        if ($ret[1]) {
-                            break;
-                        }
+                        return $ret;
                     }
                 }
 
@@ -2784,12 +2855,6 @@ class Compiler
 
                 break;
 
-            case Type::T_BREAK:
-                return [Type::T_CONTROL, true];
-
-            case Type::T_CONTINUE:
-                return [Type::T_CONTROL, false];
-
             case Type::T_RETURN:
                 return $this->reduce($child[1], true);
 
@@ -2804,8 +2869,7 @@ class Compiler
                 $mixin = $this->get(static::$namespaces['mixin'] . $name, false);
 
                 if (! $mixin) {
-                    $this->throwError("Undefined mixin $name");
-                    break;
+                    throw $this->error("Undefined mixin $name");
                 }
 
                 $callingScope = $this->getStoreEnv();
@@ -2862,10 +2926,10 @@ class Compiler
                 if (! empty($mixin->parentEnv)) {
                     $this->env->declarationScopeParent = $mixin->parentEnv;
                 } else {
-                    $this->throwError("@mixin $name() without parentEnv");
+                    throw $this->error("@mixin $name() without parentEnv");
                 }
 
-                $this->compileChildrenNoReturn($mixin->children, $out, $selfParent, $this->env->marker . " " . $name);
+                $this->compileChildrenNoReturn($mixin->children, $out, $selfParent, $this->env->marker . ' ' . $name);
 
                 $this->popEnv();
                 break;
@@ -2907,9 +2971,9 @@ class Compiler
 
                 $fname = $this->sourceNames[$this->sourceIndex];
                 $line  = $this->sourceLine;
-                $value = $this->compileValue($this->reduce($value, true));
+                $value = $this->compileDebugValue($value);
 
-                fwrite($this->stderr, "File $fname on line $line DEBUG: $value\n");
+                fwrite($this->stderr, "$fname:$line DEBUG: $value\n");
                 break;
 
             case Type::T_WARN:
@@ -2917,9 +2981,9 @@ class Compiler
 
                 $fname = $this->sourceNames[$this->sourceIndex];
                 $line  = $this->sourceLine;
-                $value = $this->compileValue($this->reduce($value, true));
+                $value = $this->compileDebugValue($value);
 
-                fwrite($this->stderr, "File $fname on line $line WARN: $value\n");
+                fwrite($this->stderr, "WARNING: $value\n         on line $line of $fname\n\n");
                 break;
 
             case Type::T_ERROR:
@@ -2929,15 +2993,10 @@ class Compiler
                 $line  = $this->sourceLine;
                 $value = $this->compileValue($this->reduce($value, true));
 
-                $this->throwError("File $fname on line $line ERROR: $value\n");
-                break;
-
-            case Type::T_CONTROL:
-                $this->throwError('@break/@continue not permitted in this scope');
-                break;
+                throw $this->error("File $fname on line $line ERROR: $value\n");
 
             default:
-                $this->throwError("unknown child type: $child[0]");
+                throw $this->error("unknown child type: $child[0]");
         }
     }
 
@@ -2945,14 +3004,21 @@ class Compiler
      * Reduce expression to string
      *
      * @param array $exp
+     * @param true $keepParens
      *
      * @return array
      */
-    protected function expToString($exp)
+    protected function expToString($exp, $keepParens = false)
     {
-        list(, $op, $left, $right, /* $inParens */, $whiteLeft, $whiteRight) = $exp;
+        list(, $op, $left, $right, $inParens, $whiteLeft, $whiteRight) = $exp;
+
+        $content = [];
 
-        $content = [$this->reduce($left)];
+        if ($keepParens && $inParens) {
+            $content[] = '(';
+        }
+
+        $content[] = $this->reduce($left);
 
         if ($whiteLeft) {
             $content[] = ' ';
@@ -2966,6 +3032,10 @@ class Compiler
 
         $content[] = $this->reduce($right);
 
+        if ($keepParens && $inParens) {
+            $content[] = ')';
+        }
+
         return [Type::T_STRING, '', $content];
     }
 
@@ -3023,7 +3093,7 @@ class Compiler
      * @param array   $value
      * @param boolean $inExp
      *
-     * @return null|string|array|\ScssPhp\ScssPhp\Node\Number
+     * @return null|string|array|Number
      */
     protected function reduce($value, $inExp = false)
     {
@@ -3045,8 +3115,9 @@ class Compiler
                 }
 
                 // special case: looks like css shorthand
-                if ($opName == 'div' && ! $inParens && ! $inExp && isset($right[2]) &&
-                    (($right[0] !== Type::T_NUMBER && $right[2] != '') ||
+                if (
+                    $opName == 'div' && ! $inParens && ! $inExp &&
+                    (($right[0] !== Type::T_NUMBER && isset($right[2]) && $right[2] != '') ||
                     ($right[0] === Type::T_NUMBER && ! $right->unitless()))
                 ) {
                     return $this->expToString($value);
@@ -3067,7 +3138,8 @@ class Compiler
                 // 3. op[op name]
                 $fn = "op${ucOpName}${ucLType}${ucRType}";
 
-                if (\is_callable([$this, $fn]) ||
+                if (
+                    \is_callable([$this, $fn]) ||
                     (($fn = "op${ucLType}${ucRType}") &&
                         \is_callable([$this, $fn]) &&
                         $passOp = true) ||
@@ -3075,52 +3147,6 @@ class Compiler
                         \is_callable([$this, $fn]) &&
                         $genOp = true)
                 ) {
-                    $coerceUnit = false;
-
-                    if (! isset($genOp) &&
-                        $left[0] === Type::T_NUMBER && $right[0] === Type::T_NUMBER
-                    ) {
-                        $coerceUnit = true;
-
-                        switch ($opName) {
-                            case 'mul':
-                                $targetUnit = $left[2];
-
-                                foreach ($right[2] as $unit => $exp) {
-                                    $targetUnit[$unit] = (isset($targetUnit[$unit]) ? $targetUnit[$unit] : 0) + $exp;
-                                }
-                                break;
-
-                            case 'div':
-                                $targetUnit = $left[2];
-
-                                foreach ($right[2] as $unit => $exp) {
-                                    $targetUnit[$unit] = (isset($targetUnit[$unit]) ? $targetUnit[$unit] : 0) - $exp;
-                                }
-                                break;
-
-                            case 'mod':
-                                $targetUnit = $left[2];
-                                break;
-
-                            default:
-                                $targetUnit = $left->unitless() ? $right[2] : $left[2];
-                        }
-
-                        $baseUnitLeft = $left->isNormalizable();
-                        $baseUnitRight = $right->isNormalizable();
-
-                        if ($baseUnitLeft && $baseUnitRight && $baseUnitLeft === $baseUnitRight) {
-                            $left = $left->normalize();
-                            $right = $right->normalize();
-                        }
-                        else {
-                            if ($coerceUnit) {
-                                $left = new Node\Number($left[1], []);
-                            }
-                        }
-                    }
-
                     $shouldEval = $inParens || $inExp;
 
                     if (isset($passOp)) {
@@ -3130,10 +3156,6 @@ class Compiler
                     }
 
                     if (isset($out)) {
-                        if ($coerceUnit && $out[0] === Type::T_NUMBER) {
-                            $out = $out->coerce($targetUnit);
-                        }
-
                         return $out;
                     }
                 }
@@ -3146,13 +3168,13 @@ class Compiler
                 $inExp = $inExp || $this->shouldEval($exp);
                 $exp = $this->reduce($exp);
 
-                if ($exp[0] === Type::T_NUMBER) {
+                if ($exp instanceof Number) {
                     switch ($op) {
                         case '+':
-                            return new Node\Number($exp[1], $exp[2]);
+                            return $exp;
 
                         case '-':
-                            return new Node\Number(-$exp[1], $exp[2]);
+                            return $exp->unaryMinus();
                     }
                 }
 
@@ -3213,7 +3235,8 @@ class Compiler
                 return $this->fncall($value[1], $value[2]);
 
             case Type::T_SELF:
-                $selfSelector = $this->multiplySelectors($this->env,!empty($this->env->block->selfParent) ? $this->env->block->selfParent : null);
+                $selfParent = ! empty($this->env->block->selfParent) ? $this->env->block->selfParent : null;
+                $selfSelector = $this->multiplySelectors($this->env, $selfParent);
                 $selfSelector = $this->collapseSelectors($selfSelector, true);
 
                 return $selfSelector;
@@ -3231,156 +3254,350 @@ class Compiler
      *
      * @return array|null
      */
-    protected function fncall($name, $argValues)
+    protected function fncall($functionReference, $argValues)
     {
-        // SCSS @function
-        if ($this->callScssFunction($name, $argValues, $returnValue)) {
-            return $returnValue;
-        }
+        // a string means this is a static hard reference coming from the parsing
+        if (is_string($functionReference)) {
+            $name = $functionReference;
 
-        // native PHP functions
-        if ($this->callNativeFunction($name, $argValues, $returnValue)) {
-            return $returnValue;
+            $functionReference = $this->getFunctionReference($name);
+            if ($functionReference === static::$null || $functionReference[0] !== Type::T_FUNCTION_REFERENCE) {
+                $functionReference = [Type::T_FUNCTION, $name, [Type::T_LIST, ',', []]];
+            }
         }
 
-        // for CSS functions, simply flatten the arguments into a list
-        $listArgs = [];
+        // a function type means we just want a plain css function call
+        if ($functionReference[0] === Type::T_FUNCTION) {
+            // for CSS functions, simply flatten the arguments into a list
+            $listArgs = [];
 
-        foreach ((array) $argValues as $arg) {
-            if (empty($arg[0])) {
-                $listArgs[] = $this->reduce($arg[1]);
+            foreach ((array) $argValues as $arg) {
+                if (empty($arg[0]) || count($argValues) === 1) {
+                    $listArgs[] = $this->reduce($this->stringifyFncallArgs($arg[1]));
+                }
             }
+
+            return [Type::T_FUNCTION, $functionReference[1], [Type::T_LIST, ',', $listArgs]];
         }
 
-        return [Type::T_FUNCTION, $name, [Type::T_LIST, ',', $listArgs]];
-    }
+        if ($functionReference === static::$null || $functionReference[0] !== Type::T_FUNCTION_REFERENCE) {
+            return static::$defaultValue;
+        }
 
-    /**
-     * Normalize name
-     *
-     * @param string $name
-     *
-     * @return string
-     */
-    protected function normalizeName($name)
-    {
-        return str_replace('-', '_', $name);
-    }
 
-    /**
-     * Normalize value
-     *
-     * @param array $value
-     *
-     * @return array
-     */
-    public function normalizeValue($value)
-    {
-        $value = $this->coerceForExpression($this->reduce($value));
+        switch ($functionReference[1]) {
+            // SCSS @function
+            case 'scss':
+                return $this->callScssFunction($functionReference[3], $argValues);
 
-        switch ($value[0]) {
-            case Type::T_LIST:
-                $value = $this->extractInterpolation($value);
+            // native PHP functions
+            case 'user':
+            case 'native':
+                list(,,$name, $fn, $prototype) = $functionReference;
 
-                if ($value[0] !== Type::T_LIST) {
-                    return [Type::T_KEYWORD, $this->compileValue($value)];
+                // special cases of css valid functions min/max
+                $name = strtolower($name);
+                if (\in_array($name, ['min', 'max'])) {
+                    $cssFunction = $this->cssValidArg(
+                        [Type::T_FUNCTION_CALL, $name, $argValues],
+                        ['min', 'max', 'calc', 'env', 'var']
+                    );
+                    if ($cssFunction !== false) {
+                        return $cssFunction;
+                    }
                 }
+                $returnValue = $this->callNativeFunction($name, $fn, $prototype, $argValues);
 
-                foreach ($value[2] as $key => $item) {
-                    $value[2][$key] = $this->normalizeValue($item);
+                if (! isset($returnValue)) {
+                    return $this->fncall([Type::T_FUNCTION, $name, [Type::T_LIST, ',', []]], $argValues);
                 }
 
-                if (! empty($value['enclosing'])) {
-                    unset($value['enclosing']);
+                return $returnValue;
+
+            default:
+                return static::$defaultValue;
+        }
+    }
+
+    protected function cssValidArg($arg, $allowed_function = [], $inFunction = false)
+    {
+        switch ($arg[0]) {
+            case Type::T_INTERPOLATE:
+                return [Type::T_KEYWORD, $this->CompileValue($arg)];
+
+            case Type::T_FUNCTION:
+                if (! \in_array($arg[1], $allowed_function)) {
+                    return false;
+                }
+                if ($arg[2][0] === Type::T_LIST) {
+                    foreach ($arg[2][2] as $k => $subarg) {
+                        $arg[2][2][$k] = $this->cssValidArg($subarg, $allowed_function, $arg[1]);
+                        if ($arg[2][2][$k] === false) {
+                            return false;
+                        }
+                    }
                 }
+                return $arg;
 
-                return $value;
+            case Type::T_FUNCTION_CALL:
+                if (! \in_array($arg[1], $allowed_function)) {
+                    return false;
+                }
+                $cssArgs = [];
+                foreach ($arg[2] as $argValue) {
+                    if ($argValue === static::$null) {
+                        return false;
+                    }
+                    $cssArg = $this->cssValidArg($argValue[1], $allowed_function, $arg[1]);
+                    if (empty($argValue[0]) && $cssArg !== false) {
+                        $cssArgs[] = [$argValue[0], $cssArg];
+                    } else {
+                        return false;
+                    }
+                }
+
+                return $this->fncall([Type::T_FUNCTION, $arg[1], [Type::T_LIST, ',', []]], $cssArgs);
 
             case Type::T_STRING:
-                return [$value[0], '"', [$this->compileStringContent($value)]];
+            case Type::T_KEYWORD:
+                if (!$inFunction or !\in_array($inFunction, ['calc', 'env', 'var'])) {
+                    return false;
+                }
+                return $this->stringifyFncallArgs($arg);
 
             case Type::T_NUMBER:
-                return $value->normalize();
+                return $this->stringifyFncallArgs($arg);
 
-            case Type::T_INTERPOLATE:
-                return [Type::T_KEYWORD, $this->compileValue($value)];
+            case Type::T_LIST:
+                if (!$inFunction) {
+                    return false;
+                }
+                if (empty($arg['enclosing']) and $arg[1] === '') {
+                    foreach ($arg[2] as $k => $subarg) {
+                        $arg[2][$k] = $this->cssValidArg($subarg, $allowed_function, $inFunction);
+                        if ($arg[2][$k] === false) {
+                            return false;
+                        }
+                    }
+                    $arg[0] = Type::T_STRING;
+                    return $arg;
+                }
+                return false;
+
+            case Type::T_EXPRESSION:
+                if (! \in_array($arg[1], ['+', '-', '/', '*'])) {
+                    return false;
+                }
+                $arg[2] = $this->cssValidArg($arg[2], $allowed_function, $inFunction);
+                $arg[3] = $this->cssValidArg($arg[3], $allowed_function, $inFunction);
+                if ($arg[2] === false || $arg[3] === false) {
+                    return false;
+                }
+                return $this->expToString($arg, true);
 
+            case Type::T_VARIABLE:
+            case Type::T_SELF:
             default:
-                return $value;
+                return false;
         }
     }
 
+
     /**
-     * Add numbers
+     * Reformat fncall arguments to proper css function output
      *
-     * @param array $left
-     * @param array $right
+     * @param $arg
      *
-     * @return \ScssPhp\ScssPhp\Node\Number
+     * @return array|\ArrayAccess|Number|string|null
      */
-    protected function opAddNumberNumber($left, $right)
+    protected function stringifyFncallArgs($arg)
     {
-        return new Node\Number($left[1] + $right[1], $left[2]);
+
+        switch ($arg[0]) {
+            case Type::T_LIST:
+                foreach ($arg[2] as $k => $v) {
+                    $arg[2][$k] = $this->stringifyFncallArgs($v);
+                }
+                break;
+
+            case Type::T_EXPRESSION:
+                if ($arg[1] === '/') {
+                    $arg[2] = $this->stringifyFncallArgs($arg[2]);
+                    $arg[3] = $this->stringifyFncallArgs($arg[3]);
+                    $arg[5] = $arg[6] = false; // no space around /
+                    $arg = $this->expToString($arg);
+                }
+                break;
+
+            case Type::T_FUNCTION_CALL:
+                $name = strtolower($arg[1]);
+
+                if (in_array($name, ['max', 'min', 'calc'])) {
+                    $args = $arg[2];
+                    $arg = $this->fncall([Type::T_FUNCTION, $name, [Type::T_LIST, ',', []]], $args);
+                }
+                break;
+        }
+
+        return $arg;
     }
 
     /**
-     * Multiply numbers
-     *
-     * @param array $left
-     * @param array $right
+     * Find a function reference
+     * @param string $name
+     * @param bool $safeCopy
+     * @return array
+     */
+    protected function getFunctionReference($name, $safeCopy = false)
+    {
+        // SCSS @function
+        if ($func = $this->get(static::$namespaces['function'] . $name, false)) {
+            if ($safeCopy) {
+                $func = clone $func;
+            }
+
+            return [Type::T_FUNCTION_REFERENCE, 'scss', $name, $func];
+        }
+
+        // native PHP functions
+
+        // try to find a native lib function
+        $normalizedName = $this->normalizeName($name);
+        $libName = null;
+
+        if (isset($this->userFunctions[$normalizedName])) {
+            // see if we can find a user function
+            list($f, $prototype) = $this->userFunctions[$normalizedName];
+
+            return [Type::T_FUNCTION_REFERENCE, 'user', $name, $f, $prototype];
+        }
+
+        if (($f = $this->getBuiltinFunction($normalizedName)) && \is_callable($f)) {
+            $libName   = $f[1];
+            $prototype = isset(static::$$libName) ? static::$$libName : null;
+
+            return [Type::T_FUNCTION_REFERENCE, 'native', $name, $f, $prototype];
+        }
+
+        return static::$null;
+    }
+
+
+    /**
+     * Normalize name
      *
-     * @return \ScssPhp\ScssPhp\Node\Number
+     * @param string $name
+     *
+     * @return string
      */
-    protected function opMulNumberNumber($left, $right)
+    protected function normalizeName($name)
     {
-        return new Node\Number($left[1] * $right[1], $left[2]);
+        return str_replace('-', '_', $name);
+    }
+
+    /**
+     * Normalize value
+     *
+     * @param array $value
+     *
+     * @return array
+     */
+    public function normalizeValue($value)
+    {
+        $value = $this->coerceForExpression($this->reduce($value));
+
+        switch ($value[0]) {
+            case Type::T_LIST:
+                $value = $this->extractInterpolation($value);
+
+                if ($value[0] !== Type::T_LIST) {
+                    return [Type::T_KEYWORD, $this->compileValue($value)];
+                }
+
+                foreach ($value[2] as $key => $item) {
+                    $value[2][$key] = $this->normalizeValue($item);
+                }
+
+                if (! empty($value['enclosing'])) {
+                    unset($value['enclosing']);
+                }
+
+                return $value;
+
+            case Type::T_STRING:
+                return [$value[0], '"', [$this->compileStringContent($value)]];
+
+            case Type::T_INTERPOLATE:
+                return [Type::T_KEYWORD, $this->compileValue($value)];
+
+            default:
+                return $value;
+        }
+    }
+
+    /**
+     * Add numbers
+     *
+     * @param Number $left
+     * @param Number $right
+     *
+     * @return Number
+     */
+    protected function opAddNumberNumber(Number $left, Number $right)
+    {
+        return $left->plus($right);
+    }
+
+    /**
+     * Multiply numbers
+     *
+     * @param Number $left
+     * @param Number $right
+     *
+     * @return Number
+     */
+    protected function opMulNumberNumber(Number $left, Number $right)
+    {
+        return $left->times($right);
     }
 
     /**
      * Subtract numbers
      *
-     * @param array $left
-     * @param array $right
+     * @param Number $left
+     * @param Number $right
      *
-     * @return \ScssPhp\ScssPhp\Node\Number
+     * @return Number
      */
-    protected function opSubNumberNumber($left, $right)
+    protected function opSubNumberNumber(Number $left, Number $right)
     {
-        return new Node\Number($left[1] - $right[1], $left[2]);
+        return $left->minus($right);
     }
 
     /**
      * Divide numbers
      *
-     * @param array $left
-     * @param array $right
+     * @param Number $left
+     * @param Number $right
      *
-     * @return array|\ScssPhp\ScssPhp\Node\Number
+     * @return Number
      */
-    protected function opDivNumberNumber($left, $right)
+    protected function opDivNumberNumber(Number $left, Number $right)
     {
-        if ($right[1] == 0) {
-            return ($left[1] == 0) ? static::$NaN : static::$Infinity;
-        }
-
-        return new Node\Number($left[1] / $right[1], $left[2]);
+        return $left->dividedBy($right);
     }
 
     /**
      * Mod numbers
      *
-     * @param array $left
-     * @param array $right
+     * @param Number $left
+     * @param Number $right
      *
-     * @return \ScssPhp\ScssPhp\Node\Number
+     * @return Number
      */
-    protected function opModNumberNumber($left, $right)
+    protected function opModNumberNumber(Number $left, Number $right)
     {
-        if ($right[1] == 0) {
-            return static::$NaN;
-        }
-
-        return new Node\Number($left[1] % $right[1], $left[2]);
+        return $left->modulo($right);
     }
 
     /**
@@ -3503,13 +3720,16 @@ class Compiler
                     break;
 
                 case '%':
+                    if ($rval == 0) {
+                        throw $this->error("color: Can't take modulo by zero");
+                    }
+
                     $out[] = $lval % $rval;
                     break;
 
                 case '/':
                     if ($rval == 0) {
-                        $this->throwError("color: Can't divide by zero");
-                        break 2;
+                        throw $this->error("color: Can't divide by zero");
                     }
 
                     $out[] = (int) ($lval / $rval);
@@ -3522,8 +3742,7 @@ class Compiler
                     return $this->opNeq($left, $right);
 
                 default:
-                    $this->throwError("color: unknown op $op");
-                    break 2;
+                    throw $this->error("color: unknown op $op");
             }
         }
 
@@ -3541,13 +3760,13 @@ class Compiler
      *
      * @param string $op
      * @param array  $left
-     * @param array  $right
+     * @param Number  $right
      *
      * @return array
      */
-    protected function opColorNumber($op, $left, $right)
+    protected function opColorNumber($op, $left, Number $right)
     {
-        $value = $right[1];
+        $value = $right->getDimension();
 
         return $this->opColorColor(
             $op,
@@ -3560,14 +3779,14 @@ class Compiler
      * Compare number and color
      *
      * @param string $op
-     * @param array  $left
+     * @param Number  $left
      * @param array  $right
      *
      * @return array
      */
-    protected function opNumberColor($op, $left, $right)
+    protected function opNumberColor($op, Number $left, $right)
     {
-        $value = $left[1];
+        $value = $left->getDimension();
 
         return $this->opColorColor(
             $op,
@@ -3619,70 +3838,81 @@ class Compiler
     }
 
     /**
-     * Compare number1 >= number2
+     * Compare number1 == number2
      *
-     * @param array $left
-     * @param array $right
+     * @param Number $left
+     * @param Number $right
      *
      * @return array
      */
-    protected function opGteNumberNumber($left, $right)
+    protected function opEqNumberNumber(Number $left, Number $right)
     {
-        return $this->toBool($left[1] >= $right[1]);
+        return $this->toBool($left->equals($right));
     }
 
     /**
-     * Compare number1 > number2
+     * Compare number1 != number2
      *
-     * @param array $left
-     * @param array $right
+     * @param Number $left
+     * @param Number $right
      *
      * @return array
      */
-    protected function opGtNumberNumber($left, $right)
+    protected function opNeqNumberNumber(Number $left, Number $right)
     {
-        return $this->toBool($left[1] > $right[1]);
+        return $this->toBool(!$left->equals($right));
     }
 
     /**
-     * Compare number1 <= number2
+     * Compare number1 >= number2
      *
-     * @param array $left
-     * @param array $right
+     * @param Number $left
+     * @param Number $right
      *
      * @return array
      */
-    protected function opLteNumberNumber($left, $right)
+    protected function opGteNumberNumber(Number $left, Number $right)
     {
-        return $this->toBool($left[1] <= $right[1]);
+        return $this->toBool($left->greaterThanOrEqual($right));
     }
 
     /**
-     * Compare number1 < number2
+     * Compare number1 > number2
      *
-     * @param array $left
-     * @param array $right
+     * @param Number $left
+     * @param Number $right
      *
      * @return array
      */
-    protected function opLtNumberNumber($left, $right)
+    protected function opGtNumberNumber(Number $left, Number $right)
     {
-        return $this->toBool($left[1] < $right[1]);
+        return $this->toBool($left->greaterThan($right));
     }
 
     /**
-     * Three-way comparison, aka spaceship operator
+     * Compare number1 <= number2
      *
-     * @param array $left
-     * @param array $right
+     * @param Number $left
+     * @param Number $right
      *
-     * @return \ScssPhp\ScssPhp\Node\Number
+     * @return array
      */
-    protected function opCmpNumberNumber($left, $right)
+    protected function opLteNumberNumber(Number $left, Number $right)
     {
-        $n = $left[1] - $right[1];
+        return $this->toBool($left->lessThanOrEqual($right));
+    }
 
-        return new Node\Number($n ? $n / abs($n) : 0, '');
+    /**
+     * Compare number1 < number2
+     *
+     * @param Number $left
+     * @param Number $right
+     *
+     * @return array
+     */
+    protected function opLtNumberNumber(Number $left, Number $right)
+    {
+        return $this->toBool($left->lessThan($right));
     }
 
     /**
@@ -3699,6 +3929,48 @@ class Compiler
         return $thing ? static::$true : static::$false;
     }
 
+    /**
+     * Escape non printable chars in strings output as in dart-sass
+     * @param $string
+     * @return string|string[]
+     */
+    public function escapeNonPrintableChars($string, $inKeyword = false)
+    {
+        static $replacement = [];
+        if (empty($replacement[$inKeyword])) {
+            for ($i = 0; $i < 32; $i++) {
+                if ($i !== 9 || $inKeyword) {
+                    $replacement[$inKeyword][chr($i)] = '\\' . dechex($i) . ($inKeyword ? ' ' : chr(0));
+                }
+            }
+        }
+        $string = str_replace(array_keys($replacement[$inKeyword]), array_values($replacement[$inKeyword]), $string);
+        // chr(0) is not a possible char from the input, so any chr(0) comes from our escaping replacement
+        if (strpos($string, chr(0)) !== false) {
+            if (substr($string, -1) === chr(0)) {
+                $string = substr($string, 0, -1);
+            }
+            $string = str_replace(
+                [chr(0) . '\\',chr(0) . ' '],
+                [ '\\', ' '],
+                $string
+            );
+            if (strpos($string, chr(0)) !== false) {
+                $parts = explode(chr(0), $string);
+                $string = array_shift($parts);
+                while (count($parts)) {
+                    $next = array_shift($parts);
+                    if (strpos("0123456789abcdefABCDEF" . chr(9), $next[0]) !== false) {
+                        $string .= " ";
+                    }
+                    $string .= $next;
+                }
+            }
+        }
+
+        return $string;
+    }
+
     /**
      * Compiles a primitive value into a CSS property value.
      *
@@ -3722,6 +3994,9 @@ class Compiler
 
         switch ($value[0]) {
             case Type::T_KEYWORD:
+                if (is_string($value[1])) {
+                    $value[1] = $this->escapeNonPrintableChars($value[1], true);
+                }
                 return $value[1];
 
             case Type::T_COLOR:
@@ -3746,7 +4021,7 @@ class Compiler
                         }
 
                         if (is_numeric($alpha)) {
-                            $a = new Node\Number($alpha, '');
+                            $a = new Number($alpha, '');
                         } else {
                             $a = $alpha;
                         }
@@ -3778,13 +4053,42 @@ class Compiler
                 return $value->output($this);
 
             case Type::T_STRING:
-                return $value[1] . $this->compileStringContent($value) . $value[1];
+                $content = $this->compileStringContent($value);
+
+                if ($value[1]) {
+                    $content = str_replace('\\', '\\\\', $content);
+
+                    $content = $this->escapeNonPrintableChars($content);
+
+                    // force double quote as string quote for the output in certain cases
+                    if (
+                        $value[1] === "'" &&
+                        (strpos($content, '"') === false or strpos($content, "'") !== false) &&
+                        strpbrk($content, '{}\\\'') !== false
+                    ) {
+                        $value[1] = '"';
+                    } elseif (
+                        $value[1] === '"' &&
+                        (strpos($content, '"') !== false and strpos($content, "'") === false)
+                    ) {
+                        $value[1] = "'";
+                    }
+
+                    $content = str_replace($value[1], '\\' . $value[1], $content);
+                }
+
+                return $value[1] . $content . $value[1];
 
             case Type::T_FUNCTION:
                 $args = ! empty($value[2]) ? $this->compileValue($value[2]) : '';
 
                 return "$value[1]($args)";
 
+            case Type::T_FUNCTION_REFERENCE:
+                $name = ! empty($value[2]) ? $value[2] : '';
+
+                return "get-function(\"$name\")";
+
             case Type::T_LIST:
                 $value = $this->extractInterpolation($value);
 
@@ -3793,42 +4097,61 @@ class Compiler
                 }
 
                 list(, $delim, $items) = $value;
-                $pre = $post = "";
+                $pre = $post = '';
 
                 if (! empty($value['enclosing'])) {
                     switch ($value['enclosing']) {
                         case 'parent':
-                            //$pre = "(";
-                            //$post = ")";
+                            //$pre = '(';
+                            //$post = ')';
                             break;
                         case 'forced_parent':
-                            $pre = "(";
-                            $post = ")";
+                            $pre = '(';
+                            $post = ')';
                             break;
                         case 'bracket':
                         case 'forced_bracket':
-                            $pre = "[";
-                            $post = "]";
+                            $pre = '[';
+                            $post = ']';
                             break;
                     }
                 }
 
                 $prefix_value = '';
+
                 if ($delim !== ' ') {
                     $prefix_value = ' ';
                 }
 
                 $filtered = [];
 
+                $same_string_quote = null;
                 foreach ($items as $item) {
+                    if (\is_null($same_string_quote)) {
+                        $same_string_quote = false;
+                        if ($item[0] === Type::T_STRING) {
+                            $same_string_quote = $item[1];
+                            foreach ($items as $ii) {
+                                if ($ii[0] !== Type::T_STRING) {
+                                    $same_string_quote = false;
+                                    break;
+                                }
+                            }
+                        }
+                    }
                     if ($item[0] === Type::T_NULL) {
                         continue;
                     }
+                    if ($same_string_quote === '"' && $item[0] === Type::T_STRING && $item[1]) {
+                        $item[1] = $same_string_quote;
+                    }
 
                     $compiled = $this->compileValue($item);
+
                     if ($prefix_value && \strlen($compiled)) {
                         $compiled = $prefix_value . $compiled;
                     }
+
                     $filtered[] = $compiled;
                 }
 
@@ -3860,8 +4183,9 @@ class Compiler
                     $delim .= ' ';
                 }
 
-                $left = \count($left[2]) > 0 ?
-                    $this->compileValue($left) . $delim . $whiteLeft: '';
+                $left = \count($left[2]) > 0
+                    ?  $this->compileValue($left) . $delim . $whiteLeft
+                    : '';
 
                 $delim = $right[1];
 
@@ -3914,7 +4238,7 @@ class Compiler
                         break;
 
                     case Type::T_STRING:
-                        $reduced = [Type::T_KEYWORD, $this->compileStringContent($reduced)];
+                        $reduced = [Type::T_STRING, '', [$this->compileStringContent($reduced)]];
                         break;
 
                     case Type::T_NULL:
@@ -3930,7 +4254,25 @@ class Compiler
                 return $this->compileCommentValue($value);
 
             default:
-                $this->throwError("unknown value type: ".json_encode($value));
+                throw $this->error('unknown value type: ' . json_encode($value));
+        }
+    }
+
+    /**
+     * @param array $value
+     *
+     * @return array|string
+     */
+    protected function compileDebugValue($value)
+    {
+        $value = $this->reduce($value, true);
+
+        switch ($value[0]) {
+            case Type::T_STRING:
+                return $this->compileStringContent($value);
+
+            default:
+                return $this->compileValue($value);
         }
     }
 
@@ -4023,8 +4365,8 @@ class Compiler
                 $prevSelectors = $selectors;
                 $selectors     = [];
 
-                foreach ($prevSelectors as $selector) {
-                    foreach ($parentSelectors as $parent) {
+                foreach ($parentSelectors as $parent) {
+                    foreach ($prevSelectors as $selector) {
                         if ($selfParentSelectors) {
                             foreach ($selfParentSelectors as $selfParent) {
                                 // if no '&' in the selector, each call will give same result, only add once
@@ -4045,7 +4387,7 @@ class Compiler
         $selectors = array_values($selectors);
 
         // case we are just starting a at-root : nothing to multiply but parentSelectors
-        if (!$selectors and $selfParentSelectors) {
+        if (! $selectors && $selfParentSelectors) {
             $selectors = $selfParentSelectors;
         }
 
@@ -4124,7 +4466,8 @@ class Compiler
      */
     protected function multiplyMedia(Environment $env = null, $childQueries = null)
     {
-        if (! isset($env) ||
+        if (
+            ! isset($env) ||
             ! empty($env->block->type) && $env->block->type !== Type::T_MEDIA
         ) {
             return $childQueries;
@@ -4209,7 +4552,7 @@ class Compiler
      */
     protected function pushEnv(Block $block = null)
     {
-        $env = new Environment;
+        $env = new Environment();
         $env->parent = $this->env;
         $env->parentStore = $this->storeEnv;
         $env->store  = [];
@@ -4417,7 +4760,7 @@ class Compiler
         }
 
         if ($shouldThrow) {
-            $this->throwError("Undefined variable \$$name" . ($maxDepth <= 0 ? " (infinite recursion)" : ""));
+            throw $this->error("Undefined variable \$$name" . ($maxDepth <= 0 ? ' (infinite recursion)' : ''));
         }
 
         // found nothing
@@ -4557,10 +4900,13 @@ class Compiler
      * @api
      *
      * @param integer $numberPrecision
+     *
+     * @deprecated The number precision is not configurable anymore. The default is enough for all browsers.
      */
     public function setNumberPrecision($numberPrecision)
     {
-        Node\Number::$precision = $numberPrecision;
+        @trigger_error('The number precision is not configurable anymore. '
+            . 'The default is enough for all browsers.', E_USER_DEPRECATED);
     }
 
     /**
@@ -4581,10 +4927,13 @@ class Compiler
      * @api
      *
      * @param string $lineNumberStyle
+     *
+     * @deprecated The line number output is not supported anymore. Use source maps instead.
      */
     public function setLineNumberStyle($lineNumberStyle)
     {
-        $this->lineNumberStyle = $lineNumberStyle;
+        @trigger_error('The line number output is not supported anymore. '
+                       . 'Use source maps instead.', E_USER_DEPRECATED);
     }
 
     /**
@@ -4643,9 +4992,13 @@ class Compiler
      * @api
      *
      * @param string $name
+     *
+     * @deprecated Registering additional features is deprecated.
      */
     public function addFeature($name)
     {
+        @trigger_error('Registering additional features is deprecated.', E_USER_DEPRECATED);
+
         $this->registeredFeatures[$name] = true;
     }
 
@@ -4657,7 +5010,7 @@ class Compiler
      */
     protected function importFile($path, OutputBlock $out)
     {
-        $this->pushCallStack('import '.$path);
+        $this->pushCallStack('import ' . $path);
         // see if tree is cached
         $realPath = realpath($path);
 
@@ -4709,9 +5062,8 @@ class Compiler
 
             if (! $hasExtension) {
                 $urls[] = "$url/index.scss";
-                $urls[] = "$url/_index.scss";
                 // allow to find a plain css file, *if* no scss or partial scss is found
-                $urls[] .= $url . ".css";
+                $urls[] .= $url . '.css';
             }
         }
 
@@ -4719,6 +5071,7 @@ class Compiler
             if (\is_string($dir)) {
                 // check urls for normal import paths
                 foreach ($urls as $full) {
+                    $found = [];
                     $separator = (
                         ! empty($dir) &&
                         substr($dir, -1) !== '/' &&
@@ -4727,7 +5080,23 @@ class Compiler
                     $full = $dir . $separator . $full;
 
                     if (is_file($file = $full)) {
-                        return $file;
+                        $found[] = $file;
+                    }
+                    if (! $isPartial) {
+                        $full = dirname($full) . '/_' . basename($full);
+                        if (is_file($file = $full)) {
+                            $found[] = $file;
+                        }
+                    }
+                    if ($found) {
+                        if (\count($found) === 1) {
+                            return reset($found);
+                        }
+                        if (\count($found) > 1) {
+                            throw $this->error(
+                                "Error: It's not clear which file to import. Found: " . implode(', ', $found)
+                            );
+                        }
                     }
                 }
             } elseif (\is_callable($dir)) {
@@ -4741,8 +5110,8 @@ class Compiler
         }
 
         if ($urls) {
-            if (! $hasExtension or preg_match('/[.]scss$/', $url)) {
-                $this->throwError("`$url` file not found for @import");
+            if (! $hasExtension || preg_match('/[.]scss$/', $url)) {
+                throw $this->error("`$url` file not found for @import");
             }
         }
 
@@ -4769,14 +5138,30 @@ class Compiler
      * @param boolean $ignoreErrors
      *
      * @return \ScssPhp\ScssPhp\Compiler
+     *
+     * @deprecated Ignoring Sass errors is not longer supported.
      */
     public function setIgnoreErrors($ignoreErrors)
     {
-        $this->ignoreErrors = $ignoreErrors;
+        @trigger_error('Ignoring Sass errors is not longer supported.', E_USER_DEPRECATED);
 
         return $this;
     }
 
+    /**
+     * Get source position
+     *
+     * @api
+     *
+     * @return array
+     */
+    public function getSourcePosition()
+    {
+        $sourceFile = isset($this->sourceNames[$this->sourceIndex]) ? $this->sourceNames[$this->sourceIndex] : '';
+
+        return [$sourceFile, $this->sourceLine, $this->sourceColumn];
+    }
+
     /**
      * Throw error (exception)
      *
@@ -4785,15 +5170,32 @@ class Compiler
      * @param string $msg Message with optional sprintf()-style vararg parameters
      *
      * @throws \ScssPhp\ScssPhp\Exception\CompilerException
+     *
+     * @deprecated use "error" and throw the exception in the caller instead.
      */
     public function throwError($msg)
     {
-        if ($this->ignoreErrors) {
-            return;
-        }
+        @trigger_error(
+            'The method "throwError" is deprecated. Use "error" and throw the exception in the caller instead',
+            E_USER_DEPRECATED
+        );
+
+        throw $this->error(...func_get_args());
+    }
 
-        if (\func_num_args() > 1) {
-            $msg = \call_user_func_array('sprintf', \func_get_args());
+    /**
+     * Build an error (exception)
+     *
+     * @api
+     *
+     * @param string $msg Message with optional sprintf()-style vararg parameters
+     *
+     * @return CompilerException
+     */
+    public function error($msg, ...$args)
+    {
+        if ($args) {
+            $msg = sprintf($msg, ...$args);
         }
 
         if (! $this->ignoreCallStackMessage) {
@@ -4813,7 +5215,40 @@ class Compiler
             }
         }
 
-        throw new CompilerException($msg);
+        return new CompilerException($msg);
+    }
+
+    /**
+     * @param string $functionName
+     * @param array $ExpectedArgs
+     * @param int $nbActual
+     * @return CompilerException
+     */
+    public function errorArgsNumber($functionName, $ExpectedArgs, $nbActual)
+    {
+        $nbExpected = \count($ExpectedArgs);
+
+        if ($nbActual > $nbExpected) {
+            return $this->error(
+                'Error: Only %d arguments allowed in %s(), but %d were passed.',
+                $nbExpected,
+                $functionName,
+                $nbActual
+            );
+        } else {
+            $missing = [];
+
+            while (count($ExpectedArgs) && count($ExpectedArgs) > $nbActual) {
+                array_unshift($missing, array_pop($ExpectedArgs));
+            }
+
+            return $this->error(
+                'Error: %s() argument%s %s missing.',
+                $functionName,
+                count($missing) > 1 ? 's' : '',
+                implode(', ', $missing)
+            );
+        }
     }
 
     /**
@@ -4832,11 +5267,11 @@ class Compiler
         if ($this->callStack) {
             foreach (array_reverse($this->callStack) as $call) {
                 if ($all || (isset($call['n']) && $call['n'])) {
-                    $msg = "#" . $ncall++ . " " . $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];
+                    $msg .= ' on line ' . $call[Parser::SOURCE_LINE];
 
                     $callStackMsg[] = $msg;
 
@@ -4867,8 +5302,7 @@ class Compiler
             $file = $this->sourceNames[$env->block->sourceIndex];
 
             if (realpath($file) === $name) {
-                $this->throwError('An @import loop has been found: %s imports %s', $file, basename($file));
-                break;
+                throw $this->error('An @import loop has been found: %s imports %s', $file, basename($file));
             }
         }
     }
@@ -4876,19 +5310,17 @@ class Compiler
     /**
      * Call SCSS @function
      *
-     * @param string $name
+     * @param Object $func
      * @param array  $argValues
-     * @param array  $returnValue
      *
-     * @return boolean Returns true if returnValue is set; otherwise, false
+     * @return array $returnValue
      */
-    protected function callScssFunction($name, $argValues, &$returnValue)
+    protected function callScssFunction($func, $argValues)
     {
-        $func = $this->get(static::$namespaces['function'] . $name, false);
-
         if (! $func) {
-            return false;
+            return static::$defaultValue;
         }
+        $name = $func->name;
 
         $this->pushEnv();
 
@@ -4898,7 +5330,7 @@ class Compiler
         }
 
         // throw away lines and children
-        $tmp = new OutputBlock;
+        $tmp = new OutputBlock();
         $tmp->lines    = [];
         $tmp->children = [];
 
@@ -4907,44 +5339,35 @@ class Compiler
         if (! empty($func->parentEnv)) {
             $this->env->declarationScopeParent = $func->parentEnv;
         } else {
-            $this->throwError("@function $name() without parentEnv");
+            throw $this->error("@function $name() without parentEnv");
         }
 
-        $ret = $this->compileChildren($func->children, $tmp, $this->env->marker . " " . $name);
+        $ret = $this->compileChildren($func->children, $tmp, $this->env->marker . ' ' . $name);
 
         $this->popEnv();
 
-        $returnValue = ! isset($ret) ? static::$defaultValue : $ret;
-
-        return true;
+        return ! isset($ret) ? static::$defaultValue : $ret;
     }
 
     /**
      * Call built-in and registered (PHP) functions
      *
      * @param string $name
+     * @param string|array $function
+     * @param array  $prototype
      * @param array  $args
-     * @param array  $returnValue
      *
-     * @return boolean Returns true if returnValue is set; otherwise, false
+     * @return array
      */
-    protected function callNativeFunction($name, $args, &$returnValue)
+    protected function callNativeFunction($name, $function, $prototype, $args)
     {
-        // try a lib function
-        $name = $this->normalizeName($name);
-        $libName = null;
+        $libName = (is_array($function) ? end($function) : null);
+        $sorted_kwargs = $this->sortNativeFunctionArgs($libName, $prototype, $args);
 
-        if (isset($this->userFunctions[$name])) {
-            // see if we can find a user function
-            list($f, $prototype) = $this->userFunctions[$name];
-        } elseif (($f = $this->getBuiltinFunction($name)) && \is_callable($f)) {
-            $libName   = $f[1];
-            $prototype = isset(static::$$libName) ? static::$$libName : null;
-        } else {
-            return false;
+        if (\is_null($sorted_kwargs)) {
+            return null;
         }
-
-        @list($sorted, $kwargs) = $this->sortNativeFunctionArgs($libName, $prototype, $args);
+        @list($sorted, $kwargs) = $sorted_kwargs;
 
         if ($name !== 'if' && $name !== 'call') {
             $inExp = true;
@@ -4958,15 +5381,13 @@ class Compiler
             }
         }
 
-        $returnValue = \call_user_func($f, $sorted, $kwargs);
+        $returnValue = \call_user_func($function, $sorted, $kwargs);
 
         if (! isset($returnValue)) {
-            return false;
+            return null;
         }
 
-        $returnValue = $this->coerceValue($returnValue);
-
-        return true;
+        return $this->coerceValue($returnValue);
     }
 
     /**
@@ -4978,6 +5399,18 @@ class Compiler
      */
     protected function getBuiltinFunction($name)
     {
+        $libName = self::normalizeNativeFunctionName($name);
+        return [$this, $libName];
+    }
+
+    /**
+     * Normalize native function name
+     * @param $name
+     * @return string
+     */
+    public static function normalizeNativeFunctionName($name)
+    {
+        $name = str_replace("-", "_", $name);
         $libName = 'lib' . preg_replace_callback(
             '/_(.)/',
             function ($m) {
@@ -4985,8 +5418,17 @@ class Compiler
             },
             ucfirst($name)
         );
+        return $libName;
+    }
 
-        return [$this, $libName];
+    /**
+     * Check if a function is a native built-in scss function, for css parsing
+     * @param $name
+     * @return bool
+     */
+    public static function isNativeFunction($name)
+    {
+        return method_exists(Compiler::class, self::normalizeNativeFunctionName($name));
     }
 
     /**
@@ -4996,7 +5438,7 @@ class Compiler
      * @param array  $prototypes
      * @param array  $args
      *
-     * @return array
+     * @return array|null
      */
     protected function sortNativeFunctionArgs($functionName, $prototypes, $args)
     {
@@ -5006,16 +5448,18 @@ class Compiler
             $keyArgs = [];
             $posArgs = [];
 
+            if (\is_array($args) && \count($args) && \end($args) === static::$null) {
+                array_pop($args);
+            }
+
             // separate positional and keyword arguments
             foreach ($args as $arg) {
                 list($key, $value) = $arg;
 
-                $key = $key[1];
-
-                if (empty($key)) {
+                if (empty($key) or empty($key[1])) {
                     $posArgs[] = empty($arg[2]) ? $value : $arg;
                 } else {
-                    $keyArgs[$key] = $value;
+                    $keyArgs[$key[1]] = $value;
                 }
             }
 
@@ -5088,6 +5532,14 @@ class Compiler
             $this->ignoreCallStackMessage = true;
 
             try {
+                if (\count($args) > \count($argDef)) {
+                    $lastDef = end($argDef);
+
+                    // check that last arg is not a ...
+                    if (empty($lastDef[2])) {
+                        throw $this->errorArgsNumber($functionName, $argDef, \count($args));
+                    }
+                }
                 $vars = $this->applyArguments($argDef, $args, false, false);
 
                 // ensure all args are populated
@@ -5128,7 +5580,16 @@ class Compiler
         }
 
         if ($exceptionMessage && ! $prototypeHasMatch) {
-            $this->throwError($exceptionMessage);
+            if (\in_array($functionName, ['libRgb', 'libRgba', 'libHsl', 'libHsla'])) {
+                // if var() or calc() is used as an argument, return as a css function
+                foreach ($args as $arg) {
+                    if ($arg[1][0] === Type::T_FUNCTION_CALL && in_array($arg[1][1], ['var'])) {
+                        return null;
+                    }
+                }
+            }
+
+            throw $this->error($exceptionMessage);
         }
 
         return [$finalArgs, $keyArgs];
@@ -5158,7 +5619,7 @@ class Compiler
         if ($storeInEnv) {
             $storeEnv = $this->getStoreEnv();
 
-            $env = new Environment;
+            $env = new Environment();
             $env->store = $storeEnv->store;
         }
 
@@ -5175,6 +5636,7 @@ class Compiler
         $splatSeparator      = null;
         $keywordArgs         = [];
         $deferredKeywordArgs = [];
+        $deferredNamedKeywordArgs = [];
         $remaining           = [];
         $hasKeywordArgument  = false;
 
@@ -5184,9 +5646,10 @@ class Compiler
                 $hasKeywordArgument = true;
 
                 $name = $arg[0][1];
+
                 if (! isset($args[$name])) {
                     foreach (array_keys($args) as $an) {
-                        if (str_replace("_", "-", $an) === str_replace("_", "-", $name)) {
+                        if (str_replace('_', '-', $an) === str_replace('_', '-', $name)) {
                             $name = $an;
                             break;
                         }
@@ -5195,18 +5658,17 @@ class Compiler
 
                 if (! isset($args[$name]) || $args[$name][3]) {
                     if ($hasVariable) {
-                        $deferredKeywordArgs[$name] = $arg[1];
+                        $deferredNamedKeywordArgs[$name] = $arg[1];
                     } else {
-                        $this->throwError("Mixin or function doesn't have an argument named $%s.", $arg[0][1]);
-                        break;
+                        throw $this->error("Mixin or function doesn't have an argument named $%s.", $arg[0][1]);
                     }
                 } elseif ($args[$name][0] < \count($remaining)) {
-                    $this->throwError("The argument $%s was passed both by position and by name.", $arg[0][1]);
-                    break;
+                    throw $this->error("The argument $%s was passed both by position and by name.", $arg[0][1]);
                 } else {
                     $keywordArgs[$name] = $arg[1];
                 }
-            } elseif ($arg[2] === true) {
+            } elseif (! empty($arg[2])) {
+                // $arg[2] means a var followed by ... in the arg ($list... )
                 $val = $this->reduce($arg[1], true);
 
                 if ($val[0] === Type::T_LIST) {
@@ -5214,7 +5676,7 @@ class Compiler
                         if (! is_numeric($name)) {
                             if (! isset($args[$name])) {
                                 foreach (array_keys($args) as $an) {
-                                    if (str_replace("_", "-", $an) === str_replace("_", "-", $name)) {
+                                    if (str_replace('_', '-', $an) === str_replace('_', '-', $name)) {
                                         $name = $an;
                                         break;
                                     }
@@ -5242,7 +5704,7 @@ class Compiler
                         if (! is_numeric($name)) {
                             if (! isset($args[$name])) {
                                 foreach (array_keys($args) as $an) {
-                                    if (str_replace("_", "-", $an) === str_replace("_", "-", $name)) {
+                                    if (str_replace('_', '-', $an) === str_replace('_', '-', $name)) {
                                         $name = $an;
                                         break;
                                     }
@@ -5266,8 +5728,7 @@ class Compiler
                     $remaining[] = $val;
                 }
             } elseif ($hasKeywordArgument) {
-                $this->throwError('Positional arguments must come before keyword arguments.');
-                break;
+                throw $this->error('Positional arguments must come before keyword arguments.');
             } else {
                 $remaining[] = $arg[1];
             }
@@ -5277,6 +5738,14 @@ class Compiler
             list($i, $name, $default, $isVariable) = $arg;
 
             if ($isVariable) {
+                // only if more than one arg : can not be passed as position and value
+                // see https://github.com/sass/libsass/issues/2927
+                if (count($args) > 1) {
+                    if (isset($remaining[$i]) && isset($deferredNamedKeywordArgs[$name])) {
+                        throw $this->error("The argument $%s was passed both by position and by name.", $name);
+                    }
+                }
+
                 $val = [Type::T_LIST, \is_null($splatSeparator) ? ',' : $splatSeparator , [], $isVariable];
 
                 for ($count = \count($remaining); $i < $count; $i++) {
@@ -5286,6 +5755,10 @@ class Compiler
                 foreach ($deferredKeywordArgs as $itemName => $item) {
                     $val[2][$itemName] = $item;
                 }
+
+                foreach ($deferredNamedKeywordArgs as $itemName => $item) {
+                    $val[2][$itemName] = $item;
+                }
             } elseif (isset($remaining[$i])) {
                 $val = $remaining[$i];
             } elseif (isset($keywordArgs[$name])) {
@@ -5293,8 +5766,7 @@ class Compiler
             } elseif (! empty($default)) {
                 continue;
             } else {
-                $this->throwError("Missing argument $name");
-                break;
+                throw $this->error("Missing argument $name");
             }
 
             if ($storeInEnv) {
@@ -5330,7 +5802,7 @@ class Compiler
      *
      * @param mixed $value
      *
-     * @return array|\ScssPhp\ScssPhp\Node\Number
+     * @return array|Number
      */
     protected function coerceValue($value)
     {
@@ -5347,7 +5819,7 @@ class Compiler
         }
 
         if (is_numeric($value)) {
-            return new Node\Number($value, '');
+            return new Number($value, '');
         }
 
         if ($value === '') {
@@ -5377,14 +5849,15 @@ class Compiler
             return $item;
         }
 
-        if ($item[0] === static::$emptyList[0] &&
+        if (
+            $item[0] === static::$emptyList[0] &&
             $item[1] === static::$emptyList[1] &&
             $item[2] === static::$emptyList[2]
         ) {
             return static::$emptyMap;
         }
 
-        return [Type::T_MAP, [$item], [static::$null]];
+        return $item;
     }
 
     /**
@@ -5438,7 +5911,7 @@ class Compiler
             return [Type::T_LIST, ',', $list];
         }
 
-        return [Type::T_LIST, $delim, ! isset($item) ? []: [$item]];
+        return [Type::T_LIST, $delim, ! isset($item) ? [] : [$item]];
     }
 
     /**
@@ -5551,7 +6024,7 @@ class Compiler
                             if ($color[3] === 255) {
                                 $color[3] = 1; // fully opaque
                             } else {
-                                $color[3] = round($color[3] / 255, 3);
+                                $color[3] = round($color[3] / 255, Number::PRECISION);
                             }
                         }
 
@@ -5574,8 +6047,8 @@ class Compiler
     }
 
     /**
-     * @param integer|\ScssPhp\ScssPhp\Node\Number $value
-     * @param boolean                              $isAlpha
+     * @param integer|Number $value
+     * @param boolean        $isAlpha
      *
      * @return integer|mixed
      */
@@ -5593,36 +6066,27 @@ class Compiler
      * @param integer|float $min
      * @param integer|float $max
      * @param boolean       $isInt
-     * @param boolean       $clamp
-     * @param boolean       $modulo
      *
      * @return integer|mixed
      */
-    protected function compileColorPartValue($value, $min, $max, $isInt = true, $clamp = true, $modulo = false)
+    protected function compileColorPartValue($value, $min, $max, $isInt = true)
     {
         if (! is_numeric($value)) {
             if (\is_array($value)) {
                 $reduced = $this->reduce($value);
 
-                if (\is_object($reduced) && $value->type === Type::T_NUMBER) {
+                if ($reduced instanceof Number) {
                     $value = $reduced;
                 }
             }
 
-            if (\is_object($value) && $value->type === Type::T_NUMBER) {
-                $num = $value->dimension;
-
-                if (\count($value->units)) {
-                    $unit = array_keys($value->units);
-                    $unit = reset($unit);
-
-                    switch ($unit) {
-                        case '%':
-                            $num *= $max / 100;
-                            break;
-                        default:
-                            break;
-                    }
+            if ($value instanceof Number) {
+                if ($value->unitless()) {
+                    $num = $value->getDimension();
+                } elseif ($value->hasUnit('%')) {
+                    $num = $max * $value->getDimension() / 100;
+                } else {
+                    throw $this->error('Expected %s to have no units or "%%".', $value);
                 }
 
                 $value = $num;
@@ -5636,18 +6100,7 @@ class Compiler
                 $value = round($value);
             }
 
-            if ($clamp) {
-                $value = min($max, max($min, $value));
-            }
-
-            if ($modulo) {
-                $value = $value % $max;
-
-                // still negative?
-                while ($value < $min) {
-                    $value += $max;
-                }
-            }
+            $value = min($max, max($min, $value));
 
             return $value;
         }
@@ -5671,6 +6124,36 @@ class Compiler
         return [Type::T_STRING, '', [$this->compileValue($value)]];
     }
 
+    /**
+     * Assert value is a string (or keyword)
+     *
+     * @api
+     *
+     * @param array $value
+     * @param string $varName
+     *
+     * @return array
+     *
+     * @throws \Exception
+     */
+    public function assertString($value, $varName = null)
+    {
+        // case of url(...) parsed a a function
+        if ($value[0] === Type::T_FUNCTION) {
+            $value = $this->coerceString($value);
+        }
+
+        if (! \in_array($value[0], [Type::T_STRING, Type::T_KEYWORD])) {
+            $value = $this->compileValue($value);
+            $var_display = ($varName ? " \${$varName}:" : '');
+            throw $this->error("Error:{$var_display} $value is not a string.");
+        }
+
+        $value = $this->coerceString($value);
+
+        return $value;
+    }
+
     /**
      * Coerce value to a percentage
      *
@@ -5680,12 +6163,12 @@ class Compiler
      */
     protected function coercePercent($value)
     {
-        if ($value[0] === Type::T_NUMBER) {
-            if (! empty($value[2]['%'])) {
-                return $value[1] / 100;
+        if ($value instanceof Number) {
+            if ($value->hasUnit('%')) {
+                return $value->getDimension() / 100;
             }
 
-            return $value[1];
+            return $value->getDimension();
         }
 
         return 0;
@@ -5707,7 +6190,7 @@ class Compiler
         $value = $this->coerceMap($value);
 
         if ($value[0] !== Type::T_MAP) {
-            $this->throwError('expecting map, %s received', $value[0]);
+            throw $this->error('expecting map, %s received', $value[0]);
         }
 
         return $value;
@@ -5727,7 +6210,7 @@ class Compiler
     public function assertList($value)
     {
         if ($value[0] !== Type::T_LIST) {
-            $this->throwError('expecting list, %s received', $value[0]);
+            throw $this->error('expecting list, %s received', $value[0]);
         }
 
         return $value;
@@ -5750,7 +6233,7 @@ class Compiler
             return $color;
         }
 
-        $this->throwError('expecting color, %s received', $value[0]);
+        throw $this->error('expecting color, %s received', $value[0]);
     }
 
     /**
@@ -5758,21 +6241,49 @@ class Compiler
      *
      * @api
      *
+     * @param mixed $value
+     * @param string $varName
+     *
+     * @return Number
+     *
+     * @throws \Exception
+     */
+    public function assertNumber($value, $varName = null)
+    {
+        if (!$value instanceof Number) {
+            $value = $this->compileValue($value);
+            $var_display = ($varName ? " \${$varName}:" : '');
+            throw $this->error("Error:{$var_display} $value is not a number.");
+        }
+
+        return $value;
+    }
+
+    /**
+     * Assert value is a integer
+     *
+     * @api
+     *
      * @param array $value
+     * @param string $varName
      *
      * @return integer|float
      *
      * @throws \Exception
      */
-    public function assertNumber($value)
+    public function assertInteger($value, $varName = null)
     {
-        if ($value[0] !== Type::T_NUMBER) {
-            $this->throwError('expecting number, %s received', $value[0]);
+
+        $value = $this->assertNumber($value, $varName)->getDimension();
+        if (round($value - \intval($value), Number::PRECISION) > 0) {
+            $var_display = ($varName ? " \${$varName}:" : '');
+            throw $this->error("Error:{$var_display} $value is not an integer.");
         }
 
-        return $value[1];
+        return intval($value);
     }
 
+
     /**
      * Make sure a color's components don't go out of bounds
      *
@@ -5861,7 +6372,7 @@ class Compiler
         }
 
         if ($h * 3 < 2) {
-            return $m1 + ($m2 - $m1) * (2/3 - $h) * 6;
+            return $m1 + ($m2 - $m1) * (2 / 3 - $h) * 6;
         }
 
         return $m1;
@@ -5891,9 +6402,9 @@ class Compiler
         $m2 = $l <= 0.5 ? $l * ($s + 1) : $l + $s - $l * $s;
         $m1 = $l * 2 - $m2;
 
-        $r = $this->hueToRGB($m1, $m2, $h + 1/3) * 255;
+        $r = $this->hueToRGB($m1, $m2, $h + 1 / 3) * 255;
         $g = $this->hueToRGB($m1, $m2, $h) * 255;
-        $b = $this->hueToRGB($m1, $m2, $h - 1/3) * 255;
+        $b = $this->hueToRGB($m1, $m2, $h - 1 / 3) * 255;
 
         $out = [Type::T_COLOR, $r, $g, $b];
 
@@ -5902,10 +6413,27 @@ class Compiler
 
     // Built in functions
 
-    protected static $libCall = ['name', 'args...'];
+    protected static $libCall = ['function', 'args...'];
     protected function libCall($args, $kwargs)
     {
-        $name = $this->compileStringContent($this->coerceString($this->reduce(array_shift($args), true)));
+        $functionReference = $this->reduce(array_shift($args), true);
+
+        if (in_array($functionReference[0], [Type::T_STRING, Type::T_KEYWORD])) {
+            $name = $this->compileStringContent($this->coerceString($this->reduce($functionReference, true)));
+            $warning = "DEPRECATION WARNING: Passing a string to call() is deprecated and will be illegal\n"
+                . "in Sass 4.0. Use call(function-reference($name)) instead.";
+            fwrite($this->stderr, "$warning\n\n");
+            $functionReference = $this->libGetFunction([$functionReference]);
+        }
+
+        if ($functionReference === static::$null) {
+            return static::$null;
+        }
+
+        if (! in_array($functionReference[0], [Type::T_FUNCTION_REFERENCE, Type::T_FUNCTION])) {
+            throw $this->error('Function reference expected, got ' . $functionReference[0]);
+        }
+
         $callArgs = [];
 
         // $kwargs['args'] is [Type::T_LIST, ',', [..]]
@@ -5919,7 +6447,29 @@ class Compiler
             $callArgs[] = [$varname, $arg, false];
         }
 
-        return $this->reduce([Type::T_FUNCTION_CALL, $name, $callArgs]);
+        return $this->reduce([Type::T_FUNCTION_CALL, $functionReference, $callArgs]);
+    }
+
+
+    protected static $libGetFunction = [
+        ['name'],
+        ['name', 'css']
+    ];
+    protected function libGetFunction($args)
+    {
+        $name = $this->compileStringContent($this->coerceString($this->reduce(array_shift($args), true)));
+        $isCss = false;
+
+        if (count($args)) {
+            $isCss = $this->reduce(array_shift($args), true);
+            $isCss = (($isCss === static::$true) ? true : false);
+        }
+
+        if ($isCss) {
+            return [Type::T_FUNCTION, $name, [Type::T_LIST, ',', []]];
+        }
+
+        return $this->getFunctionReference($name, true);
     }
 
     protected static $libIf = ['condition', 'if-true', 'if-false:'];
@@ -5939,7 +6489,8 @@ class Compiler
     {
         list($list, $value) = $args;
 
-        if ($list[0] === Type::T_MAP ||
+        if (
+            $list[0] === Type::T_MAP ||
             $list[0] === Type::T_STRING ||
             $list[0] === Type::T_KEYWORD ||
             $list[0] === Type::T_INTERPOLATE
@@ -5951,8 +6502,25 @@ class Compiler
             return static::$null;
         }
 
+        // Numbers are represented with value objects, for which the PHP equality operator does not
+        // match the Sass rules (and we cannot overload it). As they are the only type of values
+        // represented with a value object for now, they require a special case.
+        if ($value instanceof Number) {
+            $key = 0;
+            foreach ($list[2] as $item) {
+                $key++;
+                $itemValue = $this->normalizeValue($item);
+
+                if ($itemValue instanceof Number && $value->equals($itemValue)) {
+                    return new Number($key, '');
+                }
+            }
+            return static::$null;
+        }
+
         $values = [];
 
+
         foreach ($list[2] as $item) {
             $values[] = $this->normalizeValue($item);
         }
@@ -5981,7 +6549,7 @@ class Compiler
                 $color = [Type::T_COLOR, $args[0], $args[1], $args[2]];
 
                 if (! $color = $this->coerceColor($color)) {
-                    $color = [Type::T_STRING, '', [$funcName .'(', $args[0], ', ', $args[1], ', ', $args[2], ')']];
+                    $color = [Type::T_STRING, '', [$funcName . '(', $args[0], ', ', $args[1], ', ', $args[2], ')']];
                 }
 
                 return $color;
@@ -6033,7 +6601,7 @@ class Compiler
 
         foreach ([1 => 1, 2 => 2, 3 => 3, 7 => 4] as $iarg => $irgba) {
             if (isset($args[$iarg])) {
-                $val = $this->assertNumber($args[$iarg]);
+                $val = $this->assertNumber($args[$iarg])->getDimension();
 
                 if (! isset($color[$irgba])) {
                     $color[$irgba] = (($irgba < 4) ? 0 : 1);
@@ -6048,7 +6616,7 @@ class Compiler
 
             foreach ([4 => 1, 5 => 2, 6 => 3] as $iarg => $ihsl) {
                 if (! empty($args[$iarg])) {
-                    $val = $this->assertNumber($args[$iarg]);
+                    $val = $this->assertNumber($args[$iarg])->getDimension();
                     $hsl[$ihsl] = \call_user_func($fn, $hsl[$ihsl], $val, $iarg);
                 }
             }
@@ -6130,6 +6698,11 @@ class Compiler
     protected function libIeHexStr($args)
     {
         $color = $this->coerceColor($args[0]);
+
+        if (\is_null($color)) {
+            $this->throwError('Error: argument `$color` of `ie-hex-str($color)` must be a color');
+        }
+
         $color[4] = isset($color[4]) ? round(255 * $color[4]) : 255;
 
         return [Type::T_STRING, '', [sprintf('#%02X%02X%02X%02X', $color[4], $color[1], $color[2], $color[3])]];
@@ -6140,6 +6713,10 @@ class Compiler
     {
         $color = $this->coerceColor($args[0]);
 
+        if (\is_null($color)) {
+            $this->throwError('Error: argument `$color` of `red($color)` must be a color');
+        }
+
         return $color[1];
     }
 
@@ -6148,6 +6725,10 @@ class Compiler
     {
         $color = $this->coerceColor($args[0]);
 
+        if (\is_null($color)) {
+            $this->throwError('Error: argument `$color` of `green($color)` must be a color');
+        }
+
         return $color[2];
     }
 
@@ -6156,6 +6737,10 @@ class Compiler
     {
         $color = $this->coerceColor($args[0]);
 
+        if (\is_null($color)) {
+            $this->throwError('Error: argument `$color` of `blue($color)` must be a color');
+        }
+
         return $color[3];
     }
 
@@ -6175,7 +6760,7 @@ class Compiler
     {
         $value = $args[0];
 
-        if ($value[0] === Type::T_NUMBER) {
+        if ($value instanceof Number) {
             return null;
         }
 
@@ -6183,7 +6768,10 @@ class Compiler
     }
 
     // mix two colors
-    protected static $libMix = ['color-1', 'color-2', 'weight:0.5'];
+    protected static $libMix = [
+        ['color1', 'color2', 'weight:0.5'],
+        ['color-1', 'color-2', 'weight:0.5']
+        ];
     protected function libMix($args)
     {
         list($first, $second, $weight) = $args;
@@ -6219,40 +6807,72 @@ class Compiler
         return $this->fixColor($new);
     }
 
-    protected static $libHsl =[
+    protected static $libHsl = [
         ['channels'],
         ['hue', 'saturation', 'lightness'],
         ['hue', 'saturation', 'lightness', 'alpha'] ];
     protected function libHsl($args, $kwargs, $funcName = 'hsl')
     {
+        $args_to_check = $args;
+
         if (\count($args) == 1) {
             if ($args[0][0] !== Type::T_LIST || \count($args[0][2]) < 3 || \count($args[0][2]) > 4) {
                 return [Type::T_STRING, '', [$funcName . '(', $args[0], ')']];
             }
 
             $args = $args[0][2];
+            $args_to_check = $kwargs['channels'][2];
         }
 
-        $hue = $this->compileColorPartValue($args[0], 0, 360, false, false, true);
-        $saturation = $this->compileColorPartValue($args[1], 0, 100, false);
-        $lightness = $this->compileColorPartValue($args[2], 0, 100, false);
+        foreach ($kwargs as $k => $arg) {
+            if (in_array($arg[0], [Type::T_FUNCTION_CALL]) && in_array($arg[1], ['min', 'max'])) {
+                return null;
+            }
+        }
+
+        foreach ($args_to_check as $k => $arg) {
+            if (in_array($arg[0], [Type::T_FUNCTION_CALL]) && in_array($arg[1], ['min', 'max'])) {
+                if (count($kwargs) > 1 || ($k >= 2 && count($args) === 4)) {
+                    return null;
+                }
+
+                $args[$k] = $this->stringifyFncallArgs($arg);
+            }
 
+            if (
+                $k >= 2 && count($args) === 4 &&
+                in_array($arg[0], [Type::T_FUNCTION_CALL, Type::T_FUNCTION]) &&
+                in_array($arg[1], ['calc','env'])
+            ) {
+                return null;
+            }
+        }
+
+        $hue = $this->reduce($args[0]);
+        $saturation = $this->reduce($args[1]);
+        $lightness = $this->reduce($args[2]);
         $alpha = null;
 
         if (\count($args) === 4) {
             $alpha = $this->compileColorPartValue($args[3], 0, 100, false);
 
-            if (! is_numeric($hue) || ! is_numeric($saturation) || ! is_numeric($lightness) || ! is_numeric($alpha)) {
+            if (!$hue instanceof Number || !$saturation instanceof Number || ! $lightness instanceof Number || ! is_numeric($alpha)) {
                 return [Type::T_STRING, '',
                     [$funcName . '(', $args[0], ', ', $args[1], ', ', $args[2], ', ', $args[3], ')']];
             }
         } else {
-            if (! is_numeric($hue) || ! is_numeric($saturation) || ! is_numeric($lightness)) {
+            if (!$hue instanceof Number || !$saturation instanceof Number || ! $lightness instanceof Number) {
                 return [Type::T_STRING, '', [$funcName . '(', $args[0], ', ', $args[1], ', ', $args[2], ')']];
             }
         }
 
-        $color = $this->toRGB($hue, $saturation, $lightness);
+        $hueValue = $hue->getDimension() % 360;
+
+        while ($hueValue < 0) {
+            $hueValue += 360;
+        }
+
+        $color = $this->toRGB($hueValue, max(0, min($saturation->getDimension(), 100)), max(0, min($lightness->getDimension(), 100)));
 
         if (! \is_null($alpha)) {
             $color[4] = $alpha;
@@ -6263,7 +6883,8 @@ class Compiler
 
     protected static $libHsla = [
             ['channels'],
-            ['hue', 'saturation', 'lightness', 'alpha:1'] ];
+            ['hue', 'saturation', 'lightness'],
+            ['hue', 'saturation', 'lightness', 'alpha']];
     protected function libHsla($args, $kwargs)
     {
         return $this->libHsl($args, $kwargs, 'hsla');
@@ -6275,7 +6896,7 @@ class Compiler
         $color = $this->assertColor($args[0]);
         $hsl = $this->toHSL($color[1], $color[2], $color[3]);
 
-        return new Node\Number($hsl[1], 'deg');
+        return new Number($hsl[1], 'deg');
     }
 
     protected static $libSaturation = ['color'];
@@ -6284,7 +6905,7 @@ class Compiler
         $color = $this->assertColor($args[0]);
         $hsl = $this->toHSL($color[1], $color[2], $color[3]);
 
-        return new Node\Number($hsl[2], '%');
+        return new Number($hsl[2], '%');
     }
 
     protected static $libLightness = ['color'];
@@ -6293,7 +6914,7 @@ class Compiler
         $color = $this->assertColor($args[0]);
         $hsl = $this->toHSL($color[1], $color[2], $color[3]);
 
-        return new Node\Number($hsl[3], '%');
+        return new Number($hsl[3], '%');
     }
 
     protected function adjustHsl($color, $idx, $amount)
@@ -6313,7 +6934,7 @@ class Compiler
     protected function libAdjustHue($args)
     {
         $color = $this->assertColor($args[0]);
-        $degrees = $this->assertNumber($args[1]);
+        $degrees = $this->assertNumber($args[1])->getDimension();
 
         return $this->adjustHsl($color, 1, $degrees);
     }
@@ -6336,15 +6957,20 @@ class Compiler
         return $this->adjustHsl($color, 3, -$amount);
     }
 
-    protected static $libSaturate = [['color', 'amount'], ['number']];
+    protected static $libSaturate = [['color', 'amount'], ['amount']];
     protected function libSaturate($args)
     {
         $value = $args[0];
 
-        if ($value[0] === Type::T_NUMBER) {
+        if ($value instanceof Number) {
             return null;
         }
 
+        if (count($args) === 1) {
+            $val = $this->compileValue($value);
+            throw $this->error("\$amount: $val is not a number");
+        }
+
         $color = $this->assertColor($value);
         $amount = 100 * $this->coercePercent($args[1]);
 
@@ -6365,7 +6991,7 @@ class Compiler
     {
         $value = $args[0];
 
-        if ($value[0] === Type::T_NUMBER) {
+        if ($value instanceof Number) {
             return null;
         }
 
@@ -6389,7 +7015,7 @@ class Compiler
             $weight = $this->coercePercent($weight);
         }
 
-        if ($value[0] === Type::T_NUMBER) {
+        if ($value instanceof Number) {
             return null;
         }
 
@@ -6400,7 +7026,7 @@ class Compiler
         $inverted[3] = 255 - $inverted[3];
 
         if ($weight < 1) {
-            return $this->libMix([$inverted, $color, [Type::T_NUMBER, $weight]]);
+            return $this->libMix([$inverted, $color, new Number($weight, '')]);
         }
 
         return $inverted;
@@ -6462,6 +7088,7 @@ class Compiler
         $value = $args[0];
 
         if ($value[0] === Type::T_STRING && ! empty($value[1])) {
+            $value[1] = '"';
             return $value;
         }
 
@@ -6471,120 +7098,86 @@ class Compiler
     protected static $libPercentage = ['number'];
     protected function libPercentage($args)
     {
-        return new Node\Number($this->coercePercent($args[0]) * 100, '%');
+        $num = $this->assertNumber($args[0], 'number');
+        $num->assertNoUnits('number');
+
+        return new Number($num->getDimension() * 100, '%');
     }
 
     protected static $libRound = ['number'];
     protected function libRound($args)
     {
-        $num = $args[0];
+        $num = $this->assertNumber($args[0], 'number');
 
-        return new Node\Number(round($num[1]), $num[2]);
+        return new Number(round($num->getDimension()), $num->getNumeratorUnits(), $num->getDenominatorUnits());
     }
 
     protected static $libFloor = ['number'];
     protected function libFloor($args)
     {
-        $num = $args[0];
+        $num = $this->assertNumber($args[0], 'number');
 
-        return new Node\Number(floor($num[1]), $num[2]);
+        return new Number(floor($num->getDimension()), $num->getNumeratorUnits(), $num->getDenominatorUnits());
     }
 
     protected static $libCeil = ['number'];
     protected function libCeil($args)
     {
-        $num = $args[0];
+        $num = $this->assertNumber($args[0], 'number');
 
-        return new Node\Number(ceil($num[1]), $num[2]);
+        return new Number(ceil($num->getDimension()), $num->getNumeratorUnits(), $num->getDenominatorUnits());
     }
 
     protected static $libAbs = ['number'];
     protected function libAbs($args)
     {
-        $num = $args[0];
+        $num = $this->assertNumber($args[0], 'number');
 
-        return new Node\Number(abs($num[1]), $num[2]);
+        return new Number(abs($num->getDimension()), $num->getNumeratorUnits(), $num->getDenominatorUnits());
     }
 
     protected function libMin($args)
     {
-        $numbers = $this->getNormalizedNumbers($args);
-        $minOriginal = null;
-        $minNormalized = null;
+        /**
+         * @var Number|null
+         */
+        $min = null;
 
-        foreach ($numbers as $key => $pair) {
-            list($original, $normalized) = $pair;
+        foreach ($args as $arg) {
+            $number = $this->assertNumber($arg);
 
-            if (\is_null($normalized) or \is_null($minNormalized)) {
-                if (\is_null($minOriginal) || $original[1] <= $minOriginal[1]) {
-                    $minOriginal = $original;
-                    $minNormalized = $normalized;
-                }
-            } elseif ($normalized[1] <= $minNormalized[1]) {
-                $minOriginal = $original;
-                $minNormalized = $normalized;
+            if (\is_null($min) || $min->greaterThan($number)) {
+                $min = $number;
             }
         }
 
-        return $minOriginal;
-    }
-
-    protected function libMax($args)
-    {
-        $numbers = $this->getNormalizedNumbers($args);
-        $maxOriginal = null;
-        $maxNormalized = null;
-
-        foreach ($numbers as $key => $pair) {
-            list($original, $normalized) = $pair;
-
-            if (\is_null($normalized) or \is_null($maxNormalized)) {
-                if (\is_null($maxOriginal) || $original[1] >= $maxOriginal[1]) {
-                    $maxOriginal = $original;
-                    $maxNormalized = $normalized;
-                }
-            } elseif ($normalized[1] >= $maxNormalized[1]) {
-                $maxOriginal = $original;
-                $maxNormalized = $normalized;
-            }
+        if (!\is_null($min)) {
+            return $min;
         }
 
-        return $maxOriginal;
+        throw $this->error('At least one argument must be passed.');
     }
 
-    /**
-     * Helper to normalize args containing numbers
-     *
-     * @param array $args
-     *
-     * @return array
-     */
-    protected function getNormalizedNumbers($args)
+    protected function libMax($args)
     {
-        $unit         = null;
-        $originalUnit = null;
-        $numbers      = [];
-
-        foreach ($args as $key => $item) {
-            if ($item[0] !== Type::T_NUMBER) {
-                $this->throwError('%s is not a number', $item[0]);
-                break;
-            }
+        /**
+         * @var Number|null
+         */
+        $max = null;
 
-            $number = $item->normalize();
+        foreach ($args as $arg) {
+            $number = $this->assertNumber($arg);
 
-            if (empty($unit)) {
-                $unit = $number[2];
-                $originalUnit = $item->unitStr();
-            } elseif ($number[1] && $unit !== $number[2] && ! empty($number[2])) {
-                $this->throwError('Incompatible units: "%s" and "%s".', $originalUnit, $item->unitStr());
-                break;
+            if (\is_null($max) || $max->lessThan($number)) {
+                $max = $number;
             }
+        }
 
-            $numbers[$key] = [$args[$key], empty($number[2]) ? null : $number];
+        if (!\is_null($max)) {
+            return $max;
         }
 
-        return $numbers;
+        throw $this->error('At least one argument must be passed.');
     }
 
     protected static $libLength = ['list'];
@@ -6602,9 +7195,13 @@ class Compiler
             return 'comma';
         }
 
+        if (! \in_array($args[0][0], [Type::T_LIST, Type::T_MAP])) {
+            return 'space';
+        }
+
         $list = $this->coerceList($args[0]);
 
-        if (\count($list[2]) <= 1) {
+        if (\count($list[2]) <= 1 && empty($list['enclosing'])) {
             return 'space';
         }
 
@@ -6619,7 +7216,7 @@ class Compiler
     protected function libNth($args)
     {
         $list = $this->coerceList($args[0], ',', false);
-        $n = $this->assertNumber($args[1]);
+        $n = $this->assertNumber($args[1])->getDimension();
 
         if ($n > 0) {
             $n--;
@@ -6634,7 +7231,7 @@ class Compiler
     protected function libSetNth($args)
     {
         $list = $this->coerceList($args[0]);
-        $n = $this->assertNumber($args[1]);
+        $n = $this->assertNumber($args[1])->getDimension();
 
         if ($n > 0) {
             $n--;
@@ -6643,9 +7240,7 @@ class Compiler
         }
 
         if (! isset($list[2][$n])) {
-            $this->throwError('Invalid argument for "n"');
-
-            return null;
+            throw $this->error('Invalid argument for "n"');
         }
 
         $list[2][$n] = $args[2];
@@ -6690,14 +7285,20 @@ class Compiler
         return [Type::T_LIST, ',', $values];
     }
 
-    protected static $libMapRemove = ['map', 'key'];
+    protected static $libMapRemove = ['map', 'key...'];
     protected function libMapRemove($args)
     {
         $map = $this->assertMap($args[0]);
-        $key = $this->compileStringContent($this->coerceString($args[1]));
+        $keyList = $this->assertList($args[1]);
+
+        $keys = [];
+
+        foreach ($keyList[2] as $key) {
+            $keys[] = $this->compileStringContent($this->coerceString($key));
+        }
 
         for ($i = \count($map[1]) - 1; $i >= 0; $i--) {
-            if ($key === $this->compileStringContent($this->coerceString($map[1][$i]))) {
+            if (in_array($this->compileStringContent($this->coerceString($map[1][$i])), $keys)) {
                 array_splice($map[1], $i, 1);
                 array_splice($map[2], $i, 1);
             }
@@ -6721,7 +7322,10 @@ class Compiler
         return false;
     }
 
-    protected static $libMapMerge = ['map-1', 'map-2'];
+    protected static $libMapMerge = [
+        ['map1', 'map2'],
+        ['map-1', 'map-2']
+    ];
     protected function libMapMerge($args)
     {
         $map1 = $this->assertMap($args[0]);
@@ -6863,21 +7467,28 @@ class Compiler
         $lists = [];
         $firstList = array_shift($args);
 
-        foreach ($firstList[2] as $key => $item) {
-            $list = [Type::T_LIST, '', [$item]];
+        $result = [Type::T_LIST, ',', $lists];
+        if (! \is_null($firstList)) {
+            foreach ($firstList[2] as $key => $item) {
+                $list = [Type::T_LIST, '', [$item]];
 
-            foreach ($args as $arg) {
-                if (isset($arg[2][$key])) {
-                    $list[2][] = $arg[2][$key];
-                } else {
-                    break 2;
+                foreach ($args as $arg) {
+                    if (isset($arg[2][$key])) {
+                        $list[2][] = $arg[2][$key];
+                    } else {
+                        break 2;
+                    }
                 }
+
+                $lists[] = $list;
             }
 
-            $lists[] = $list;
+            $result[2] = $lists;
+        } else {
+            $result['enclosing'] = 'parent';
         }
 
-        return [Type::T_LIST, ',', $lists];
+        return $result;
     }
 
     protected static $libTypeOf = ['value'];
@@ -6899,6 +7510,9 @@ class Compiler
             case Type::T_FUNCTION:
                 return 'string';
 
+            case Type::T_FUNCTION_REFERENCE:
+                return 'function';
+
             case Type::T_LIST:
                 if (isset($value[3]) && $value[3]) {
                     return 'arglist';
@@ -6915,7 +7529,7 @@ class Compiler
     {
         $num = $args[0];
 
-        if ($num[0] === Type::T_NUMBER) {
+        if ($num instanceof Number) {
             return [Type::T_STRING, '"', [$num->unitStr()]];
         }
 
@@ -6927,54 +7541,67 @@ class Compiler
     {
         $value = $args[0];
 
-        return $value[0] === Type::T_NUMBER && $value->unitless();
+        return $value instanceof Number && $value->unitless();
     }
 
-    protected static $libComparable = ['number-1', 'number-2'];
+    protected static $libComparable = [
+        ['number1', 'number2'],
+        ['number-1', 'number-2']
+    ];
     protected function libComparable($args)
     {
         list($number1, $number2) = $args;
 
-        if (! isset($number1[0]) || $number1[0] !== Type::T_NUMBER ||
-            ! isset($number2[0]) || $number2[0] !== Type::T_NUMBER
+        if (
+            ! $number1 instanceof Number ||
+            ! $number2 instanceof Number
         ) {
-            $this->throwError('Invalid argument(s) for "comparable"');
-
-            return null;
+            throw $this->error('Invalid argument(s) for "comparable"');
         }
 
-        $number1 = $number1->normalize();
-        $number2 = $number2->normalize();
-
-        return $number1[2] === $number2[2] || $number1->unitless() || $number2->unitless();
+        return $number1->isComparableTo($number2);
     }
 
     protected static $libStrIndex = ['string', 'substring'];
     protected function libStrIndex($args)
     {
-        $string = $this->coerceString($args[0]);
+        $string = $this->assertString($args[0], 'string');
         $stringContent = $this->compileStringContent($string);
 
-        $substring = $this->coerceString($args[1]);
+        $substring = $this->assertString($args[1], 'substring');
         $substringContent = $this->compileStringContent($substring);
 
-        $result = strpos($stringContent, $substringContent);
+        if (! \strlen($substringContent)) {
+            $result = 0;
+        } else {
+            $result = strpos($stringContent, $substringContent);
+        }
 
-        return $result === false ? static::$null : new Node\Number($result + 1, '');
+        return $result === false ? static::$null : new Number($result + 1, '');
     }
 
     protected static $libStrInsert = ['string', 'insert', 'index'];
     protected function libStrInsert($args)
     {
-        $string = $this->coerceString($args[0]);
+        $string = $this->assertString($args[0], 'string');
         $stringContent = $this->compileStringContent($string);
 
-        $insert = $this->coerceString($args[1]);
+        $insert = $this->assertString($args[1], 'insert');
         $insertContent = $this->compileStringContent($insert);
 
-        list(, $index) = $args[2];
+        $index = $this->assertInteger($args[2], 'index');
+        if ($index > 0) {
+            $index = $index - 1;
+        }
+        if ($index < 0) {
+            $index = Util::mbStrlen($stringContent) + 1 + $index;
+        }
 
-        $string[2] = [substr_replace($stringContent, $insertContent, $index - 1, 0)];
+        $string[2] = [
+            Util::mbSubstr($stringContent, 0, $index),
+            $insertContent,
+            Util::mbSubstr($stringContent, $index)
+        ];
 
         return $string;
     }
@@ -6982,10 +7609,10 @@ class Compiler
     protected static $libStrLength = ['string'];
     protected function libStrLength($args)
     {
-        $string = $this->coerceString($args[0]);
+        $string = $this->assertString($args[0], 'string');
         $stringContent = $this->compileStringContent($string);
 
-        return new Node\Number(\strlen($stringContent), '');
+        return new Number(Util::mbStrlen($stringContent), '');
     }
 
     protected static $libStrSlice = ['string', 'start-at', 'end-at:-1'];
@@ -7111,28 +7738,25 @@ class Compiler
         return [Type::T_STRING, '', ['counter(' . implode(',', $list) . ')']];
     }
 
-    protected static $libRandom = ['limit:1'];
+    protected static $libRandom = ['limit:null'];
     protected function libRandom($args)
     {
-        if (isset($args[0])) {
-            $n = $this->assertNumber($args[0]);
+        if (isset($args[0]) & $args[0] !== static::$null) {
+            $n = $this->assertNumber($args[0])->getDimension();
 
             if ($n < 1) {
-                $this->throwError("\$limit must be greater than or equal to 1");
-
-                return null;
+                throw $this->error("\$limit must be greater than or equal to 1");
             }
 
-            if ($n - \intval($n) > 0) {
-                $this->throwError("Expected \$limit to be an integer but got $n for `random`");
-
-                return null;
+            if (round($n - \intval($n), Number::PRECISION) > 0) {
+                throw $this->error("Expected \$limit to be an integer but got $n for `random`");
             }
 
-            return new Node\Number(mt_rand(1, \intval($n)), '');
+            return new Number(mt_rand(1, \intval($n)), '');
         }
 
-        return new Node\Number(mt_rand(1, mt_getrandmax()), '');
+        $max = mt_getrandmax();
+        return new Number(mt_rand(0, $max - 1) / $max, '');
     }
 
     protected function libUniqueId()
@@ -7165,12 +7789,13 @@ class Compiler
                 $force_enclosing_display = true;
             }
 
-            if (! empty($value['enclosing']) &&
+            if (
+                ! empty($value['enclosing']) &&
                 ($force_enclosing_display ||
                     ($value['enclosing'] === 'bracket') ||
                     ! \count($value[2]))
             ) {
-                $value['enclosing'] = 'forced_'.$value['enclosing'];
+                $value['enclosing'] = 'forced_' . $value['enclosing'];
                 $force_enclosing_display = true;
             }
 
@@ -7199,7 +7824,7 @@ class Compiler
      *
      * @return array|boolean
      */
-    protected function getSelectorArg($arg)
+    protected function getSelectorArg($arg, $varname = null, $allowParent = false)
     {
         static $parser = null;
 
@@ -7207,19 +7832,60 @@ class Compiler
             $parser = $this->parserFactory(__METHOD__);
         }
 
+        if (! $this->checkSelectorArgType($arg)) {
+            $var_display = ($varname ? ' $' . $varname . ':' : '');
+            $var_value = $this->compileValue($arg);
+            throw $this->error("Error:{$var_display} $var_value is not a valid selector: it must be a string,"
+                . " a list of strings, or a list of lists of strings");
+        }
+
         $arg = $this->libUnquote([$arg]);
         $arg = $this->compileValue($arg);
 
         $parsedSelector = [];
 
-        if ($parser->parseSelector($arg, $parsedSelector)) {
+        if ($parser->parseSelector($arg, $parsedSelector, true)) {
             $selector = $this->evalSelectors($parsedSelector);
             $gluedSelector = $this->glueFunctionSelectors($selector);
 
+            if (! $allowParent) {
+                foreach ($gluedSelector as $selector) {
+                    foreach ($selector as $s) {
+                        if (in_array(static::$selfSelector, $s)) {
+                            $var_display = ($varname ? ' $' . $varname . ':' : '');
+                            throw $this->error("Error:{$var_display} Parent selectors aren't allowed here.");
+                        }
+                    }
+                }
+            }
+
             return $gluedSelector;
         }
 
-        return false;
+        $var_display = ($varname ? ' $' . $varname . ':' : '');
+        throw $this->error("Error:{$var_display} expected more input, invalid selector.");
+    }
+
+    /**
+     * Check variable type for getSelectorArg() function
+     * @param array $arg
+     * @param int $maxDepth
+     * @return bool
+     */
+    protected function checkSelectorArgType($arg, $maxDepth = 2)
+    {
+        if ($arg[0] === Type::T_LIST && $maxDepth > 0) {
+            foreach ($arg[2] as $elt) {
+                if (! $this->checkSelectorArgType($elt, $maxDepth - 1)) {
+                    return false;
+                }
+            }
+            return true;
+        }
+        if (!in_array($arg[0], [Type::T_STRING, Type::T_KEYWORD])) {
+            return false;
+        }
+        return true;
     }
 
     /**
@@ -7241,8 +7907,8 @@ class Compiler
     {
         list($super, $sub) = $args;
 
-        $super = $this->getSelectorArg($super);
-        $sub = $this->getSelectorArg($sub);
+        $super = $this->getSelectorArg($super, 'super');
+        $sub = $this->getSelectorArg($sub, 'sub');
 
         return $this->isSuperSelector($super, $sub);
     }
@@ -7258,12 +7924,30 @@ class Compiler
     protected function isSuperSelector($super, $sub)
     {
         // one and only one selector for each arg
-        if (! $super || \count($super) !== 1) {
-            $this->throwError("Invalid super selector for isSuperSelector()");
+        if (! $super) {
+            throw $this->error('Invalid super selector for isSuperSelector()');
         }
 
-        if (! $sub || \count($sub) !== 1) {
-            $this->throwError("Invalid sub selector for isSuperSelector()");
+        if (! $sub) {
+            throw $this->error('Invalid sub selector for isSuperSelector()');
+        }
+
+        if (count($sub) > 1) {
+            foreach ($sub as $s) {
+                if (! $this->isSuperSelector($super, [$s])) {
+                    return false;
+                }
+            }
+            return true;
+        }
+
+        if (count($super) > 1) {
+            foreach ($super as $s) {
+                if ($this->isSuperSelector([$s], $sub)) {
+                    return true;
+                }
+            }
+            return false;
         }
 
         $super = reset($super);
@@ -7345,10 +8029,13 @@ class Compiler
         $args = $args[2];
 
         if (\count($args) < 1) {
-            $this->throwError("selector-append() needs at least 1 argument");
+            throw $this->error('selector-append() needs at least 1 argument');
         }
 
-        $selectors = array_map([$this, 'getSelectorArg'], $args);
+        $selectors = [];
+        foreach ($args as $arg) {
+            $selectors[] = $this->getSelectorArg($arg, 'selector');
+        }
 
         return $this->formatOutputSelector($this->selectorAppend($selectors));
     }
@@ -7367,14 +8054,14 @@ class Compiler
         $lastSelectors = array_pop($selectors);
 
         if (! $lastSelectors) {
-            $this->throwError("Invalid selector list in selector-append()");
+            throw $this->error('Invalid selector list in selector-append()');
         }
 
         while (\count($selectors)) {
             $previousSelectors = array_pop($selectors);
 
             if (! $previousSelectors) {
-                $this->throwError("Invalid selector list in selector-append()");
+                throw $this->error('Invalid selector list in selector-append()');
             }
 
             // do the trick, happening $lastSelector to $previousSelector
@@ -7404,17 +8091,20 @@ class Compiler
         return $lastSelectors;
     }
 
-    protected static $libSelectorExtend = ['selectors', 'extendee', 'extender'];
+    protected static $libSelectorExtend = [
+        ['selector', 'extendee', 'extender'],
+        ['selectors', 'extendee', 'extender']
+    ];
     protected function libSelectorExtend($args)
     {
         list($selectors, $extendee, $extender) = $args;
 
-        $selectors = $this->getSelectorArg($selectors);
-        $extendee  = $this->getSelectorArg($extendee);
-        $extender  = $this->getSelectorArg($extender);
+        $selectors = $this->getSelectorArg($selectors, 'selector');
+        $extendee  = $this->getSelectorArg($extendee, 'extendee');
+        $extender  = $this->getSelectorArg($extender, 'extender');
 
         if (! $selectors || ! $extendee || ! $extender) {
-            $this->throwError("selector-extend() invalid arguments");
+            throw $this->error('selector-extend() invalid arguments');
         }
 
         $extended = $this->extendOrReplaceSelectors($selectors, $extendee, $extender);
@@ -7422,17 +8112,20 @@ class Compiler
         return $this->formatOutputSelector($extended);
     }
 
-    protected static $libSelectorReplace = ['selectors', 'original', 'replacement'];
+    protected static $libSelectorReplace = [
+        ['selector', 'original', 'replacement'],
+        ['selectors', 'original', 'replacement']
+    ];
     protected function libSelectorReplace($args)
     {
         list($selectors, $original, $replacement) = $args;
 
-        $selectors   = $this->getSelectorArg($selectors);
-        $original    = $this->getSelectorArg($original);
-        $replacement = $this->getSelectorArg($replacement);
+        $selectors   = $this->getSelectorArg($selectors, 'selector');
+        $original    = $this->getSelectorArg($original, 'original');
+        $replacement = $this->getSelectorArg($replacement, 'replacement');
 
         if (! $selectors || ! $original || ! $replacement) {
-            $this->throwError("selector-replace() invalid arguments");
+            throw $this->error('selector-replace() invalid arguments');
         }
 
         $replaced = $this->extendOrReplaceSelectors($selectors, $original, $replacement, true);
@@ -7476,7 +8169,7 @@ class Compiler
             $this->matchExtends($selector, $extended);
 
             // if didnt match, keep the original selector if we are in a replace operation
-            if ($replace and \count($extended) === $n) {
+            if ($replace && \count($extended) === $n) {
                 $extended[] = $selector;
             }
         }
@@ -7495,10 +8188,14 @@ class Compiler
         $args = $args[2];
 
         if (\count($args) < 1) {
-            $this->throwError("selector-nest() needs at least 1 argument");
+            throw $this->error('selector-nest() needs at least 1 argument');
+        }
+
+        $selectorsMap = [];
+        foreach ($args as $arg) {
+            $selectorsMap[] = $this->getSelectorArg($arg, 'selector', true);
         }
 
-        $selectorsMap = array_map([$this, 'getSelectorArg'], $args);
         $envs = [];
 
         foreach ($selectorsMap as $selectors) {
@@ -7515,11 +8212,14 @@ class Compiler
         return $this->formatOutputSelector($outputSelectors);
     }
 
-    protected static $libSelectorParse = ['selectors'];
+    protected static $libSelectorParse = [
+        ['selector'],
+        ['selectors']
+    ];
     protected function libSelectorParse($args)
     {
         $selectors = reset($args);
-        $selectors = $this->getSelectorArg($selectors);
+        $selectors = $this->getSelectorArg($selectors, 'selector');
 
         return $this->formatOutputSelector($selectors);
     }
@@ -7529,11 +8229,11 @@ class Compiler
     {
         list($selectors1, $selectors2) = $args;
 
-        $selectors1 = $this->getSelectorArg($selectors1);
-        $selectors2 = $this->getSelectorArg($selectors2);
+        $selectors1 = $this->getSelectorArg($selectors1, 'selectors1');
+        $selectors2 = $this->getSelectorArg($selectors2, 'selectors2');
 
         if (! $selectors1 || ! $selectors2) {
-            $this->throwError("selector-unify() invalid arguments");
+            throw $this->error('selector-unify() invalid arguments');
         }
 
         // only consider the first compound of each
@@ -7806,7 +8506,7 @@ class Compiler
     protected function libSimpleSelectors($args)
     {
         $selector = reset($args);
-        $selector = $this->getSelectorArg($selector);
+        $selector = $this->getSelectorArg($selector, 'selector');
 
         // remove selectors list layer, keeping the first one
         $selector = reset($selector);
index c91ec96a21139a191e34e09e8587fbd77bdf2646..343da4c7028271fb4da10a2d795777d91874d2c0 100644 (file)
@@ -1,4 +1,5 @@
 <?php
+
 /**
  * SCSSPHP
  *
@@ -16,6 +17,6 @@ namespace ScssPhp\ScssPhp\Exception;
  *
  * @author Oleksandr Savchenko <traveltino@gmail.com>
  */
-class CompilerException extends \Exception
+class CompilerException extends \Exception implements SassException
 {
 }
index 62e9df4604377981ad42e127de234fb213bb5d2a..5237f30795d476e09f5076cfa71058e4c4db63ec 100644 (file)
@@ -1,4 +1,5 @@
 <?php
+
 /**
  * SCSSPHP
  *
@@ -16,6 +17,32 @@ namespace ScssPhp\ScssPhp\Exception;
  *
  * @author Oleksandr Savchenko <traveltino@gmail.com>
  */
-class ParserException extends \Exception
+class ParserException extends \Exception implements SassException
 {
+    /**
+     * @var array
+     */
+    private $sourcePosition;
+
+    /**
+     * Get source position
+     *
+     * @api
+     */
+    public function getSourcePosition()
+    {
+        return $this->sourcePosition;
+    }
+
+    /**
+     * Set source position
+     *
+     * @api
+     *
+     * @param array $sourcePosition
+     */
+    public function setSourcePosition($sourcePosition)
+    {
+        $this->sourcePosition = $sourcePosition;
+    }
 }
index 5f7537f9fe7fbe242ef43572b21eab411e0840a7..b18c32d6c3d95766eba14e11b01951c74f51fcad 100644 (file)
@@ -1,4 +1,5 @@
 <?php
+
 /**
  * SCSSPHP
  *
@@ -16,6 +17,6 @@ namespace ScssPhp\ScssPhp\Exception;
  *
  * @author Anthon Pang <anthon.pang@gmail.com>
  */
-class RangeException extends \Exception
+class RangeException extends \Exception implements SassException
 {
 }
diff --git a/wcfsetup/install/files/lib/system/api/scssphp/scssphp/src/Exception/SassException.php b/wcfsetup/install/files/lib/system/api/scssphp/scssphp/src/Exception/SassException.php
new file mode 100644 (file)
index 0000000..9f62b3c
--- /dev/null
@@ -0,0 +1,7 @@
+<?php
+
+namespace ScssPhp\ScssPhp\Exception;
+
+interface SassException
+{
+}
diff --git a/wcfsetup/install/files/lib/system/api/scssphp/scssphp/src/Exception/SassScriptException.php b/wcfsetup/install/files/lib/system/api/scssphp/scssphp/src/Exception/SassScriptException.php
new file mode 100644 (file)
index 0000000..7812379
--- /dev/null
@@ -0,0 +1,14 @@
+<?php
+
+namespace ScssPhp\ScssPhp\Exception;
+
+/**
+ * Internal exception thrown in places not having access to reporting the location.
+ *
+ * This class does not implement SassException on purpose, as it should never be returned to the outside code.
+ *
+ * @internal
+ */
+class SassScriptException extends \Exception
+{
+}
index 48533442930c5310f8910192b860c1c6bca7da54..ad5b37998f5bfddbe8d59cdcf50bee9adb732247 100644 (file)
@@ -1,4 +1,5 @@
 <?php
+
 /**
  * SCSSPHP
  *
@@ -16,6 +17,6 @@ namespace ScssPhp\ScssPhp\Exception;
  *
  * @author Anthon Pang <anthon.pang@gmail.com>
  */
-class ServerException extends \Exception
+class ServerException extends \Exception implements SassException
 {
 }
index eb09c394d745bfdb31d8f19a0cf4c1a95b150a2b..290f5f3c74d334fdd1203977a90e91ba2ebd4fa9 100644 (file)
@@ -1,4 +1,5 @@
 <?php
+
 /**
  * SCSSPHP
  *
@@ -297,7 +298,8 @@ abstract class Formatter
          * Maybe Strip semi-colon appended by property(); it's a separator, not a terminator
          * will be striped for real before a closing, otherwise displayed unchanged starting the next write
          */
-        if (! $this->keepSemicolons &&
+        if (
+            ! $this->keepSemicolons &&
             $str &&
             (strpos($str, ';') !== false) &&
             (substr($str, -1) === ';')
@@ -319,7 +321,7 @@ abstract class Formatter
 
             $lines = explode("\n", $str);
             $lineCount = \count($lines);
-            $this->currentLine += $lineCount-1;
+            $this->currentLine += $lineCount - 1;
 
             $lastLine = array_pop($lines);
 
index e3510fac92b29ed41250ba086831887cb7e596c5..3d5344cfd009ee45699a8079a60cc75e7c3b1bd4 100644 (file)
@@ -1,4 +1,5 @@
 <?php
+
 /**
  * SCSSPHP
  *
@@ -55,7 +56,7 @@ class Expanded extends Formatter
 
         foreach ($block->lines as $index => $line) {
             if (substr($line, 0, 2) === '/*') {
-                $block->lines[$index] = preg_replace('/[\r\n]+/', $glue, $line);
+                $block->lines[$index] = preg_replace('/\r\n?|\n|\f/', $this->break, $line);
             }
         }
 
index 7179908c20902e295eef2d3eeb2fc09bd7952ac6..046ecc85b004cea2996f01775cf990b64cded6f3 100644 (file)
@@ -1,4 +1,5 @@
 <?php
+
 /**
  * SCSSPHP
  *
@@ -62,7 +63,7 @@ class Nested extends Formatter
 
         foreach ($block->lines as $index => $line) {
             if (substr($line, 0, 2) === '/*') {
-                $block->lines[$index] = preg_replace('/[\r\n]+/', $glue, $line);
+                $block->lines[$index] = preg_replace('/\r\n?|\n|\f/', $this->break, $line);
             }
         }
 
@@ -97,7 +98,8 @@ class Nested extends Formatter
             array_pop($depths);
             $this->depth--;
 
-            if (! $this->depth && ($block->depth <= 1 || (! $this->indentLevel && $block->type === Type::T_COMMENT)) &&
+            if (
+                ! $this->depth && ($block->depth <= 1 || (! $this->indentLevel && $block->type === Type::T_COMMENT)) &&
                 (($block->selectors && ! $isMediaOrDirective) || $previousHasSelector)
             ) {
                 $downLevel = $this->break;
index 4e6f664648f7f082102f62bf9e97e7e68c7b1669..1ebd30d7ab02f4b8bb10ba52385e11f656d3e874 100644 (file)
@@ -1,4 +1,5 @@
 <?php
+
 /**
  * SCSSPHP
  *
@@ -12,6 +13,7 @@
 namespace ScssPhp\ScssPhp\Node;
 
 use ScssPhp\ScssPhp\Compiler;
+use ScssPhp\ScssPhp\Exception\SassScriptException;
 use ScssPhp\ScssPhp\Node;
 use ScssPhp\ScssPhp\Type;
 
@@ -28,10 +30,13 @@ use ScssPhp\ScssPhp\Type;
  */
 class Number extends Node implements \ArrayAccess
 {
+    const PRECISION = 10;
+
     /**
      * @var integer
+     * @deprecated use {Number::PRECISION} instead to read the precision. Configuring it is not supported anymore.
      */
-    public static $precision = 10;
+    public static $precision = self::PRECISION;
 
     /**
      * @see http://www.w3.org/TR/2012/WD-css3-values-20120308/
@@ -64,82 +69,63 @@ class Number extends Node implements \ArrayAccess
         ],
         'dpi' => [
             'dpi'  => 1,
-            'dpcm' => 1/2.54,
-            'dppx' => 1/96,
+            'dpcm' => 1 / 2.54,
+            'dppx' => 1 / 96,
         ],
     ];
 
     /**
      * @var integer|float
      */
-    public $dimension;
+    private $dimension;
 
-    /**
-     * @var array
-     */
-    public $units;
+    private $numeratorUnits;
+    private $denominatorUnits;
 
     /**
      * Initialize number
      *
-     * @param mixed $dimension
-     * @param mixed $initialUnit
+     * @param integer|float   $dimension
+     * @param string[]|string $numeratorUnits
+     * @param string[]        $denominatorUnits
      */
-    public function __construct($dimension, $initialUnit)
+    public function __construct($dimension, $numeratorUnits, array $denominatorUnits = [])
     {
-        $this->type      = Type::T_NUMBER;
+        if (is_string($numeratorUnits)) {
+            $numeratorUnits = $numeratorUnits ? [$numeratorUnits] : [];
+        } elseif (isset($numeratorUnits['numerator_units'], $numeratorUnits['denominator_units'])) {
+            // TODO get rid of this once `$number[2]` is not used anymore
+            $denominatorUnits = $numeratorUnits['denominator_units'];
+            $numeratorUnits = $numeratorUnits['numerator_units'];
+        }
+
         $this->dimension = $dimension;
-        $this->units     = \is_array($initialUnit)
-            ? $initialUnit
-            : ($initialUnit ? [$initialUnit => 1]
-                            : []);
+        $this->numeratorUnits = $numeratorUnits;
+        $this->denominatorUnits = $denominatorUnits;
     }
 
     /**
-     * Coerce number to target units
-     *
-     * @param array $units
-     *
-     * @return \ScssPhp\ScssPhp\Node\Number
+     * @return float|int
      */
-    public function coerce($units)
+    public function getDimension()
     {
-        if ($this->unitless()) {
-            return new Number($this->dimension, $units);
-        }
-
-        $dimension = $this->dimension;
-
-        if (\count($units)) {
-            $baseUnit = array_keys($units);
-            $baseUnit = reset($baseUnit);
-            $baseUnit = $this->findBaseUnit($baseUnit);
-            if ($baseUnit && isset(static::$unitTable[$baseUnit])) {
-                foreach (static::$unitTable[$baseUnit] as $unit => $conv) {
-                    $from       = isset($this->units[$unit]) ? $this->units[$unit] : 0;
-                    $to         = isset($units[$unit]) ? $units[$unit] : 0;
-                    $factor     = pow($conv, $from - $to);
-                    $dimension /= $factor;
-                }
-            }
-        }
-
-        return new Number($dimension, $units);
+        return $this->dimension;
     }
 
     /**
-     * Normalize number
-     *
-     * @return \ScssPhp\ScssPhp\Node\Number
+     * @return string[]
      */
-    public function normalize()
+    public function getNumeratorUnits()
     {
-        $dimension = $this->dimension;
-        $units     = [];
-
-        $this->normalizeUnits($dimension, $units);
+        return $this->numeratorUnits;
+    }
 
-        return new Number($dimension, $units);
+    /**
+     * @return string[]
+     */
+    public function getDenominatorUnits()
+    {
+        return $this->denominatorUnits;
     }
 
     /**
@@ -155,7 +141,8 @@ class Number extends Node implements \ArrayAccess
             return ! \is_null($this->sourceLine);
         }
 
-        if ($offset === -1 ||
+        if (
+            $offset === -1 ||
             $offset === 0 ||
             $offset === 1 ||
             $offset === 2
@@ -182,13 +169,13 @@ class Number extends Node implements \ArrayAccess
                 return $this->sourceIndex;
 
             case 0:
-                return $this->type;
+                return Type::T_NUMBER;
 
             case 1:
                 return $this->dimension;
 
             case 2:
-                return $this->units;
+                return array('numerator_units' => $this->numeratorUnits, 'denominator_units' => $this->denominatorUnits);
         }
     }
 
@@ -197,17 +184,7 @@ class Number extends Node implements \ArrayAccess
      */
     public function offsetSet($offset, $value)
     {
-        if ($offset === 1) {
-            $this->dimension = $value;
-        } elseif ($offset === 2) {
-            $this->units = $value;
-        } elseif ($offset == -1) {
-            $this->sourceIndex = $value;
-        } elseif ($offset == -2) {
-            $this->sourceLine = $value;
-        } elseif ($offset == -3) {
-            $this->sourceColumn = $value;
-        }
+        throw new \BadMethodCallException('Number is immutable');
     }
 
     /**
@@ -215,17 +192,7 @@ class Number extends Node implements \ArrayAccess
      */
     public function offsetUnset($offset)
     {
-        if ($offset === 1) {
-            $this->dimension = null;
-        } elseif ($offset === 2) {
-            $this->units = null;
-        } elseif ($offset === -1) {
-            $this->sourceIndex = null;
-        } elseif ($offset === -2) {
-            $this->sourceLine = null;
-        } elseif ($offset === -3) {
-            $this->sourceColumn = null;
-        }
+        throw new \BadMethodCallException('Number is immutable');
     }
 
     /**
@@ -235,61 +202,238 @@ class Number extends Node implements \ArrayAccess
      */
     public function unitless()
     {
-        return ! array_sum($this->units);
+        return \count($this->numeratorUnits) === 0 && \count($this->denominatorUnits) === 0;
     }
 
     /**
-     * Test if a number can be normalized in a base unit
-     * ie if its units are homogeneous
+     * Checks whether the number has exactly this unit
      *
-     * @return boolean
+     * @param string $unit
+     *
+     * @return bool
+     */
+    public function hasUnit($unit)
+    {
+        return \count($this->numeratorUnits) === 1 && \count($this->denominatorUnits) === 0 && $this->numeratorUnits[0] === $unit;
+    }
+
+    /**
+     * Returns unit(s) as the product of numerator units divided by the product of denominator units
+     *
+     * @return string
      */
-    public function isNormalizable()
+    public function unitStr()
+    {
+        if ($this->unitless()) {
+            return '';
+        }
+
+        return self::getUnitString($this->numeratorUnits, $this->denominatorUnits);
+    }
+
+    public function assertNoUnits($varName = null)
     {
         if ($this->unitless()) {
+            return;
+        }
+
+        $varDisplay = !\is_null($varName) ? "\${$varName}: " : '';
+
+        throw new SassScriptException(sprintf('%sExpected %s to have no units', $varDisplay, $this));
+    }
+
+    public function assertSameUnitOrUnitless(Number $other)
+    {
+        if ($other->unitless()) {
+            return;
+        }
+
+        if ($this->numeratorUnits === $other->numeratorUnits && $this->denominatorUnits === $other->denominatorUnits) {
+            return;
+        }
+
+        throw new SassScriptException(sprintf(
+            'Incompatible units %s and %s.',
+            self::getUnitString($this->numeratorUnits, $this->denominatorUnits),
+            self::getUnitString($other->numeratorUnits, $other->denominatorUnits)
+        ));
+    }
+
+    /**
+     * @param Number $other
+     *
+     * @return bool
+     */
+    public function isComparableTo(Number $other)
+    {
+        if ($this->unitless() || $other->unitless()) {
+            return true;
+        }
+
+        try {
+            $this->greaterThan($other);
+            return true;
+        } catch (SassScriptException $e) {
             return false;
         }
+    }
+
+    /**
+     * @param Number $other
+     *
+     * @return bool
+     */
+    public function lessThan(Number $other)
+    {
+        return $this->coerceUnits($other, function ($num1, $num2) {
+            return $num1 < $num2;
+        });
+    }
 
-        $baseUnit = null;
+    /**
+     * @param Number $other
+     *
+     * @return bool
+     */
+    public function lessThanOrEqual(Number $other)
+    {
+        return $this->coerceUnits($other, function ($num1, $num2) {
+            return $num1 <= $num2;
+        });
+    }
 
-        foreach ($this->units as $unit => $exp) {
-            $b = $this->findBaseUnit($unit);
+    /**
+     * @param Number $other
+     *
+     * @return bool
+     */
+    public function greaterThan(Number $other)
+    {
+        return $this->coerceUnits($other, function ($num1, $num2) {
+            return $num1 > $num2;
+        });
+    }
 
-            if (\is_null($baseUnit)) {
-                $baseUnit = $b;
+    /**
+     * @param Number $other
+     *
+     * @return bool
+     */
+    public function greaterThanOrEqual(Number $other)
+    {
+        return $this->coerceUnits($other, function ($num1, $num2) {
+            return $num1 >= $num2;
+        });
+    }
+
+    /**
+     * @param Number $other
+     *
+     * @return Number
+     */
+    public function plus(Number $other)
+    {
+        return $this->coerceNumber($other, function ($num1, $num2) {
+            return $num1 + $num2;
+        });
+    }
+
+    /**
+     * @param Number $other
+     *
+     * @return Number
+     */
+    public function minus(Number $other)
+    {
+        return $this->coerceNumber($other, function ($num1, $num2) {
+            return $num1 - $num2;
+        });
+    }
+
+    /**
+     * @return Number
+     */
+    public function unaryMinus()
+    {
+        return new Number(-$this->dimension, $this->numeratorUnits, $this->denominatorUnits);
+    }
+
+    /**
+     * @param Number $other
+     *
+     * @return Number
+     */
+    public function modulo(Number $other)
+    {
+        return $this->coerceNumber($other, function ($num1, $num2) {
+            if ($num2 == 0) {
+                return NAN;
             }
 
-            if (\is_null($b) or $b !== $baseUnit) {
-                return false;
+            return $num1 % $num2;
+        });
+    }
+
+    /**
+     * @param Number $other
+     *
+     * @return Number
+     */
+    public function times(Number $other)
+    {
+        return $this->multiplyUnits($this->dimension * $other->dimension, $this->numeratorUnits, $this->denominatorUnits, $other->numeratorUnits, $other->denominatorUnits);
+    }
+
+    /**
+     * @param Number $other
+     *
+     * @return Number
+     */
+    public function dividedBy(Number $other)
+    {
+        if ($other->dimension == 0) {
+            if ($this->dimension == 0) {
+                $value = NAN;
+            } elseif ($this->dimension > 0) {
+                $value = INF;
+            } else {
+                $value = -INF;
             }
+        } else {
+            $value = $this->dimension / $other->dimension;
         }
 
-        return $baseUnit;
+        return $this->multiplyUnits($value, $this->numeratorUnits, $this->denominatorUnits, $other->denominatorUnits, $other->numeratorUnits);
     }
 
     /**
-     * Returns unit(s) as the product of numerator units divided by the product of denominator units
+     * @param Number $other
      *
-     * @return string
+     * @return bool
      */
-    public function unitStr()
+    public function equals(Number $other)
     {
-        $numerators   = [];
-        $denominators = [];
+        // Unitless numbers are convertable to unit numbers, but not equal, so we special-case unitless here.
+        if ($this->unitless() !== $other->unitless()) {
+            return false;
+        }
 
-        foreach ($this->units as $unit => $unitSize) {
-            if ($unitSize > 0) {
-                $numerators = array_pad($numerators, \count($numerators) + $unitSize, $unit);
-                continue;
-            }
+        // In Sass, neither NaN nor Infinity are equal to themselves, while PHP defines INF==INF
+        if (is_nan($this->dimension) || is_nan($other->dimension) || !is_finite($this->dimension) || !is_finite($other->dimension)) {
+            return false;
+        }
 
-            if ($unitSize < 0) {
-                $denominators = array_pad($denominators, \count($denominators) - $unitSize, $unit);
-                continue;
-            }
+        if ($this->unitless()) {
+            return round($this->dimension, self::PRECISION) == round($other->dimension, self::PRECISION);
         }
 
-        return implode('*', $numerators) . (\count($denominators) ? '/' . implode('*', $denominators) : '');
+        try {
+            return $this->coerceUnits($other, function ($num1, $num2) {
+                return round($num1,self::PRECISION) == round($num2, self::PRECISION);
+            });
+        } catch (SassScriptException $e) {
+            return false;
+        }
     }
 
     /**
@@ -301,37 +445,31 @@ class Number extends Node implements \ArrayAccess
      */
     public function output(Compiler $compiler = null)
     {
-        $dimension = round($this->dimension, static::$precision);
-
-        $units = array_filter($this->units, function ($unitSize) {
-            return $unitSize;
-        });
-
-        if (\count($units) > 1 && array_sum($units) === 0) {
-            $dimension = $this->dimension;
-            $units     = [];
+        $dimension = round($this->dimension, self::PRECISION);
 
-            $this->normalizeUnits($dimension, $units);
+        if (is_nan($dimension)) {
+            return 'NaN';
+        }
 
-            $dimension = round($dimension, static::$precision);
-            $units     = array_filter($units, function ($unitSize) {
-                return $unitSize;
-            });
+        if ($dimension === INF) {
+            return 'Infinity';
         }
 
-        $unitSize = array_sum($units);
+        if ($dimension === -INF) {
+            return '-Infinity';
+        }
 
-        if ($compiler && ($unitSize > 1 || $unitSize < 0 || \count($units) > 1)) {
-            $this->units = $units;
+        if ($compiler) {
             $unit = $this->unitStr();
+        } elseif (isset($this->numeratorUnits[0])) {
+            $unit = $this->numeratorUnits[0];
         } else {
-            reset($units);
-            $unit = key($units);
+            $unit = '';
         }
 
-        $dimension = number_format($dimension, static::$precision, '.', '');
+        $dimension = number_format($dimension, self::PRECISION, '.', '');
 
-        return (static::$precision ? rtrim(rtrim($dimension, '0'), '.') : $dimension) . $unit;
+        return rtrim(rtrim($dimension, '0'), '.') . $unit;
     }
 
     /**
@@ -343,48 +481,216 @@ class Number extends Node implements \ArrayAccess
     }
 
     /**
-     * Normalize units
+     * @param Number   $other
+     * @param callable $operation
+     *
+     * @return Number
+     *
+     * @phpstan-param callable(int|float, int|float): int|float $operation
+     */
+    private function coerceNumber(Number $other, $operation)
+    {
+        $result = $this->coerceUnits($other, $operation);
+
+        if (!$this->unitless()) {
+            return new Number($result, $this->numeratorUnits, $this->denominatorUnits);
+        }
+
+        return new Number($result, $other->numeratorUnits, $other->denominatorUnits);
+    }
+
+    /**
+     * @param Number $other
+     * @param callable $operation
+     *
+     * @return mixed
      *
-     * @param integer|float $dimension
-     * @param array         $units
-     * @param string        $baseUnit
+     * @phpstan-template T
+     * @phpstan-param callable(int|float, int|float): T $operation
+     * @phpstan-return T
      */
-    private function normalizeUnits(&$dimension, &$units, $baseUnit = null)
+    private function coerceUnits(Number $other, $operation)
     {
-        $dimension = $this->dimension;
-        $units     = [];
+        if (!$this->unitless()) {
+            $num1 = $this->dimension;
+            $num2 = $other->valueInUnits($this->numeratorUnits, $this->denominatorUnits);
+        } else {
+            $num1 = $this->valueInUnits($other->numeratorUnits, $other->denominatorUnits);
+            $num2 = $other->dimension;
+        }
+
+        return \call_user_func($operation, $num1, $num2);
+    }
 
-        foreach ($this->units as $unit => $exp) {
-            if (! $baseUnit) {
-                $baseUnit = $this->findBaseUnit($unit);
+    /**
+     * @param string[] $numeratorUnits
+     * @param string[] $denominatorUnits
+     *
+     * @return int|float
+     */
+    private function valueInUnits(array $numeratorUnits, array $denominatorUnits)
+    {
+        if (
+            $this->unitless()
+            || (\count($numeratorUnits) === 0 && \count($denominatorUnits) === 0)
+            || ($this->numeratorUnits === $numeratorUnits && $this->denominatorUnits === $denominatorUnits)
+        ) {
+            return $this->dimension;
+        }
+
+        $value = $this->dimension;
+        $oldNumerators = $this->numeratorUnits;
+
+        foreach ($numeratorUnits as $newNumerator) {
+            foreach ($oldNumerators as $key => $oldNumerator) {
+                $conversionFactor = self::getConversionFactor($newNumerator, $oldNumerator);
+
+                if (\is_null($conversionFactor)) {
+                    continue;
+                }
+
+                $value *= $conversionFactor;
+                unset($oldNumerators[$key]);
+                continue 2;
             }
 
-            if ($baseUnit && isset(static::$unitTable[$baseUnit][$unit])) {
-                $factor = pow(static::$unitTable[$baseUnit][$unit], $exp);
+            throw new SassScriptException(sprintf(
+                'Incompatible units %s and %s.',
+                self::getUnitString($this->numeratorUnits, $this->denominatorUnits),
+                self::getUnitString($numeratorUnits, $denominatorUnits)
+            ));
+        }
+
+        $oldDenominators = $this->denominatorUnits;
 
-                $unit = $baseUnit;
-                $dimension /= $factor;
+        foreach ($denominatorUnits as $newDenominator) {
+            foreach ($oldDenominators as $key => $oldDenominator) {
+                $conversionFactor = self::getConversionFactor($newDenominator, $oldDenominator);
+
+                if (\is_null($conversionFactor)) {
+                    continue;
+                }
+
+                $value /= $conversionFactor;
+                unset($oldDenominators[$key]);
+                continue 2;
             }
 
-            $units[$unit] = $exp + (isset($units[$unit]) ? $units[$unit] : 0);
+            throw new SassScriptException(sprintf(
+                'Incompatible units %s and %s.',
+                self::getUnitString($this->numeratorUnits, $this->denominatorUnits),
+                self::getUnitString($numeratorUnits, $denominatorUnits)
+            ));
         }
+
+        if (\count($oldNumerators) || \count($oldDenominators)) {
+            throw new SassScriptException(sprintf(
+                'Incompatible units %s and %s.',
+                self::getUnitString($this->numeratorUnits, $this->denominatorUnits),
+                self::getUnitString($numeratorUnits, $denominatorUnits)
+            ));
+        }
+
+        return $value;
     }
 
     /**
-     * Find the base unit family for a given unit
+     * @param int|float $value
+     * @param string[] $numerators1
+     * @param string[] $denominators1
+     * @param string[] $numerators2
+     * @param string[] $denominators2
      *
-     * @param string $unit
+     * @return Number
+     */
+    private function multiplyUnits($value, array $numerators1, array $denominators1, array $numerators2, array $denominators2)
+    {
+        $newNumerators = array();
+
+        foreach ($numerators1 as $numerator) {
+            foreach ($denominators2 as $key => $denominator) {
+                $conversionFactor = self::getConversionFactor($numerator, $denominator);
+
+                if (\is_null($conversionFactor)) {
+                    continue;
+                }
+
+                $value /= $conversionFactor;
+                unset($denominators2[$key]);
+                continue 2;
+            }
+
+            $newNumerators[] = $numerator;
+        }
+
+        foreach ($numerators2 as $numerator) {
+            foreach ($denominators1 as $key => $denominator) {
+                $conversionFactor = self::getConversionFactor($numerator, $denominator);
+
+                if (\is_null($conversionFactor)) {
+                    continue;
+                }
+
+                $value /= $conversionFactor;
+                unset($denominators1[$key]);
+                continue 2;
+            }
+
+            $newNumerators[] = $numerator;
+        }
+
+        $newDenominators = array_values(array_merge($denominators1, $denominators2));
+
+        return new Number($value, $newNumerators, $newDenominators);
+    }
+
+    /**
+     * Returns the number of [unit1]s per [unit2].
      *
-     * @return string|null
+     * Equivalently, `1unit1 * conversionFactor(unit1, unit2) = 1unit2`.
+     *
+     * @param string $unit1
+     * @param string $unit2
+     *
+     * @return float|int|null
      */
-    private function findBaseUnit($unit)
+    private static function getConversionFactor($unit1, $unit2)
     {
-        foreach (static::$unitTable as $baseUnit => $unitVariants) {
-            if (isset($unitVariants[$unit])) {
-                return $baseUnit;
+        if ($unit1 === $unit2) {
+            return 1;
+        }
+
+        foreach (static::$unitTable as $unitVariants) {
+            if (isset($unitVariants[$unit1]) && isset($unitVariants[$unit2])) {
+                return $unitVariants[$unit1] / $unitVariants[$unit2];
             }
         }
 
         return null;
     }
+
+    /**
+     * Returns unit(s) as the product of numerator units divided by the product of denominator units
+     *
+     * @param string[] $numerators
+     * @param string[] $denominators
+     *
+     * @return string
+     */
+    private static function getUnitString(array $numerators, array $denominators)
+    {
+        if (!\count($numerators)) {
+            if (\count($denominators) === 0) {
+                return 'no units';
+            }
+
+            if (\count($denominators) === 1) {
+                return $denominators[0] . '^-1';
+            }
+
+            return '(' . implode('*', $denominators) . ')^-1';
+        }
+
+        return implode('*', $numerators) . (\count($denominators) ? '/' . implode('*', $denominators) : '');
+    }
 }
index 5083d562493706fac8fc99a0221ab7c2b833d065..55371e53a34f2f3e851359f22863910da88faf75 100644 (file)
@@ -1,4 +1,5 @@
 <?php
+
 /**
  * SCSSPHP
  *
@@ -38,7 +39,6 @@ class Parser
         'and' => 2,
         '=='  => 3,
         '!='  => 3,
-        '<=>' => 3,
         '<='  => 4,
         '>='  => 4,
         '<'   => 4,
@@ -97,7 +97,7 @@ class Parser
         $this->cssOnly          = $cssOnly;
 
         if (empty(static::$operatorPattern)) {
-            static::$operatorPattern = '([*\/%+-]|[!=]\=|\>\=?|\<\=\>|\<\=?|and|or)';
+            static::$operatorPattern = '([*\/%+-]|[!=]\=|\>\=?|\<\=?|and|or)';
 
             $commentSingle      = '\/\/';
             $commentMultiLeft   = '\/\*';
@@ -134,8 +134,29 @@ class Parser
      * @param string $msg
      *
      * @throws \ScssPhp\ScssPhp\Exception\ParserException
+     *
+     * @deprecated use "parseError" and throw the exception in the caller instead.
      */
     public function throwParseError($msg = 'parse error')
+    {
+        @trigger_error(
+            'The method "throwParseError" is deprecated. Use "parseError" and throw the exception in the caller instead',
+            E_USER_DEPRECATED
+        );
+
+        throw $this->parseError($msg);
+    }
+
+    /**
+     * Creates a parser error
+     *
+     * @api
+     *
+     * @param string $msg
+     *
+     * @return ParserException
+     */
+    public function parseError($msg = 'parse error')
     {
         list($line, $column) = $this->getSourcePosition($this->count);
 
@@ -143,15 +164,21 @@ class Parser
              ? "line: $line, column: $column"
              : "$this->sourceName on line $line, at column $column";
 
-        if ($this->peek("(.*?)(\n|$)", $m, $this->count)) {
+        if ($this->peek('(.*?)(\n|$)', $m, $this->count)) {
             $this->restoreEncoding();
 
-            throw new ParserException("$msg: failed at `$m[1]` $loc");
+            $e = new ParserException("$msg: failed at `$m[1]` $loc");
+            $e->setSourcePosition([$this->sourceName, $line, $column]);
+
+            return $e;
         }
 
         $this->restoreEncoding();
 
-        throw new ParserException("$msg: $loc");
+        $e = new ParserException("$msg: $loc");
+        $e->setSourcePosition([$this->sourceName, $line, $column]);
+
+        return $e;
     }
 
     /**
@@ -166,12 +193,12 @@ class Parser
     public function parse($buffer)
     {
         if ($this->cache) {
-            $cacheKey = $this->sourceName . ":" . md5($buffer);
+            $cacheKey = $this->sourceName . ':' . md5($buffer);
             $parseOptions = [
                 'charset' => $this->charset,
                 'utf8' => $this->utf8,
             ];
-            $v = $this->cache->getCache("parse", $cacheKey, $parseOptions);
+            $v = $this->cache->getCache('parse', $cacheKey, $parseOptions);
 
             if (! \is_null($v)) {
                 return $v;
@@ -202,11 +229,11 @@ class Parser
         }
 
         if ($this->count !== \strlen($this->buffer)) {
-            $this->throwParseError();
+            throw $this->parseError();
         }
 
         if (! empty($this->env->parent)) {
-            $this->throwParseError('unclosed block');
+            throw $this->parseError('unclosed block');
         }
 
         if ($this->charset) {
@@ -216,7 +243,7 @@ class Parser
         $this->restoreEncoding();
 
         if ($this->cache) {
-            $this->cache->setCache("parse", $cacheKey, $this->env, $parseOptions);
+            $this->cache->setCache('parse', $cacheKey, $this->env, $parseOptions);
         }
 
         return $this->env;
@@ -241,6 +268,7 @@ class Parser
         $this->buffer          = (string) $buffer;
 
         $this->saveEncoding();
+        $this->extractLineNumbers($this->buffer);
 
         $list = $this->valueList($out);
 
@@ -256,10 +284,11 @@ class Parser
      *
      * @param string       $buffer
      * @param string|array $out
+     * @param bool         $shouldValidate
      *
      * @return boolean
      */
-    public function parseSelector($buffer, &$out)
+    public function parseSelector($buffer, &$out, $shouldValidate = true)
     {
         $this->count           = 0;
         $this->env             = null;
@@ -268,11 +297,16 @@ class Parser
         $this->buffer          = (string) $buffer;
 
         $this->saveEncoding();
+        $this->extractLineNumbers($this->buffer);
 
         $selector = $this->selectors($out);
 
         $this->restoreEncoding();
 
+        if ($shouldValidate && $this->count !== strlen($buffer)) {
+            throw $this->parseError("`" . substr($buffer, $this->count) . "` is not a valid Selector in `$buffer`");
+        }
+
         return $selector;
     }
 
@@ -295,6 +329,7 @@ class Parser
         $this->buffer          = (string) $buffer;
 
         $this->saveEncoding();
+        $this->extractLineNumbers($this->buffer);
 
         $isMediaQuery = $this->mediaQueryList($out);
 
@@ -348,17 +383,16 @@ class Parser
 
         // the directives
         if (isset($this->buffer[$this->count]) && $this->buffer[$this->count] === '@') {
-            if ($this->literal('@at-root', 8) &&
+            if (
+                $this->literal('@at-root', 8) &&
                 ($this->selectors($selector) || true) &&
                 ($this->map($with) || true) &&
-                (($this->matchChar('(')
-                    && $this->interpolation($with)
-                    && $this->matchChar(')')) || true) &&
+                (($this->matchChar('(') &&
+                    $this->interpolation($with) &&
+                    $this->matchChar(')')) || true) &&
                 $this->matchChar('{', false)
             ) {
-                if ($this->cssOnly) {
-                    $this->throwParseError("SCSS syntax not allowed in CSS file");
-                }
+                ! $this->cssOnly || $this->assertPlainCssValid(false, $s);
 
                 $atRoot = $this->pushSpecialBlock(Type::T_AT_ROOT, $s);
                 $atRoot->selector = $selector;
@@ -369,7 +403,11 @@ class Parser
 
             $this->seek($s);
 
-            if ($this->literal('@media', 6) && $this->mediaQueryList($mediaQueryList) && $this->matchChar('{', false)) {
+            if (
+                $this->literal('@media', 6) &&
+                $this->mediaQueryList($mediaQueryList) &&
+                $this->matchChar('{', false)
+            ) {
                 $media = $this->pushSpecialBlock(Type::T_MEDIA, $s);
                 $media->queryList = $mediaQueryList[2];
 
@@ -378,14 +416,13 @@ class Parser
 
             $this->seek($s);
 
-            if ($this->literal('@mixin', 6) &&
+            if (
+                $this->literal('@mixin', 6) &&
                 $this->keyword($mixinName) &&
                 ($this->argumentDef($args) || true) &&
                 $this->matchChar('{', false)
             ) {
-                if ($this->cssOnly) {
-                    $this->throwParseError("SCSS syntax not allowed in CSS file");
-                }
+                ! $this->cssOnly || $this->assertPlainCssValid(false, $s);
 
                 $mixin = $this->pushSpecialBlock(Type::T_MIXIN, $s);
                 $mixin->name = $mixinName;
@@ -396,20 +433,19 @@ class Parser
 
             $this->seek($s);
 
-            if ($this->literal('@include', 8) &&
-                $this->keyword($mixinName) &&
-                ($this->matchChar('(') &&
+            if (
+                ($this->literal('@include', 8) &&
+                    $this->keyword($mixinName) &&
+                    ($this->matchChar('(') &&
                     ($this->argValues($argValues) || true) &&
                     $this->matchChar(')') || true) &&
-                ($this->end() ||
-                    ($this->literal('using', 5) &&
-                        $this->argumentDef($argUsing) &&
-                        ($this->end() || $this->matchChar('{') && $hasBlock = true)) ||
-                    $this->matchChar('{') && $hasBlock = true)
+                    ($this->end()) ||
+                ($this->literal('using', 5) &&
+                    $this->argumentDef($argUsing) &&
+                    ($this->end() || $this->matchChar('{') && $hasBlock = true)) ||
+                $this->matchChar('{') && $hasBlock = true)
             ) {
-                if ($this->cssOnly) {
-                    $this->throwParseError("SCSS syntax not allowed in CSS file");
-                }
+                ! $this->cssOnly || $this->assertPlainCssValid(false, $s);
 
                 $child = [
                     Type::T_INCLUDE,
@@ -431,13 +467,12 @@ class Parser
 
             $this->seek($s);
 
-            if ($this->literal('@scssphp-import-once', 20) &&
+            if (
+                $this->literal('@scssphp-import-once', 20) &&
                 $this->valueList($importPath) &&
                 $this->end()
             ) {
-                if ($this->cssOnly) {
-                    $this->throwParseError("SCSS syntax not allowed in CSS file");
-                }
+                ! $this->cssOnly || $this->assertPlainCssValid(false, $s);
 
                 $this->append([Type::T_SCSSPHP_IMPORT_ONCE, $importPath], $s);
 
@@ -446,10 +481,18 @@ class Parser
 
             $this->seek($s);
 
-            if ($this->literal('@import', 7) &&
+            if (
+                $this->literal('@import', 7) &&
                 $this->valueList($importPath) &&
+                $importPath[0] !== Type::T_FUNCTION_CALL &&
                 $this->end()
             ) {
+                if ($this->cssOnly) {
+                    $this->assertPlainCssValid([Type::T_IMPORT, $importPath], $s);
+                    $this->append([Type::T_COMMENT, rtrim(substr($this->buffer, $s, $this->count - $s))]);
+                    return true;
+                }
+
                 $this->append([Type::T_IMPORT, $importPath], $s);
 
                 return true;
@@ -457,12 +500,15 @@ class Parser
 
             $this->seek($s);
 
-            if ($this->literal('@import', 7) &&
+            if (
+                $this->literal('@import', 7) &&
                 $this->url($importPath) &&
                 $this->end()
             ) {
                 if ($this->cssOnly) {
-                    $this->throwParseError("SCSS syntax not allowed in CSS file");
+                    $this->assertPlainCssValid([Type::T_IMPORT, $importPath], $s);
+                    $this->append([Type::T_COMMENT, rtrim(substr($this->buffer, $s, $this->count - $s))]);
+                    return true;
                 }
 
                 $this->append([Type::T_IMPORT, $importPath], $s);
@@ -472,13 +518,12 @@ class Parser
 
             $this->seek($s);
 
-            if ($this->literal('@extend', 7) &&
+            if (
+                $this->literal('@extend', 7) &&
                 $this->selectors($selectors) &&
                 $this->end()
             ) {
-                if ($this->cssOnly) {
-                    $this->throwParseError("SCSS syntax not allowed in CSS file");
-                }
+                ! $this->cssOnly || $this->assertPlainCssValid(false, $s);
 
                 // check for '!flag'
                 $optional = $this->stripOptionalFlag($selectors);
@@ -489,14 +534,13 @@ class Parser
 
             $this->seek($s);
 
-            if ($this->literal('@function', 9) &&
+            if (
+                $this->literal('@function', 9) &&
                 $this->keyword($fnName) &&
                 $this->argumentDef($args) &&
                 $this->matchChar('{', false)
             ) {
-                if ($this->cssOnly) {
-                    $this->throwParseError("SCSS syntax not allowed in CSS file");
-                }
+                ! $this->cssOnly || $this->assertPlainCssValid(false, $s);
 
                 $func = $this->pushSpecialBlock(Type::T_FUNCTION, $s);
                 $func->name = $fnName;
@@ -507,34 +551,12 @@ class Parser
 
             $this->seek($s);
 
-            if ($this->literal('@break', 6) && $this->end()) {
-                if ($this->cssOnly) {
-                    $this->throwParseError("SCSS syntax not allowed in CSS file");
-                }
-
-                $this->append([Type::T_BREAK], $s);
-
-                return true;
-            }
-
-            $this->seek($s);
-
-            if ($this->literal('@continue', 9) && $this->end()) {
-                if ($this->cssOnly) {
-                    $this->throwParseError("SCSS syntax not allowed in CSS file");
-                }
-
-                $this->append([Type::T_CONTINUE], $s);
-
-                return true;
-            }
-
-            $this->seek($s);
-
-            if ($this->literal('@return', 7) && ($this->valueList($retVal) || true) && $this->end()) {
-                if ($this->cssOnly) {
-                    $this->throwParseError("SCSS syntax not allowed in CSS file");
-                }
+            if (
+                $this->literal('@return', 7) &&
+                ($this->valueList($retVal) || true) &&
+                $this->end()
+            ) {
+                ! $this->cssOnly || $this->assertPlainCssValid(false, $s);
 
                 $this->append([Type::T_RETURN, isset($retVal) ? $retVal : [Type::T_NULL]], $s);
 
@@ -543,15 +565,14 @@ class Parser
 
             $this->seek($s);
 
-            if ($this->literal('@each', 5) &&
+            if (
+                $this->literal('@each', 5) &&
                 $this->genericList($varNames, 'variable', ',', false) &&
                 $this->literal('in', 2) &&
                 $this->valueList($list) &&
                 $this->matchChar('{', false)
             ) {
-                if ($this->cssOnly) {
-                    $this->throwParseError("SCSS syntax not allowed in CSS file");
-                }
+                ! $this->cssOnly || $this->assertPlainCssValid(false, $s);
 
                 $each = $this->pushSpecialBlock(Type::T_EACH, $s);
 
@@ -566,12 +587,20 @@ class Parser
 
             $this->seek($s);
 
-            if ($this->literal('@while', 6) &&
+            if (
+                $this->literal('@while', 6) &&
                 $this->expression($cond) &&
                 $this->matchChar('{', false)
             ) {
-                if ($this->cssOnly) {
-                    $this->throwParseError("SCSS syntax not allowed in CSS file");
+                ! $this->cssOnly || $this->assertPlainCssValid(false, $s);
+
+                while (
+                    $cond[0] === Type::T_LIST &&
+                    ! empty($cond['enclosing']) &&
+                    $cond['enclosing'] === 'parent' &&
+                    \count($cond[2]) == 1
+                ) {
+                    $cond = reset($cond[2]);
                 }
 
                 $while = $this->pushSpecialBlock(Type::T_WHILE, $s);
@@ -582,7 +611,8 @@ class Parser
 
             $this->seek($s);
 
-            if ($this->literal('@for', 4) &&
+            if (
+                $this->literal('@for', 4) &&
                 $this->variable($varName) &&
                 $this->literal('from', 4) &&
                 $this->expression($start) &&
@@ -591,9 +621,7 @@ class Parser
                 $this->expression($end) &&
                 $this->matchChar('{', false)
             ) {
-                if ($this->cssOnly) {
-                    $this->throwParseError("SCSS syntax not allowed in CSS file");
-                }
+                ! $this->cssOnly || $this->assertPlainCssValid(false, $s);
 
                 $for = $this->pushSpecialBlock(Type::T_FOR, $s);
                 $for->var   = $varName[1];
@@ -606,17 +634,20 @@ class Parser
 
             $this->seek($s);
 
-            if ($this->literal('@if', 3) && $this->valueList($cond) && $this->matchChar('{', false)) {
-                if ($this->cssOnly) {
-                    $this->throwParseError("SCSS syntax not allowed in CSS file");
-                }
+            if (
+                $this->literal('@if', 3) &&
+                $this->functionCallArgumentsList($cond, false, '{', false)
+            ) {
+                ! $this->cssOnly || $this->assertPlainCssValid(false, $s);
 
                 $if = $this->pushSpecialBlock(Type::T_IF, $s);
 
-                while ($cond[0] === Type::T_LIST
-                    && ! empty($cond['enclosing'])
-                    && $cond['enclosing'] === 'parent'
-                    && \count($cond[2]) == 1) {
+                while (
+                    $cond[0] === Type::T_LIST &&
+                    ! empty($cond['enclosing']) &&
+                    $cond['enclosing'] === 'parent' &&
+                    \count($cond[2]) == 1
+                ) {
                     $cond = reset($cond[2]);
                 }
 
@@ -628,13 +659,11 @@ class Parser
 
             $this->seek($s);
 
-            if ($this->literal('@debug', 6) &&
-                $this->valueList($value) &&
-                $this->end()
+            if (
+                $this->literal('@debug', 6) &&
+                $this->functionCallArgumentsList($value, false)
             ) {
-                if ($this->cssOnly) {
-                    $this->throwParseError("SCSS syntax not allowed in CSS file");
-                }
+                ! $this->cssOnly || $this->assertPlainCssValid(false, $s);
 
                 $this->append([Type::T_DEBUG, $value], $s);
 
@@ -643,13 +672,11 @@ class Parser
 
             $this->seek($s);
 
-            if ($this->literal('@warn', 5) &&
-                $this->valueList($value) &&
-                $this->end()
+            if (
+                $this->literal('@warn', 5) &&
+                $this->functionCallArgumentsList($value, false)
             ) {
-                if ($this->cssOnly) {
-                    $this->throwParseError("SCSS syntax not allowed in CSS file");
-                }
+                ! $this->cssOnly || $this->assertPlainCssValid(false, $s);
 
                 $this->append([Type::T_WARN, $value], $s);
 
@@ -658,13 +685,11 @@ class Parser
 
             $this->seek($s);
 
-            if ($this->literal('@error', 6) &&
-                $this->valueList($value) &&
-                $this->end()
+            if (
+                $this->literal('@error', 6) &&
+                $this->functionCallArgumentsList($value, false)
             ) {
-                if ($this->cssOnly) {
-                    $this->throwParseError("SCSS syntax not allowed in CSS file");
-                }
+                ! $this->cssOnly || $this->assertPlainCssValid(false, $s);
 
                 $this->append([Type::T_ERROR, $value], $s);
 
@@ -673,16 +698,15 @@ class Parser
 
             $this->seek($s);
 
-            if ($this->literal('@content', 8) &&
+            if (
+                $this->literal('@content', 8) &&
                 ($this->end() ||
                     $this->matchChar('(') &&
                     $this->argValues($argContent) &&
                     $this->matchChar(')') &&
                     $this->end())
             ) {
-                if ($this->cssOnly) {
-                    $this->throwParseError("SCSS syntax not allowed in CSS file");
-                }
+                ! $this->cssOnly || $this->assertPlainCssValid(false, $s);
 
                 $this->append([Type::T_MIXIN_CONTENT, isset($argContent) ? $argContent : null], $s);
 
@@ -699,7 +723,10 @@ class Parser
                 if ($this->literal('@else', 5)) {
                     if ($this->matchChar('{', false)) {
                         $else = $this->pushSpecialBlock(Type::T_ELSE, $s);
-                    } elseif ($this->literal('if', 2) && $this->valueList($cond) && $this->matchChar('{', false)) {
+                    } elseif (
+                        $this->literal('if', 2) &&
+                        $this->functionCallArgumentsList($cond, false, '{', false)
+                    ) {
                         $else = $this->pushSpecialBlock(Type::T_ELSEIF, $s);
                         $else->cond = $cond;
                     }
@@ -716,7 +743,8 @@ class Parser
             }
 
             // only retain the first @charset directive encountered
-            if ($this->literal('@charset', 8) &&
+            if (
+                $this->literal('@charset', 8) &&
                 $this->valueList($charset) &&
                 $this->end()
             ) {
@@ -737,9 +765,10 @@ class Parser
 
             $this->seek($s);
 
-            if ($this->literal('@supports', 9) &&
-                ($t1=$this->supportsQuery($supportQuery)) &&
-                ($t2=$this->matchChar('{', false))
+            if (
+                $this->literal('@supports', 9) &&
+                ($t1 = $this->supportsQuery($supportQuery)) &&
+                ($t2 = $this->matchChar('{', false))
             ) {
                 $directive = $this->pushSpecialBlock(Type::T_DIRECTIVE, $s);
                 $directive->name  = 'supports';
@@ -751,10 +780,16 @@ class Parser
             $this->seek($s);
 
             // doesn't match built in directive, do generic one
-            if ($this->matchChar('@', false) &&
-                $this->keyword($dirName) &&
+            if (
+                $this->matchChar('@', false) &&
+                $this->mixedKeyword($dirName) &&
                 $this->directiveValue($dirValue, '{')
             ) {
+                if (count($dirName) === 1 && is_string(reset($dirName))) {
+                    $dirName = reset($dirName);
+                } else {
+                    $dirName = [Type::T_STRING, '', $dirName];
+                }
                 if ($dirName === 'media') {
                     $directive = $this->pushSpecialBlock(Type::T_MEDIA, $s);
                 } else {
@@ -763,6 +798,7 @@ class Parser
                 }
 
                 if (isset($dirValue)) {
+                    ! $this->cssOnly || ($dirValue = $this->assertPlainCssValid($dirValue));
                     $directive->value = $dirValue;
                 }
 
@@ -772,12 +808,38 @@ class Parser
             $this->seek($s);
 
             // maybe it's a generic blockless directive
-            if ($this->matchChar('@', false) &&
-                $this->keyword($dirName) &&
-                $this->directiveValue($dirValue) &&
-                $this->end()
+            if (
+                $this->matchChar('@', false) &&
+                $this->mixedKeyword($dirName) &&
+                ! $this->isKnownGenericDirective($dirName) &&
+                ($this->end(false) || ($this->directiveValue($dirValue, '') && $this->end(false)))
             ) {
-                $this->append([Type::T_DIRECTIVE, [$dirName, $dirValue]], $s);
+                if (\count($dirName) === 1 && \is_string(\reset($dirName))) {
+                    $dirName = \reset($dirName);
+                } else {
+                    $dirName = [Type::T_STRING, '', $dirName];
+                }
+                if (
+                    ! empty($this->env->parent) &&
+                    $this->env->type &&
+                    ! \in_array($this->env->type, [Type::T_DIRECTIVE, Type::T_MEDIA])
+                ) {
+                    $plain = \trim(\substr($this->buffer, $s, $this->count - $s));
+                    throw $this->parseError(
+                        "Unknown directive `{$plain}` not allowed in `" . $this->env->type . "` block"
+                    );
+                }
+                // blockless directives with a blank line after keeps their blank lines after
+                // sass-spec compliance purpose
+                $s = $this->count;
+                $hasBlankLine = false;
+                if ($this->match('\s*?\n\s*\n', $out, false)) {
+                    $hasBlankLine = true;
+                    $this->seek($s);
+                }
+                $isNotRoot = ! empty($this->env->parent);
+                $this->append([Type::T_DIRECTIVE, [$dirName, $dirValue, $hasBlankLine, $isNotRoot]], $s);
+                $this->whitespace();
 
                 return true;
             }
@@ -787,15 +849,19 @@ class Parser
             return false;
         }
 
+        $inCssSelector = null;
+        if ($this->cssOnly) {
+            $inCssSelector = (! empty($this->env->parent) &&
+                ! in_array($this->env->type, [Type::T_DIRECTIVE, Type::T_MEDIA]));
+        }
         // custom properties : right part is static
-        if (($this->customProperty($name) || ($this->cssOnly && $this->propertyName($name))) &&
-            $this->matchChar(':', false)
-        ) {
+        if (($this->customProperty($name) ) && $this->matchChar(':', false)) {
             $start = $this->count;
 
             // but can be complex and finish with ; or }
             foreach ([';','}'] as $ending) {
-                if ($this->openString($ending, $stringValue, '(', ')', false) &&
+                if (
+                    $this->openString($ending, $stringValue, '(', ')', false) &&
                     $this->end()
                 ) {
                     $end = $this->count;
@@ -810,7 +876,8 @@ class Parser
                         if ($p && $p < $end) {
                             $this->seek($start);
 
-                            if ($this->openString($ending, $stringValue, $nestingPair[0], $nestingPair[1], false) &&
+                            if (
+                                $this->openString($ending, $stringValue, $nestingPair[0], $nestingPair[1], false) &&
                                 $this->end() &&
                                 $this->count > $end
                             ) {
@@ -834,7 +901,8 @@ class Parser
 
         // property shortcut
         // captures most properties before having to parse a selector
-        if ($this->keyword($name, false) &&
+        if (
+            $this->keyword($name, false) &&
             $this->literal(': ', 2) &&
             $this->valueList($value) &&
             $this->end()
@@ -848,14 +916,13 @@ class Parser
         $this->seek($s);
 
         // variable assigns
-        if ($this->variable($name) &&
+        if (
+            $this->variable($name) &&
             $this->matchChar(':') &&
             $this->valueList($value) &&
             $this->end()
         ) {
-            if ($this->cssOnly) {
-                $this->throwParseError("SCSS syntax not allowed in CSS file");
-            }
+            ! $this->cssOnly || $this->assertPlainCssValid(false, $s);
 
             // check for '!flag'
             $assignmentFlags = $this->stripAssignmentFlags($value);
@@ -872,12 +939,11 @@ class Parser
         }
 
         // opening css block
-        if ($this->selectors($selectors) && $this->matchChar('{', false)) {
-            if ($this->cssOnly) {
-                if (! empty($this->env->parent)) {
-                    $this->throwParseError("SCSS syntax not allowed in CSS file");
-                }
-            }
+        if (
+            $this->selectors($selectors) &&
+            $this->matchChar('{', false)
+        ) {
+            ! $this->cssOnly || ! $inCssSelector || $this->assertPlainCssValid(false);
 
             $this->pushBlock($selectors, $s);
 
@@ -892,12 +958,15 @@ class Parser
         $this->seek($s);
 
         // property assign, or nested assign
-        if ($this->propertyName($name) && $this->matchChar(':')) {
+        if (
+            $this->propertyName($name) &&
+            $this->matchChar(':')
+        ) {
             $foundSomething = false;
 
             if ($this->valueList($value)) {
                 if (empty($this->env->parent)) {
-                    $this->throwParseError('expected "{"');
+                    throw $this->parseError('expected "{"');
                 }
 
                 $this->append([Type::T_ASSIGN, $name, $value], $s);
@@ -905,9 +974,7 @@ class Parser
             }
 
             if ($this->matchChar('{', false)) {
-                if ($this->cssOnly) {
-                    $this->throwParseError("SCSS syntax not allowed in CSS file");
-                }
+                ! $this->cssOnly || $this->assertPlainCssValid(false);
 
                 $propBlock = $this->pushSpecialBlock(Type::T_NESTED_PROPERTY, $s);
                 $propBlock->prefix = $name;
@@ -958,7 +1025,8 @@ class Parser
         }
 
         // extra stuff
-        if ($this->matchChar(';') ||
+        if (
+            $this->matchChar(';') ||
             $this->literal('<!--', 4)
         ) {
             return true;
@@ -979,7 +1047,7 @@ class Parser
     {
         list($line, $column) = $this->getSourcePosition($pos);
 
-        $b = new Block;
+        $b = new Block();
         $b->sourceName   = $this->sourceName;
         $b->sourceLine   = $line;
         $b->sourceColumn = $column;
@@ -1048,7 +1116,7 @@ class Parser
         $block = $this->env;
 
         if (empty($block->parent)) {
-            $this->throwParseError('unexpected }');
+            throw $this->parseError('unexpected }');
         }
 
         if ($block->type == Type::T_AT_ROOT) {
@@ -1094,13 +1162,215 @@ class Parser
         $this->count = $where;
     }
 
+    /**
+     * Assert a parsed part is plain CSS Valid
+     *
+     * @param array $parsed
+     * @param int $startPos
+     * @throws ParserException
+     */
+    protected function assertPlainCssValid($parsed, $startPos = null)
+    {
+        $type = '';
+        if ($parsed) {
+            $type = $parsed[0];
+            $parsed = $this->isPlainCssValidElement($parsed);
+        }
+        if (! $parsed) {
+            if (! \is_null($startPos)) {
+                $plain = rtrim(substr($this->buffer, $startPos, $this->count - $startPos));
+                $message = "Error : `{$plain}` isn't allowed in plain CSS";
+            } else {
+                $message = 'Error: SCSS syntax not allowed in CSS file';
+            }
+            if ($type) {
+                $message .= " ($type)";
+            }
+            throw $this->parseError($message);
+        }
+
+        return $parsed;
+    }
+
+    /**
+     * Check a parsed element is plain CSS Valid
+     * @param array $parsed
+     * @return bool|array
+     */
+    protected function isPlainCssValidElement($parsed, $allowExpression = false)
+    {
+        // keep string as is
+        if (is_string($parsed)) {
+            return $parsed;
+        }
+
+        if (
+            \in_array($parsed[0], [Type::T_FUNCTION, Type::T_FUNCTION_CALL]) &&
+            !\in_array($parsed[1], [
+                'alpha',
+                'attr',
+                'calc',
+                'cubic-bezier',
+                'env',
+                'grayscale',
+                'hsl',
+                'hsla',
+                'invert',
+                'linear-gradient',
+                'min',
+                'max',
+                'radial-gradient',
+                'repeating-linear-gradient',
+                'repeating-radial-gradient',
+                'rgb',
+                'rgba',
+                'rotate',
+                'saturate',
+                'var',
+            ]) &&
+            Compiler::isNativeFunction($parsed[1])
+        ) {
+            return false;
+        }
+
+        switch ($parsed[0]) {
+            case Type::T_BLOCK:
+            case Type::T_KEYWORD:
+            case Type::T_NULL:
+            case Type::T_NUMBER:
+            case Type::T_MEDIA:
+                return $parsed;
+
+            case Type::T_COMMENT:
+                if (isset($parsed[2])) {
+                    return false;
+                }
+                return $parsed;
+
+            case Type::T_DIRECTIVE:
+                if (\is_array($parsed[1])) {
+                    $parsed[1][1] = $this->isPlainCssValidElement($parsed[1][1]);
+                    if (! $parsed[1][1]) {
+                        return false;
+                    }
+                }
+
+                return $parsed;
+
+            case Type::T_IMPORT:
+                if ($parsed[1][0] === Type::T_LIST) {
+                    return false;
+                }
+                $parsed[1] = $this->isPlainCssValidElement($parsed[1]);
+                if ($parsed[1] === false) {
+                    return false;
+                }
+                return $parsed;
+
+            case Type::T_STRING:
+                foreach ($parsed[2] as $k => $substr) {
+                    if (\is_array($substr)) {
+                        $parsed[2][$k] = $this->isPlainCssValidElement($substr);
+                        if (! $parsed[2][$k]) {
+                            return false;
+                        }
+                    }
+                }
+                return $parsed;
+
+            case Type::T_LIST:
+                if (!empty($parsed['enclosing'])) {
+                    return false;
+                }
+                foreach ($parsed[2] as $k => $listElement) {
+                    $parsed[2][$k] = $this->isPlainCssValidElement($listElement);
+                    if (! $parsed[2][$k]) {
+                        return false;
+                    }
+                }
+                return $parsed;
+
+            case Type::T_ASSIGN:
+                foreach ([1, 2, 3] as $k) {
+                    if (! empty($parsed[$k])) {
+                        $parsed[$k] = $this->isPlainCssValidElement($parsed[$k]);
+                        if (! $parsed[$k]) {
+                            return false;
+                        }
+                    }
+                }
+                return $parsed;
+
+            case Type::T_EXPRESSION:
+                list( ,$op, $lhs, $rhs, $inParens, $whiteBefore, $whiteAfter) = $parsed;
+                if (! $allowExpression &&  ! \in_array($op, ['and', 'or', '/'])) {
+                    return false;
+                }
+                $lhs = $this->isPlainCssValidElement($lhs, true);
+                if (! $lhs) {
+                    return false;
+                }
+                $rhs = $this->isPlainCssValidElement($rhs, true);
+                if (! $rhs) {
+                    return false;
+                }
+
+                return [
+                    Type::T_STRING,
+                    '', [
+                        $this->inParens ? '(' : '',
+                        $lhs,
+                        ($whiteBefore ? ' ' : '') . $op . ($whiteAfter ? ' ' : ''),
+                        $rhs,
+                        $this->inParens ? ')' : ''
+                    ]
+                ];
+
+            case Type::T_UNARY:
+                $parsed[2] = $this->isPlainCssValidElement($parsed[2]);
+                if (! $parsed[2]) {
+                    return false;
+                }
+                return $parsed;
+
+            case Type::T_FUNCTION:
+                $argsList = $parsed[2];
+                foreach ($argsList[2] as $argElement) {
+                    if (! $this->isPlainCssValidElement($argElement)) {
+                        return false;
+                    }
+                }
+                return $parsed;
+
+            case Type::T_FUNCTION_CALL:
+                $parsed[0] = Type::T_FUNCTION;
+                $argsList = [Type::T_LIST, ',', []];
+                foreach ($parsed[2] as $arg) {
+                    if ($arg[0] || ! empty($arg[2])) {
+                        // no named arguments possible in a css function call
+                        // nor ... argument
+                        return false;
+                    }
+                    $arg = $this->isPlainCssValidElement($arg[1], $parsed[1] === 'calc');
+                    if (! $arg) {
+                        return false;
+                    }
+                    $argsList[2][] = $arg;
+                }
+                $parsed[2] = $argsList;
+                return $parsed;
+        }
+
+        return false;
+    }
+
     /**
      * Match string looking for either ending delim, escape, or string interpolation
      *
      * {@internal This is a workaround for preg_match's 250K string match limit. }}
      *
      * @param array  $m     Matches (passed by reference)
-     * @param string $delim Delimeter
+     * @param string $delim Delimiter
      *
      * @return boolean True if match; false otherwise
      */
@@ -1111,7 +1381,7 @@ class Parser
         $end = \strlen($this->buffer);
 
         // look for either ending delim, escape, or string interpolation
-        foreach (['#{', '\\', $delim] as $lookahead) {
+        foreach (['#{', '\\', "\r", $delim] as $lookahead) {
             $pos = strpos($this->buffer, $lookahead, $this->count);
 
             if ($pos !== false && $pos < $end) {
@@ -1248,7 +1518,7 @@ class Parser
                     if ($this->interpolation($out)) {
                         // keep right spaces in the following string part
                         if ($out[3]) {
-                            while ($this->buffer[$this->count-1] !== '}') {
+                            while ($this->buffer[$this->count - 1] !== '}') {
                                 $this->count--;
                             }
 
@@ -1282,6 +1552,10 @@ class Parser
             } else {
                 // comment that are ignored and not kept in the output css
                 $this->count += \strlen($m[0]);
+                // silent comments are not allowed in plain CSS files
+                ! $this->cssOnly
+                  || ! \strlen(trim($m[0]))
+                  || $this->assertPlainCssValid(false, $this->count - \strlen($m[0]));
             }
 
             $gotWhite = true;
@@ -1298,25 +1572,6 @@ class Parser
     protected function appendComment($comment)
     {
         if (! $this->discardComments) {
-            if ($comment[0] === Type::T_COMMENT) {
-                if (\is_string($comment[1])) {
-                    $comment[1] = substr(preg_replace(['/^\s+/m', '/^(.)/m'], ['', ' \1'], $comment[1]), 1);
-                }
-
-                if (isset($comment[2]) and \is_array($comment[2]) and $comment[2][0] === Type::T_STRING) {
-                    foreach ($comment[2][2] as $k => $v) {
-                        if (\is_string($v)) {
-                            $p = strpos($v, "\n");
-
-                            if ($p !== false) {
-                                $comment[2][2][$k] = substr($v, 0, $p + 1)
-                                    . preg_replace(['/^\s+/m', '/^(.)/m'], ['', ' \1'], substr($v, $p+1));
-                            }
-                        }
-                    }
-                }
-            }
-
             $this->env->comments[] = $comment;
         }
     }
@@ -1330,6 +1585,8 @@ class Parser
     protected function append($statement, $pos = null)
     {
         if (! \is_null($statement)) {
+            ! $this->cssOnly || ($statement = $this->assertPlainCssValid($statement, $pos));
+
             if (! \is_null($pos)) {
                 list($line, $column) = $this->getSourcePosition($pos);
 
@@ -1387,7 +1644,9 @@ class Parser
         $expressions = null;
         $parts = [];
 
-        if (($this->literal('only', 4) && ($only = true) || $this->literal('not', 3) && ($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];
@@ -1443,7 +1702,8 @@ class Parser
 
         $not = false;
 
-        if (($this->literal('not', 3) && ($not = true) || true) &&
+        if (
+            ($this->literal('not', 3) && ($not = true) || true) &&
             $this->matchChar('(') &&
             ($this->expression($property)) &&
             $this->literal(': ', 2) &&
@@ -1462,7 +1722,8 @@ class Parser
             $this->seek($s);
         }
 
-        if ($this->matchChar('(') &&
+        if (
+            $this->matchChar('(') &&
             $this->supportsQuery($subQuery) &&
             $this->matchChar(')')
         ) {
@@ -1472,7 +1733,8 @@ class Parser
             $this->seek($s);
         }
 
-        if ($this->literal('not', 3) &&
+        if (
+            $this->literal('not', 3) &&
             $this->supportsQuery($subQuery)
         ) {
             $parts[] = [Type::T_STRING, '', [[Type::T_KEYWORD, 'not '], $subQuery]];
@@ -1481,7 +1743,8 @@ class Parser
             $this->seek($s);
         }
 
-        if ($this->literal('selector(', 9) &&
+        if (
+            $this->literal('selector(', 9) &&
             $this->selector($selector) &&
             $this->matchChar(')')
         ) {
@@ -1518,8 +1781,10 @@ class Parser
             $this->seek($s);
         }
 
-        if ($this->literal('and', 3) &&
-            $this->genericList($expressions, 'supportsQuery', ' and', false)) {
+        if (
+            $this->literal('and', 3) &&
+            $this->genericList($expressions, 'supportsQuery', ' and', false)
+        ) {
             array_unshift($expressions[2], [Type::T_STRING, '', $parts]);
 
             $parts = [$expressions];
@@ -1528,8 +1793,10 @@ class Parser
             $this->seek($s);
         }
 
-        if ($this->literal('or', 2) &&
-            $this->genericList($expressions, 'supportsQuery', ' or', false)) {
+        if (
+            $this->literal('or', 2) &&
+            $this->genericList($expressions, 'supportsQuery', ' or', false)
+        ) {
             array_unshift($expressions[2], [Type::T_STRING, '', $parts]);
 
             $parts = [$expressions];
@@ -1564,9 +1831,11 @@ class Parser
         $s = $this->count;
         $value = null;
 
-        if ($this->matchChar('(') &&
+        if (
+            $this->matchChar('(') &&
             $this->expression($feature) &&
-            ($this->matchChar(':') && $this->expression($value) || true) &&
+            ($this->matchChar(':') &&
+                $this->expression($value) || true) &&
             $this->matchChar(')')
         ) {
             $out = [Type::T_MEDIA_EXPRESSION, $feature];
@@ -1592,12 +1861,19 @@ class Parser
      */
     protected function argValues(&$out)
     {
+        $discardComments = $this->discardComments;
+        $this->discardComments = true;
+
         if ($this->genericList($list, 'argValue', ',', false)) {
             $out = $list[2];
 
+            $this->discardComments = $discardComments;
+
             return true;
         }
 
+        $this->discardComments = $discardComments;
+
         return false;
     }
 
@@ -1620,7 +1896,7 @@ class Parser
             $keyword = null;
         }
 
-        if ($this->genericList($value, 'expression')) {
+        if ($this->genericList($value, 'expression', '', true)) {
             $out = [$keyword, $value, false];
             $s = $this->count;
 
@@ -1636,6 +1912,53 @@ class Parser
         return false;
     }
 
+    /**
+     * Check if a generic directive is known to be able to allow almost any syntax or not
+     * @param $directiveName
+     * @return bool
+     */
+    protected function isKnownGenericDirective($directiveName)
+    {
+        if (\is_array($directiveName) && \is_string(reset($directiveName))) {
+            $directiveName = reset($directiveName);
+        }
+        if (! \is_string($directiveName)) {
+            return false;
+        }
+        if (
+            \in_array($directiveName, [
+            'at-root',
+            'media',
+            'mixin',
+            'include',
+            'scssphp-import-once',
+            'import',
+            'extend',
+            'function',
+            'break',
+            'continue',
+            'return',
+            'each',
+            'while',
+            'for',
+            'if',
+            'debug',
+            'warn',
+            'error',
+            'content',
+            'else',
+            'charset',
+            'supports',
+            // Todo
+            'use',
+            'forward',
+            ])
+        ) {
+            return true;
+        }
+        return false;
+    }
+
     /**
      * Parse directive value list that considers $vars as keyword
      *
@@ -1660,8 +1983,13 @@ class Parser
 
         $this->seek($s);
 
-        if ($endChar and $this->openString($endChar, $out)) {
-            if ($this->matchChar($endChar, false)) {
+        if (\is_string($endChar) && $this->openString($endChar ? $endChar : ';', $out, null, null, true, ";}{")) {
+            if ($endChar && $this->matchChar($endChar, false)) {
+                return true;
+            }
+            $ss = $this->count;
+            if (!$endChar && $this->end()) {
+                $this->seek($ss);
                 return true;
             }
         }
@@ -1710,6 +2038,45 @@ class Parser
         return $res;
     }
 
+    /**
+     * Parse a function call, where externals () are part of the call
+     * and not of the value list
+     *
+     * @param $out
+     * @param bool $mandatoryEnclos
+     * @param null|string $charAfter
+     * @param null|bool $eatWhiteSp
+     * @return bool
+     */
+    protected function functionCallArgumentsList(&$out, $mandatoryEnclos = true, $charAfter = null, $eatWhiteSp = null)
+    {
+        $s = $this->count;
+
+        if (
+            $this->matchChar('(') &&
+            $this->valueList($out) &&
+            $this->matchChar(')') &&
+            ($charAfter ? $this->matchChar($charAfter, $eatWhiteSp) : $this->end())
+        ) {
+            return true;
+        }
+
+        if (! $mandatoryEnclos) {
+            $this->seek($s);
+
+            if (
+                $this->valueList($out) &&
+                ($charAfter ? $this->matchChar($charAfter, $eatWhiteSp) : $this->end())
+            ) {
+                return true;
+            }
+        }
+
+        $this->seek($s);
+
+        return false;
+    }
+
     /**
      * Parse space separated value list
      *
@@ -1748,6 +2115,59 @@ class Parser
                 }
 
                 $trailing_delim = true;
+            } else {
+                // if no delim watch that a keyword didn't eat the single/double quote
+                // from the following starting string
+                if ($value[0] === Type::T_KEYWORD) {
+                    $word = $value[1];
+
+                    $last_char = substr($word, -1);
+
+                    if (
+                        strlen($word) > 1 &&
+                        in_array($last_char, [ "'", '"']) &&
+                        substr($word, -2, 1) !== '\\'
+                    ) {
+                        // if there is a non escaped opening quote in the keyword, this seems unlikely a mistake
+                        $word = str_replace('\\' . $last_char, '\\\\', $word);
+                        if (strpos($word, $last_char) < strlen($word) - 1) {
+                            continue;
+                        }
+
+                        $currentCount = $this->count;
+
+                        // let's try to rewind to previous char and try a parse
+                        $this->count--;
+                        // in case the keyword also eat spaces
+                        while (substr($this->buffer, $this->count, 1) !== $last_char) {
+                            $this->count--;
+                        }
+
+                        $nextValue = null;
+                        if ($this->$parseItem($nextValue)) {
+                            if ($nextValue[0] === Type::T_KEYWORD && $nextValue[1] === $last_char) {
+                                // bad try, forget it
+                                $this->seek($currentCount);
+                                continue;
+                            }
+                            if ($nextValue[0] !== Type::T_STRING) {
+                                // bad try, forget it
+                                $this->seek($currentCount);
+                                continue;
+                            }
+
+                            // OK it was a good idea
+                            $value[1] = substr($value[1], 0, -1);
+                            array_pop($items);
+                            $items[] = $value;
+                            $items[] = $nextValue;
+                        } else {
+                            // bad try, forget it
+                            $this->seek($currentCount);
+                            continue;
+                        }
+                    }
+                }
             }
         }
 
@@ -1787,7 +2207,7 @@ class Parser
         $allowedTypes = ($listOnly ? [Type::T_LIST] : [Type::T_LIST, Type::T_MAP]);
 
         if ($this->matchChar('(')) {
-            if ($this->enclosedExpression($lhs, $s, ")", $allowedTypes)) {
+            if ($this->enclosedExpression($lhs, $s, ')', $allowedTypes)) {
                 if ($lookForExp) {
                     $out = $this->expHelper($lhs, 0);
                 } else {
@@ -1803,7 +2223,7 @@ class Parser
         }
 
         if (\in_array(Type::T_LIST, $allowedTypes) && $this->matchChar('[')) {
-            if ($this->enclosedExpression($lhs, $s, "]", [Type::T_LIST])) {
+            if ($this->enclosedExpression($lhs, $s, ']', [Type::T_LIST])) {
                 if ($lookForExp) {
                     $out = $this->expHelper($lhs, 0);
                 } else {
@@ -1845,17 +2265,17 @@ class Parser
      *
      * @return boolean
      */
-    protected function enclosedExpression(&$out, $s, $closingParen = ")", $allowedTypes = [Type::T_LIST, Type::T_MAP])
+    protected function enclosedExpression(&$out, $s, $closingParen = ')', $allowedTypes = [Type::T_LIST, Type::T_MAP])
     {
         if ($this->matchChar($closingParen) && \in_array(Type::T_LIST, $allowedTypes)) {
             $out = [Type::T_LIST, '', []];
 
             switch ($closingParen) {
-                case ")":
+                case ')':
                     $out['enclosing'] = 'parent'; // parenthesis list
                     break;
 
-                case "]":
+                case ']':
                     $out['enclosing'] = 'bracket'; // bracketed list
                     break;
             }
@@ -1863,8 +2283,10 @@ class Parser
             return true;
         }
 
-        if ($this->valueList($out) && $this->matchChar($closingParen) &&
-            \in_array($out[0], [Type::T_LIST, Type::T_KEYWORD]) &&
+        if (
+            $this->valueList($out) &&
+            $this->matchChar($closingParen) && ! ($closingParen === ')' &&
+            \in_array($out[0], [Type::T_EXPRESSION, Type::T_UNARY])) &&
             \in_array(Type::T_LIST, $allowedTypes)
         ) {
             if ($out[0] !== Type::T_LIST || ! empty($out['enclosing'])) {
@@ -1872,11 +2294,11 @@ class Parser
             }
 
             switch ($closingParen) {
-                case ")":
+                case ')':
                     $out['enclosing'] = 'parent'; // parenthesis list
                     break;
 
-                case "]":
+                case ']':
                     $out['enclosing'] = 'bracket'; // bracketed list
                     break;
             }
@@ -1928,12 +2350,17 @@ class Parser
                 break;
             }
 
+            if ($op === '-' && ! $whiteAfter && $rhs[0] === Type::T_KEYWORD) {
+                break;
+            }
+
             // peek and see if rhs belongs to next operator
             if ($this->peek($operators, $next) && static::$precedence[$next[1]] > static::$precedence[$op]) {
                 $rhs = $this->expHelper($rhs, static::$precedence[$next[1]]);
             }
 
             $lhs = [Type::T_EXPRESSION, $op, $lhs, $rhs, $this->inParens, $whiteBefore, $whiteAfter];
+
             $ss = $this->count;
             $whiteBefore = isset($this->buffer[$this->count - 1]) &&
                 ctype_space($this->buffer[$this->count - 1]);
@@ -1960,7 +2387,10 @@ class Parser
         $s = $this->count;
         $char = $this->buffer[$this->count];
 
-        if ($this->literal('url(', 4) && $this->match('data:([a-z]+)\/([a-z0-9.+-]+);base64,', $m, false)) {
+        if (
+            $this->literal('url(', 4) &&
+            $this->match('data:([a-z]+)\/([a-z0-9.+-]+);base64,', $m, false)
+        ) {
             $len = strspn(
                 $this->buffer,
                 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwyxz0123456789+/=',
@@ -1979,7 +2409,10 @@ class Parser
 
         $this->seek($s);
 
-        if ($this->literal('url(', 4, false) && $this->match('\s*(\/\/[^\s\)]+)\s*', $m)) {
+        if (
+            $this->literal('url(', 4, false) &&
+            $this->match('\s*(\/\/[^\s\)]+)\s*', $m)
+        ) {
             $content = 'url(' . $m[1];
 
             if ($this->matchChar(')')) {
@@ -1994,7 +2427,10 @@ class Parser
 
         // not
         if ($char === 'n' && $this->literal('not', 3, false)) {
-            if ($this->whitespace() && $this->value($inner)) {
+            if (
+                $this->whitespace() &&
+                $this->value($inner)
+            ) {
                 $out = [Type::T_UNARY, 'not', $inner, $this->inParens];
 
                 return true;
@@ -2049,7 +2485,10 @@ class Parser
                 return true;
             }
 
-            if ($this->keyword($inner) && ! $this->func($inner, $out)) {
+            if (
+                $this->keyword($inner) &&
+                ! $this->func($inner, $out)
+            ) {
                 $out = [Type::T_UNARY, '-', $inner, $this->inParens];
 
                 return true;
@@ -2077,7 +2516,7 @@ class Parser
             $this->count++;
 
             if ($this->keyword($keyword)) {
-                $out = [Type::T_KEYWORD, "#" . $keyword];
+                $out = [Type::T_KEYWORD, '#' . $keyword];
 
                 return true;
             }
@@ -2108,10 +2547,17 @@ class Parser
         }
 
         // unicode range with wildcards
-        if ($this->literal('U+', 2) && $this->match('([0-9A-F]+\?*)(-([0-9A-F]+))?', $m, false)) {
-            $out = [Type::T_KEYWORD, 'U+' . $m[0]];
+        if (
+            $this->literal('U+', 2) &&
+            $this->match('\?+|([0-9A-F]+(\?+|(-[0-9A-F]+))?)', $m, false)
+        ) {
+            $unicode = explode('-', $m[0]);
+            if (strlen(reset($unicode)) <= 6 && strlen(end($unicode)) <= 6) {
+                $out = [Type::T_KEYWORD, 'U+' . $m[0]];
 
-            return true;
+                return true;
+            }
+            $this->count -= strlen($m[0]) + 2;
         }
 
         if ($this->keyword($keyword, false)) {
@@ -2155,7 +2601,10 @@ class Parser
 
             $this->inParens = true;
 
-            if ($this->expression($exp) && $this->matchChar(')')) {
+            if (
+                $this->expression($exp) &&
+                $this->matchChar(')')
+            ) {
                 $out = $exp;
                 $this->inParens = $inParens;
 
@@ -2180,7 +2629,8 @@ class Parser
     {
         $s = $this->count;
 
-        if ($this->literal('progid:', 7, false) &&
+        if (
+            $this->literal('progid:', 7, false) &&
             $this->openString('(', $fn) &&
             $this->matchChar('(')
         ) {
@@ -2222,7 +2672,10 @@ class Parser
             if ($name !== 'expression' && ! preg_match('/^(-[a-z]+-)?calc$/', $name)) {
                 $ss = $this->count;
 
-                if ($this->argValues($args) && $this->matchChar(')')) {
+                if (
+                    $this->argValues($args) &&
+                    $this->matchChar(')')
+                ) {
                     $func = [Type::T_FUNCTION_CALL, $name, $args];
 
                     return true;
@@ -2231,7 +2684,8 @@ class Parser
                 $this->seek($ss);
             }
 
-            if (($this->openString(')', $str, '(') || true) &&
+            if (
+                ($this->openString(')', $str, '(') || true) &&
                 $this->matchChar(')')
             ) {
                 $args = [];
@@ -2266,7 +2720,10 @@ class Parser
         $args = [];
 
         while ($this->keyword($var)) {
-            if ($this->matchChar('=') && $this->expression($exp)) {
+            if (
+                $this->matchChar('=') &&
+                $this->expression($exp)
+            ) {
                 $args[] = [Type::T_STRING, '', [$var . '=']];
                 $arg = $exp;
             } else {
@@ -2312,7 +2769,10 @@ class Parser
 
             $ss = $this->count;
 
-            if ($this->matchChar(':') && $this->genericList($defaultVal, 'expression')) {
+            if (
+                $this->matchChar(':') &&
+                $this->genericList($defaultVal, 'expression', '', true)
+            ) {
                 $arg[1] = $defaultVal;
             } else {
                 $this->seek($ss);
@@ -2324,7 +2784,7 @@ class Parser
                 $sss = $this->count;
 
                 if (! $this->matchChar(')')) {
-                    $this->throwParseError('... has to be after the final argument');
+                    throw $this->parseError('... has to be after the final argument');
                 }
 
                 $arg[2] = true;
@@ -2370,8 +2830,10 @@ class Parser
         $keys = [];
         $values = [];
 
-        while ($this->genericList($key, 'expression') && $this->matchChar(':') &&
-            $this->genericList($value, 'expression')
+        while (
+            $this->genericList($key, 'expression', '', true) &&
+            $this->matchChar(':') &&
+            $this->genericList($value, 'expression', '', true)
         ) {
             $keys[] = $key;
             $values[] = $value;
@@ -2403,7 +2865,7 @@ class Parser
     {
         $s = $this->count;
 
-        if ($this->match('(#([0-9a-f]+))', $m)) {
+        if ($this->match('(#([0-9a-f]+)\b)', $m)) {
             if (\in_array(\strlen($m[2]), [3,4,6,8])) {
                 $out = [Type::T_KEYWORD, $m[0]];
 
@@ -2451,7 +2913,7 @@ class Parser
      *
      * @return boolean
      */
-    protected function string(&$out)
+    protected function string(&$out, $keepDelimWithInterpolation = false)
     {
         $s = $this->count;
 
@@ -2483,21 +2945,27 @@ class Parser
                     $this->count += \strlen($m[2]);
                     $content[] = '#{'; // ignore it
                 }
+            } elseif ($m[2] === "\r") {
+                $content[] = chr(10);
+                // TODO : warning
+                # DEPRECATION WARNING on line x, column y of zzz:
+                # Unescaped multiline strings are deprecated and will be removed in a future version of Sass.
+                # To include a newline in a string, use "\a" or "\a " as in CSS.
+                if ($this->matchChar("\n", false)) {
+                    $content[] = ' ';
+                }
             } elseif ($m[2] === '\\') {
-                if ($this->matchChar('"', false)) {
-                    $content[] = $m[2] . '"';
-                } elseif ($this->matchChar("'", false)) {
-                    $content[] = $m[2] . "'";
-                } elseif ($this->literal("\\", 1, false)) {
-                    $content[] = $m[2] . "\\";
-                } elseif ($this->literal("\r\n", 2, false) ||
+                if (
+                    $this->literal("\r\n", 2, false) ||
                     $this->matchChar("\r", false) ||
                     $this->matchChar("\n", false) ||
                     $this->matchChar("\f", false)
                 ) {
                     // this is a continuation escaping, to be ignored
+                } elseif ($this->matchEscapeCharacter($c)) {
+                    $content[] = $c;
                 } else {
-                    $content[] = $m[2];
+                    throw $this->parseError('Unterminated escape sequence');
                 }
             } else {
                 $this->count -= \strlen($delim);
@@ -2508,18 +2976,8 @@ class Parser
         $this->eatWhiteDefault = $oldWhite;
 
         if ($this->literal($delim, \strlen($delim))) {
-            if ($hasInterpolation) {
+            if ($hasInterpolation && ! $keepDelimWithInterpolation) {
                 $delim = '"';
-
-                foreach ($content as &$string) {
-                    if ($string === "\\\\") {
-                        $string = "\\";
-                    } elseif ($string === "\\'") {
-                        $string = "'";
-                    } elseif ($string === '\\"') {
-                        $string = '"';
-                    }
-                }
             }
 
             $out = [Type::T_STRING, $delim, $content];
@@ -2532,6 +2990,46 @@ class Parser
         return false;
     }
 
+    protected function matchEscapeCharacter(&$out, $inKeywords = false)
+    {
+        $s = $this->count;
+        if ($this->match('[a-f0-9]', $m, false)) {
+            $hex = $m[0];
+
+            for ($i = 5; $i--;) {
+                if ($this->match('[a-f0-9]', $m, false)) {
+                    $hex .= $m[0];
+                } else {
+                    break;
+                }
+            }
+
+            $value = hexdec($hex);
+
+            if (!$inKeywords && ($value == 0 || ($value >= 0xD800 && $value <= 0xDFFF) || $value >= 0x10FFFF)) {
+                $out = "\xEF\xBF\xBD"; // "\u{FFFD}" but with a syntax supported on PHP 5
+            } elseif ($value < 0x20) {
+                $out = Util::mbChr($value);
+            } else {
+                $out = Util::mbChr($value);
+            }
+
+            return true;
+        }
+
+        if ($this->match('.', $m, false)) {
+            if ($inKeywords && in_array($m[0], ["'",'"','@','&',' ','\\',':','/','%'])) {
+                $this->seek($s);
+                return false;
+            }
+            $out = $m[0];
+
+            return true;
+        }
+
+        return false;
+    }
+
     /**
      * Parse keyword or interpolation
      *
@@ -2581,24 +3079,26 @@ class Parser
      *
      * @param string  $end
      * @param array   $out
-     * @param string  $nestingOpen
-     * @param string  $nestingClose
-     * @param boolean $trimEnd
+     * @param string  $nestOpen
+     * @param string  $nestClose
+     * @param boolean $rtrim
+     * @param string $disallow
      *
      * @return boolean
      */
-    protected function openString($end, &$out, $nestingOpen = null, $nestingClose = null, $trimEnd = true)
+    protected function openString($end, &$out, $nestOpen = null, $nestClose = null, $rtrim = true, $disallow = null)
     {
         $oldWhite = $this->eatWhiteDefault;
         $this->eatWhiteDefault = false;
 
-        if ($nestingOpen && ! $nestingClose) {
-            $nestingClose = $end;
+        if ($nestOpen && ! $nestClose) {
+            $nestClose = $end;
         }
 
-        $patt = '(.*?)([\'"]|#\{|'
+        $patt = ($disallow ? '[^' . $this->pregQuote($disallow) . ']' : '.');
+        $patt = '(' . $patt . '*?)([\'"]|#\{|'
             . $this->pregQuote($end) . '|'
-            . (($nestingClose && $nestingClose !== $end) ? $this->pregQuote($nestingClose) . '|' : '')
+            . (($nestClose && $nestClose !== $end) ? $this->pregQuote($nestClose) . '|' : '')
             . static::$commentPattern . ')';
 
         $nestingLevel = 0;
@@ -2609,24 +3109,24 @@ class Parser
             if (isset($m[1]) && $m[1] !== '') {
                 $content[] = $m[1];
 
-                if ($nestingOpen) {
-                    $nestingLevel += substr_count($m[1], $nestingOpen);
+                if ($nestOpen) {
+                    $nestingLevel += substr_count($m[1], $nestOpen);
                 }
             }
 
             $tok = $m[2];
 
-            $this->count-= \strlen($tok);
+            $this->count -= \strlen($tok);
 
             if ($tok === $end && ! $nestingLevel) {
                 break;
             }
 
-            if ($tok === $nestingClose) {
+            if ($tok === $nestClose) {
                 $nestingLevel--;
             }
 
-            if (($tok === "'" || $tok === '"') && $this->string($str)) {
+            if (($tok === "'" || $tok === '"') && $this->string($str, true)) {
                 $content[] = $str;
                 continue;
             }
@@ -2637,7 +3137,7 @@ class Parser
             }
 
             $content[] = $tok;
-            $this->count+= \strlen($tok);
+            $this->count += \strlen($tok);
         }
 
         $this->eatWhiteDefault = $oldWhite;
@@ -2647,7 +3147,7 @@ class Parser
         }
 
         // trim the end
-        if ($trimEnd && \is_string(end($content))) {
+        if ($rtrim && \is_string(end($content))) {
             $content[\count($content) - 1] = rtrim(end($content));
         }
 
@@ -2673,13 +3173,20 @@ class Parser
 
         $s = $this->count;
 
-        if ($this->literal('#{', 2) && $this->valueList($value) && $this->matchChar('}', false)) {
+        if (
+            $this->literal('#{', 2) &&
+            $this->valueList($value) &&
+            $this->matchChar('}', false)
+        ) {
             if ($value === [Type::T_SELF]) {
                 $out = $value;
             } else {
                 if ($lookWhite) {
                     $left = ($s > 0 && preg_match('/\s/', $this->buffer[$s - 1])) ? ' ' : '';
-                    $right = preg_match('/\s/', $this->buffer[$this->count]) ? ' ': '';
+                    $right = (
+                        ! empty($this->buffer[$this->count]) &&
+                        preg_match('/\s/', $this->buffer[$this->count])
+                    ) ? ' ' : '';
                 } else {
                     $left = $right = false;
                 }
@@ -2786,6 +3293,11 @@ class Parser
                 continue;
             }
 
+            if ($this->matchChar('&', false)) {
+                $parts[] = [Type::T_SELF];
+                continue;
+            }
+
             if ($this->variable($var)) {
                 $parts[] = $var;
                 continue;
@@ -2862,11 +3374,15 @@ class Parser
     {
         $selector = [];
 
+        $discardComments = $this->discardComments;
+        $this->discardComments = true;
+
         for (;;) {
             $s = $this->count;
 
             if ($this->match('[>+~]+', $m, true)) {
-                if ($subSelector && \is_string($subSelector) && strpos($subSelector, 'nth-') === 0 &&
+                if (
+                    $subSelector && \is_string($subSelector) && strpos($subSelector, 'nth-') === 0 &&
                     $m[0] === '+' && $this->match("(\d+|n\b)", $counter)
                 ) {
                     $this->seek($s);
@@ -2878,18 +3394,15 @@ class Parser
 
             if ($this->selectorSingle($part, $subSelector)) {
                 $selector[] = $part;
-                $this->match('\s+', $m);
-                continue;
-            }
-
-            if ($this->match('\/[^\/]+\/', $m, true)) {
-                $selector[] = [$m[0]];
+                $this->whitespace();
                 continue;
             }
 
             break;
         }
 
+        $this->discardComments = $discardComments;
+
         if (! $selector) {
             return false;
         }
@@ -2945,6 +3458,7 @@ class Parser
                 case '&':
                     $parts[] = Compiler::$selfSelector;
                     $this->count++;
+                    ! $this->cssOnly || $this->assertPlainCssValid(false, $s);
                     continue 2;
 
                 case '.':
@@ -2969,6 +3483,7 @@ class Parser
                 if ($this->placeholder($placeholder)) {
                     $parts[] = '%';
                     $parts[] = $placeholder;
+                    ! $this->cssOnly || $this->assertPlainCssValid(false, $s);
                     continue;
                 }
 
@@ -2978,6 +3493,7 @@ class Parser
             if ($char === '#') {
                 if ($this->interpolation($inter)) {
                     $parts[] = $inter;
+                    ! $this->cssOnly || $this->assertPlainCssValid(false, $s);
                     continue;
                 }
 
@@ -3005,13 +3521,19 @@ class Parser
 
                     $ss = $this->count;
 
-                    if ($nameParts === ['not'] || $nameParts === ['is'] ||
-                        $nameParts === ['has'] || $nameParts === ['where'] ||
+                    if (
+                        $nameParts === ['not'] ||
+                        $nameParts === ['is'] ||
+                        $nameParts === ['has'] ||
+                        $nameParts === ['where'] ||
                         $nameParts === ['slotted'] ||
-                        $nameParts === ['nth-child'] || $nameParts == ['nth-last-child'] ||
-                        $nameParts === ['nth-of-type'] || $nameParts == ['nth-last-of-type']
+                        $nameParts === ['nth-child'] ||
+                        $nameParts === ['nth-last-child'] ||
+                        $nameParts === ['nth-of-type'] ||
+                        $nameParts === ['nth-last-of-type']
                     ) {
-                        if ($this->matchChar('(', true) &&
+                        if (
+                            $this->matchChar('(', true) &&
                             ($this->selectors($subs, reset($nameParts)) || true) &&
                             $this->matchChar(')')
                         ) {
@@ -3037,21 +3559,20 @@ class Parser
                         } else {
                             $this->seek($ss);
                         }
-                    } else {
-                        if ($this->matchChar('(') &&
-                            ($this->openString(')', $str, '(') || true) &&
-                            $this->matchChar(')')
-                        ) {
-                            $parts[] = '(';
-
-                            if (! empty($str)) {
-                                $parts[] = $str;
-                            }
+                    } elseif (
+                        $this->matchChar('(', true) &&
+                        ($this->openString(')', $str, '(') || true) &&
+                        $this->matchChar(')')
+                    ) {
+                        $parts[] = '(';
 
-                            $parts[] = ')';
-                        } else {
-                            $this->seek($ss);
+                        if (! empty($str)) {
+                            $parts[] = $str;
                         }
+
+                        $parts[] = ')';
+                    } else {
+                        $this->seek($ss);
                     }
 
                     continue;
@@ -3072,7 +3593,8 @@ class Parser
             $this->seek($s);
 
             // attribute selector
-            if ($char === '[' &&
+            if (
+                $char === '[' &&
                 $this->matchChar('[') &&
                 ($this->openString(']', $str, '[') || true) &&
                 $this->matchChar(']')
@@ -3125,7 +3647,10 @@ class Parser
     {
         $s = $this->count;
 
-        if ($this->matchChar('$', false) && $this->keyword($name)) {
+        if (
+            $this->matchChar('$', false) &&
+            $this->keyword($name)
+        ) {
             if ($this->allowVars) {
                 $out = [Type::T_VARIABLE, $name];
             } else {
@@ -3150,15 +3675,51 @@ class Parser
      */
     protected function keyword(&$word, $eatWhitespace = null)
     {
-        if ($this->match(
+        $s = $this->count;
+        $match = $this->match(
             $this->utf8
                 ? '(([\pL\w\x{00A0}-\x{10FFFF}_\-\*!"\']|[\\\\].)([\pL\w\x{00A0}-\x{10FFFF}\-_"\']|[\\\\].)*)'
                 : '(([\w_\-\*!"\']|[\\\\].)([\w\-_"\']|[\\\\].)*)',
             $m,
-            $eatWhitespace
-        )) {
+            false
+        );
+
+        if ($match) {
             $word = $m[1];
 
+            // handling of escaping in keyword : get the escaped char
+            if (strpos($word, '\\') !== false) {
+                $send = $this->count;
+                $escapedWord = [];
+                $this->seek($s);
+                $previousEscape = false;
+                while ($this->count < $send) {
+                    $char = $this->buffer[$this->count];
+                    $this->count++;
+                    if (
+                        $this->count < $send
+                        && $char === '\\'
+                        && !$previousEscape
+                        && $this->matchEscapeCharacter($out, true)
+                    ) {
+                        $escapedWord[] = $out;
+                    } else {
+                        if ($previousEscape) {
+                            $previousEscape = false;
+                        } elseif ($char === '\\') {
+                            $previousEscape = true;
+                        }
+                        $escapedWord[] = $char;
+                    }
+                }
+
+                $word = implode('', $escapedWord);
+            }
+
+            if (is_null($eatWhitespace) ? $this->eatWhiteDefault : $eatWhitespace) {
+                $this->whitespace();
+            }
+
             return true;
         }
 
@@ -3195,12 +3756,14 @@ class Parser
      */
     protected function placeholder(&$placeholder)
     {
-        if ($this->match(
+        $match = $this->match(
             $this->utf8
                 ? '([\pL\w\-_]+)'
                 : '([\w\-_]+)',
             $m
-        )) {
+        );
+
+        if ($match) {
             $placeholder = $m[1];
 
             return true;
@@ -3222,10 +3785,28 @@ class Parser
      */
     protected function url(&$out)
     {
-        if ($this->match('(url\(\s*(["\']?)([^)]+)\2\s*\))', $m)) {
-            $out = [Type::T_STRING, '', ['url(' . $m[2] . $m[3] . $m[2] . ')']];
+        if ($this->literal('url(', 4)) {
+            $s = $this->count;
 
-            return true;
+            if (
+                ($this->string($out) || $this->spaceList($out)) &&
+                $this->matchChar(')')
+            ) {
+                $out = [Type::T_STRING, '', ['url(', $out, ')']];
+
+                return true;
+            }
+
+            $this->seek($s);
+
+            if (
+                $this->openString(')', $out) &&
+                $this->matchChar(')')
+            ) {
+                $out = [Type::T_STRING, '', ['url(', $out, ')']];
+
+                return true;
+            }
         }
 
         return false;
@@ -3233,12 +3814,13 @@ class Parser
 
     /**
      * Consume an end of statement delimiter
+     * @param bool $eatWhitespace
      *
      * @return boolean
      */
-    protected function end()
+    protected function end($eatWhitespace = null)
     {
-        if ($this->matchChar(';')) {
+        if ($this->matchChar(';', $eatWhitespace)) {
             return true;
         }
 
index c96a917869ba3db42dd32b2e253a28b37d6b1200..549b08edced1234ba83ad6e3c4615098b739f799 100644 (file)
@@ -1,4 +1,5 @@
 <?php
+
 /**
  * SCSSPHP
  *
@@ -21,11 +22,14 @@ class Type
     const T_ASSIGN = 'assign';
     const T_AT_ROOT = 'at-root';
     const T_BLOCK = 'block';
+    /** @deprecated */
     const T_BREAK = 'break';
     const T_CHARSET = 'charset';
     const T_COLOR = 'color';
     const T_COMMENT = 'comment';
+    /** @deprecated */
     const T_CONTINUE = 'continue';
+    /** @deprecated */
     const T_CONTROL = 'control';
     const T_CUSTOM_PROPERTY = 'custom';
     const T_DEBUG = 'debug';
@@ -38,6 +42,7 @@ class Type
     const T_EXTEND = 'extend';
     const T_FOR = 'for';
     const T_FUNCTION = 'function';
+    const T_FUNCTION_REFERENCE = 'function-reference';
     const T_FUNCTION_CALL = 'fncall';
     const T_HSL = 'hsl';
     const T_IF = 'if';
index a5c25aaff63dded71c6f57e70a177511b213ba18..980f570af42324963897600388e15d10161c0064 100644 (file)
@@ -1,4 +1,5 @@
 <?php
+
 /**
  * SCSSPHP
  *
@@ -39,6 +40,10 @@ class Util
         $val = $value[1];
         $grace = new Range(-0.00001, 0.00001);
 
+        if (! \is_numeric($val)) {
+            throw new RangeException("$name {$val} is not a number.");
+        }
+
         if ($range->includes($val)) {
             return $val;
         }
@@ -67,4 +72,89 @@ class Util
 
         return strtr(rawurlencode($string), $revert);
     }
+
+    /**
+     * mb_chr() wrapper
+     *
+     * @param integer $code
+     *
+     * @return string
+     */
+    public static function mbChr($code)
+    {
+        // Use the native implementation if available, but not on PHP 7.2 as mb_chr(0) is buggy there
+        if (\PHP_VERSION_ID > 70300 && \function_exists('mb_chr')) {
+            return mb_chr($code, 'UTF-8');
+        }
+
+        if (0x80 > $code %= 0x200000) {
+            $s = \chr($code);
+        } elseif (0x800 > $code) {
+            $s = \chr(0xC0 | $code >> 6) . \chr(0x80 | $code & 0x3F);
+        } elseif (0x10000 > $code) {
+            $s = \chr(0xE0 | $code >> 12) . \chr(0x80 | $code >> 6 & 0x3F) . \chr(0x80 | $code & 0x3F);
+        } else {
+            $s = \chr(0xF0 | $code >> 18) . \chr(0x80 | $code >> 12 & 0x3F)
+                . \chr(0x80 | $code >> 6 & 0x3F) . \chr(0x80 | $code & 0x3F);
+        }
+
+        return $s;
+    }
+
+    /**
+     * mb_strlen() wrapper
+     *
+     * @param string $string
+     * @return false|int
+     */
+    public static function mbStrlen($string)
+    {
+        // Use the native implementation if available.
+        if (\function_exists('mb_strlen')) {
+            return mb_strlen($string, 'UTF-8');
+        }
+
+        if (\function_exists('iconv_strlen')) {
+            return @iconv_strlen($string, 'UTF-8');
+        }
+
+        return strlen($string);
+    }
+
+    /**
+     * mb_substr() wrapper
+     * @param string $string
+     * @param int $start
+     * @param null|int $length
+     * @return string
+     */
+    public static function mbSubstr($string, $start, $length = null)
+    {
+        // Use the native implementation if available.
+        if (\function_exists('mb_substr')) {
+            return mb_substr($string, $start, $length, 'UTF-8');
+        }
+
+        if (\function_exists('iconv_substr')) {
+            if ($start < 0) {
+                $start = static::mbStrlen($string) + $start;
+                if ($start < 0) {
+                    $start = 0;
+                }
+            }
+
+            if (null === $length) {
+                $length = 2147483647;
+            } elseif ($length < 0) {
+                $length = static::mbStrlen($string) + $length - $start;
+                if ($length < 0) {
+                    return '';
+                }
+            }
+
+            return (string)iconv_substr($string, $start, $length, 'UTF-8');
+        }
+
+        return substr($string, $start, $length);
+    }
 }
index 2b38067e1730ea05bd93268bb80edc8fcfd27ab1..cd3e190f159d6811c1ae51e27c8fdae0ef40b0f2 100644 (file)
@@ -1,4 +1,5 @@
 <?php
+
 /**
  * SCSSPHP
  *
@@ -18,5 +19,5 @@ namespace ScssPhp\ScssPhp;
  */
 class Version
 {
-    const VERSION = '1.1.1';
+    const VERSION = '1.3';
 }