Update scssphp to 0.6.6
authorTim Düsterhus <duesterhus@woltlab.com>
Fri, 21 Oct 2016 15:42:58 +0000 (17:42 +0200)
committerTim Düsterhus <duesterhus@woltlab.com>
Fri, 21 Oct 2016 15:42:58 +0000 (17:42 +0200)
27 files changed:
wcfsetup/install/files/lib/system/style/scssphp/LICENSE.md [new file with mode: 0644]
wcfsetup/install/files/lib/system/style/scssphp/classmap.php [deleted file]
wcfsetup/install/files/lib/system/style/scssphp/scss.inc.php
wcfsetup/install/files/lib/system/style/scssphp/src/Base/Range.php
wcfsetup/install/files/lib/system/style/scssphp/src/Block.php [new file with mode: 0644]
wcfsetup/install/files/lib/system/style/scssphp/src/Colors.php
wcfsetup/install/files/lib/system/style/scssphp/src/Compiler.php
wcfsetup/install/files/lib/system/style/scssphp/src/Compiler/Environment.php [new file with mode: 0644]
wcfsetup/install/files/lib/system/style/scssphp/src/Exception/CompilerException.php [new file with mode: 0644]
wcfsetup/install/files/lib/system/style/scssphp/src/Exception/ParserException.php [new file with mode: 0644]
wcfsetup/install/files/lib/system/style/scssphp/src/Exception/ServerException.php [new file with mode: 0644]
wcfsetup/install/files/lib/system/style/scssphp/src/Formatter.php
wcfsetup/install/files/lib/system/style/scssphp/src/Formatter/Compact.php
wcfsetup/install/files/lib/system/style/scssphp/src/Formatter/Compressed.php
wcfsetup/install/files/lib/system/style/scssphp/src/Formatter/Crunched.php
wcfsetup/install/files/lib/system/style/scssphp/src/Formatter/Debug.php [new file with mode: 0644]
wcfsetup/install/files/lib/system/style/scssphp/src/Formatter/Expanded.php
wcfsetup/install/files/lib/system/style/scssphp/src/Formatter/Nested.php
wcfsetup/install/files/lib/system/style/scssphp/src/Formatter/OutputBlock.php [new file with mode: 0644]
wcfsetup/install/files/lib/system/style/scssphp/src/Node.php [new file with mode: 0644]
wcfsetup/install/files/lib/system/style/scssphp/src/Node/Number.php [new file with mode: 0644]
wcfsetup/install/files/lib/system/style/scssphp/src/Parser.php
wcfsetup/install/files/lib/system/style/scssphp/src/Server.php
wcfsetup/install/files/lib/system/style/scssphp/src/Type.php [new file with mode: 0644]
wcfsetup/install/files/lib/system/style/scssphp/src/Util.php
wcfsetup/install/files/lib/system/style/scssphp/src/Version.php
wcfsetup/install/files/style/ui/imageViewer.scss

diff --git a/wcfsetup/install/files/lib/system/style/scssphp/LICENSE.md b/wcfsetup/install/files/lib/system/style/scssphp/LICENSE.md
new file mode 100644 (file)
index 0000000..2f5412f
--- /dev/null
@@ -0,0 +1,20 @@
+Copyright (c) 2015 Leaf Corcoran, http://leafo.github.io/scssphp
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
diff --git a/wcfsetup/install/files/lib/system/style/scssphp/classmap.php b/wcfsetup/install/files/lib/system/style/scssphp/classmap.php
deleted file mode 100644 (file)
index 2d8a52b..0000000
+++ /dev/null
@@ -1,63 +0,0 @@
-<?php
-/**
- * SCSSPHP
- *
- * Stub classes for backward compatibility
- *
- * @copyright 2012-2015 Leaf Corcoran
- *
- * @license http://opensource.org/licenses/MIT MIT
- *
- * @link http://leafo.github.io/scssphp
- */
-
-// @codingStandardsIgnoreStart
-/**
- * @deprecated since 0.1.0
- */
-class scssc extends \Leafo\ScssPhp\Compiler
-{
-}
-
-/**
- * @deprecated since 0.1.0
- */
-class scss_parser extends \Leafo\ScssPhp\Parser
-{
-}
-
-/**
- * @deprecated since 0.1.0
- */
-class scss_formatter extends \Leafo\ScssPhp\Formatter\Expanded
-{
-}
-
-/**
- * @deprecated since 0.1.0
- */
-class scss_formatter_nested extends \Leafo\ScssPhp\Formatter\Nested
-{
-}
-
-/**
- * @deprecated since 0.1.0
- */
-class scss_formatter_compressed extends \Leafo\ScssPhp\Formatter\Compressed
-{
-}
-
-/**
- * @deprecated since 0.1.0
- */
-class scss_formatter_crunched extends \Leafo\ScssPhp\Formatter\Crunched
-{
-}
-
-/**
- * @deprecated since 0.1.0
- */
-class scss_server extends \Leafo\ScssPhp\Server
-{
-}
-// @codingStandardsIgnoreEnd
index 51ae57e5c4633d3ffe7d556db6b67cb2c055615f..b6892fec4af75a80a991ac332636c5256ac8e99c 100644 (file)
@@ -1,21 +1,30 @@
 <?php
-if (version_compare(PHP_VERSION, '5.3') < 0) {
-    die('Requires PHP 5.3 or above');
+if (version_compare(PHP_VERSION, '5.4') < 0) {
+    throw new \Exception('scssphp requires PHP 5.4 or above');
 }
 
 if (! class_exists('scssc', false)) {
     include_once __DIR__ . '/src/Base/Range.php';
+    include_once __DIR__ . '/src/Block.php';
     include_once __DIR__ . '/src/Colors.php';
     include_once __DIR__ . '/src/Compiler.php';
+    include_once __DIR__ . '/src/Compiler/Environment.php';
+    include_once __DIR__ . '/src/Exception/CompilerException.php';
+    include_once __DIR__ . '/src/Exception/ParserException.php';
+    include_once __DIR__ . '/src/Exception/ServerException.php';
     include_once __DIR__ . '/src/Formatter.php';
     include_once __DIR__ . '/src/Formatter/Compact.php';
     include_once __DIR__ . '/src/Formatter/Compressed.php';
     include_once __DIR__ . '/src/Formatter/Crunched.php';
+    include_once __DIR__ . '/src/Formatter/Debug.php';
     include_once __DIR__ . '/src/Formatter/Expanded.php';
     include_once __DIR__ . '/src/Formatter/Nested.php';
+    include_once __DIR__ . '/src/Formatter/OutputBlock.php';
+    include_once __DIR__ . '/src/Node.php';
+    include_once __DIR__ . '/src/Node/Number.php';
     include_once __DIR__ . '/src/Parser.php';
+    include_once __DIR__ . '/src/Type.php';
     include_once __DIR__ . '/src/Util.php';
     include_once __DIR__ . '/src/Version.php';
     include_once __DIR__ . '/src/Server.php';
-    include_once __DIR__ . '/classmap.php';
 }
index 769205560e767b54ea00fa423b6ed41584d0dffa..a591d7b094ff97906ad9bb84a1eabd9666a4a9e2 100644 (file)
@@ -12,7 +12,7 @@
 namespace Leafo\ScssPhp\Base;
 
 /**
- * Range class
+ * Range
  *
  * @author Anthon Pang <anthon.pang@gmail.com>
  */
diff --git a/wcfsetup/install/files/lib/system/style/scssphp/src/Block.php b/wcfsetup/install/files/lib/system/style/scssphp/src/Block.php
new file mode 100644 (file)
index 0000000..6b972a1
--- /dev/null
@@ -0,0 +1,60 @@
+<?php
+/**
+ * SCSSPHP
+ *
+ * @copyright 2012-2015 Leaf Corcoran
+ *
+ * @license http://opensource.org/licenses/MIT MIT
+ *
+ * @link http://leafo.github.io/scssphp
+ */
+
+namespace Leafo\ScssPhp;
+
+/**
+ * Block
+ *
+ * @author Anthon Pang <anthon.pang@gmail.com>
+ */
+class Block
+{
+    /**
+     * @var string
+     */
+    public $type;
+
+    /**
+     * @var \Leafo\ScssPhp\Block
+     */
+    public $parent;
+
+    /**
+     * @var integer
+     */
+    public $sourceIndex;
+
+    /**
+     * @var integer
+     */
+    public $sourceLine;
+
+    /**
+     * @var integer
+     */
+    public $sourceColumn;
+
+    /**
+     * @var array
+     */
+    public $selectors;
+
+    /**
+     * @var array
+     */
+    public $comments;
+
+    /**
+     * @var array
+     */
+    public $children;
+}
index dd32be2146367aebc0737de18c1b4788a58507e2..ff48c199ee0129c4dde1ccfa56a0544992aeea8a 100644 (file)
@@ -22,8 +22,10 @@ class Colors
      * CSS Colors
      *
      * @see http://www.w3.org/TR/css3-color
+     *
+     * @var array
      */
-    public static $cssColors = array(
+    public static $cssColors = [
         'aliceblue' => '240,248,255',
         'antiquewhite' => '250,235,215',
         'aqua' => '0,255,255',
@@ -143,6 +145,7 @@ 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',
@@ -171,6 +174,6 @@ class Colors
         'white' => '255,255,255',
         'whitesmoke' => '245,245,245',
         'yellow' => '255,255,0',
-        'yellowgreen' => '154,205,50'
-    );
+        'yellowgreen' => '154,205,50',
+    ];
 }
index bef345bc6cf93f2d8ea63c76d5d1921841575d97..592f53b93bfba99ca17dd081fd7596425fec663c 100644 (file)
 namespace Leafo\ScssPhp;
 
 use Leafo\ScssPhp\Base\Range;
+use Leafo\ScssPhp\Block;
 use Leafo\ScssPhp\Colors;
+use Leafo\ScssPhp\Compiler\Environment;
+use Leafo\ScssPhp\Exception\CompilerException;
+use Leafo\ScssPhp\Formatter\OutputBlock;
+use Leafo\ScssPhp\Node;
+use Leafo\ScssPhp\Type;
 use Leafo\ScssPhp\Parser;
 use Leafo\ScssPhp\Util;
 
@@ -53,82 +59,99 @@ class Compiler
     const LINE_COMMENTS = 1;
     const DEBUG_INFO    = 2;
 
-    /**
-     * @var array
-     */
-    static protected $operatorNames = array(
-        '+' => 'add',
-        '-' => 'sub',
-        '*' => 'mul',
-        '/' => 'div',
-        '%' => 'mod',
-
-        '==' => 'eq',
-        '!=' => 'neq',
-        '<' => 'lt',
-        '>' => 'gt',
-
-        '<=' => 'lte',
-        '>=' => 'gte',
-    );
+    const WITH_RULE     = 1;
+    const WITH_MEDIA    = 2;
+    const WITH_SUPPORTS = 4;
+    const WITH_ALL      = 7;
 
     /**
      * @var array
      */
-    static protected $namespaces = array(
-        'special' => '%',
-        'mixin' => '@',
-        'function' => '^',
-    );
+    static protected $operatorNames = [
+        '+'   => 'add',
+        '-'   => 'sub',
+        '*'   => 'mul',
+        '/'   => 'div',
+        '%'   => 'mod',
+
+        '=='  => 'eq',
+        '!='  => 'neq',
+        '<'   => 'lt',
+        '>'   => 'gt',
+
+        '<='  => 'lte',
+        '>='  => 'gte',
+        '<=>' => 'cmp',
+    ];
 
     /**
      * @var array
      */
-    static protected $unitTable = array(
-        'in' => array(
-            'in' => 1,
-            'pt' => 72,
-            'pc' => 6,
-            'cm' => 2.54,
-            'mm' => 25.4,
-            'px' => 96,
-            'q'  => 101.6,
-        )
-    );
-
-    static public $true = array('keyword', 'true');
-    static public $false = array('keyword', 'false');
-    static public $null = array('null');
-    static public $defaultValue = array('keyword', '');
-    static public $selfSelector = array('self');
-    static public $emptyList = array('list', '', array());
-    static public $emptyMap = array('map', array(), array());
-    static public $emptyString = array('string', '"', array());
-
-    protected $importPaths = array('');
-    protected $importCache = array();
-    protected $userFunctions = array();
-    protected $registeredVars = array();
-
-    protected $numberPrecision = 5;
+    static protected $namespaces = [
+        'special'  => '%',
+        'mixin'    => '@',
+        'function' => '^',
+    ];
+
+    static public $true = [Type::T_KEYWORD, 'true'];
+    static public $false = [Type::T_KEYWORD, 'false'];
+    static public $null = [Type::T_NULL];
+    static public $nullString = [Type::T_STRING, '', []];
+    static public $defaultValue = [Type::T_KEYWORD, ''];
+    static public $selfSelector = [Type::T_SELF];
+    static public $emptyList = [Type::T_LIST, '', []];
+    static public $emptyMap = [Type::T_MAP, [], []];
+    static public $emptyString = [Type::T_STRING, '"', []];
+    static public $with = [Type::T_KEYWORD, 'with'];
+    static public $without = [Type::T_KEYWORD, 'without'];
+
+    protected $importPaths = [''];
+    protected $importCache = [];
+    protected $importedFiles = [];
+    protected $userFunctions = [];
+    protected $registeredVars = [];
+    protected $registeredFeatures = [
+        'extend-selector-pseudoclass' => false,
+        'at-error'                    => true,
+        'units-level-3'               => false,
+        'global-variable-shadowing'   => false,
+    ];
+
+    protected $encoding = null;
     protected $lineNumberStyle = null;
 
     protected $formatter = 'Leafo\ScssPhp\Formatter\Nested';
 
+    protected $rootEnv;
+    protected $rootBlock;
+
+    protected $env;
+    protected $scope;
+    protected $storeEnv;
+    protected $charsetSeen;
+    protected $sourceNames;
+
     private $indentLevel;
     private $commentsSeen;
     private $extends;
     private $extendsMap;
     private $parsedFiles;
-    private $env;
-    private $scope;
     private $parser;
-    private $sourcePos;
-    private $sourceParser;
-    private $storeEnv;
-    private $charsetSeen;
+    private $sourceIndex;
+    private $sourceLine;
+    private $sourceColumn;
     private $stderr;
     private $shouldEvaluate;
+    private $ignoreErrors;
+
+    /**
+     * Constructor
+     */
+    public function __construct()
+    {
+        $this->parsedFiles = [];
+        $this->sourceNames = [];
+    }
 
     /**
      * Compile scss
@@ -136,32 +159,37 @@ class Compiler
      * @api
      *
      * @param string $code
-     * @param string $name
+     * @param string $path
      *
      * @return string
      */
-    public function compile($code, $name = null)
+    public function compile($code, $path = null)
     {
-        $this->indentLevel  = -1;
-        $this->commentsSeen = array();
-        $this->extends      = array();
-        $this->extendsMap   = array();
-        $this->parsedFiles  = array();
-        $this->env          = null;
-        $this->scope        = null;
-
-        $this->stderr = fopen('php://stderr', 'w');
-
         $locale = setlocale(LC_NUMERIC, 0);
         setlocale(LC_NUMERIC, 'C');
 
-        $this->parser = new Parser($name);
-
+        $this->indentLevel    = -1;
+        $this->commentsSeen   = [];
+        $this->extends        = [];
+        $this->extendsMap     = [];
+        $this->sourceIndex    = null;
+        $this->sourceLine     = null;
+        $this->sourceColumn   = null;
+        $this->env            = null;
+        $this->scope          = null;
+        $this->storeEnv       = null;
+        $this->charsetSeen    = null;
+        $this->shouldEvaluate = null;
+        $this->stderr         = fopen('php://stderr', 'w');
+
+        $this->parser = $this->parserFactory($path);
         $tree = $this->parser->parse($code);
+        $this->parser = null;
 
         $this->formatter = new $this->formatter();
+        $this->rootBlock = null;
+        $this->rootEnv   = $this->pushEnv($tree);
 
-        $this->rootEnv = $this->pushEnv($tree);
         $this->injectVariables($this->registeredVars);
         $this->compileRoot($tree);
         $this->popEnv();
@@ -173,6 +201,23 @@ class Compiler
         return $out;
     }
 
+    /**
+     * Instantiate parser
+     *
+     * @param string $path
+     *
+     * @return \Leafo\ScssPhp\Parser
+     */
+    protected function parserFactory($path)
+    {
+        $parser = new Parser($path, count($this->sourceNames), $this->encoding);
+
+        $this->sourceNames[] = $path;
+        $this->addParsedFile($path);
+
+        return $parser;
+    }
+
     /**
      * Is self extend?
      *
@@ -195,23 +240,24 @@ class Compiler
     /**
      * Push extends
      *
-     * @param array $target
-     * @param array $origin
+     * @param array     $target
+     * @param array     $origin
+     * @param \stdClass $block
      */
-    protected function pushExtends($target, $origin)
+    protected function pushExtends($target, $origin, $block)
     {
         if ($this->isSelfExtend($target, $origin)) {
             return;
         }
 
         $i = count($this->extends);
-        $this->extends[] = array($target, $origin);
+        $this->extends[] = [$target, $origin, $block];
 
         foreach ($target as $part) {
             if (isset($this->extendsMap[$part])) {
                 $this->extendsMap[$part][] = $i;
             } else {
-                $this->extendsMap[$part] = array($i);
+                $this->extendsMap[$part] = [$i];
             }
         }
     }
@@ -222,17 +268,17 @@ class Compiler
      * @param string $type
      * @param array  $selectors
      *
-     * @return \stdClass
+     * @return \Leafo\ScssPhp\Formatter\OutputBlock
      */
     protected function makeOutputBlock($type, $selectors = null)
     {
-        $out = new \stdClass;
-        $out->type = $type;
-        $out->lines = array();
-        $out->children = array();
-        $out->parent = $this->scope;
+        $out = new OutputBlock;
+        $out->type      = $type;
+        $out->lines     = [];
+        $out->children  = [];
+        $out->parent    = $this->scope;
         $out->selectors = $selectors;
-        $out->depth = $this->env->depth;
+        $out->depth     = $this->env->depth;
 
         return $out;
     }
@@ -240,26 +286,52 @@ class Compiler
     /**
      * Compile root
      *
-     * @param \stdClass $rootBlock
+     * @param \Leafo\ScssPhp\Block $rootBlock
      */
-    protected function compileRoot($rootBlock)
+    protected function compileRoot(Block $rootBlock)
     {
-        $this->scope = $this->makeOutputBlock('root');
+        $this->rootBlock = $this->scope = $this->makeOutputBlock(Type::T_ROOT);
 
-        $this->compileChildren($rootBlock->children, $this->scope);
+        $this->compileChildrenNoReturn($rootBlock->children, $this->scope);
         $this->flattenSelectors($this->scope);
+        $this->missingSelectors();
+    }
+
+    /**
+     * Report missing selectors
+     */
+    protected function missingSelectors()
+    {
+        foreach ($this->extends as $extend) {
+            if (isset($extend[3])) {
+                continue;
+            }
+
+            list($target, $origin, $block) = $extend;
+
+            // ignore if !optional
+            if ($block[2]) {
+                continue;
+            }
+
+            $target = implode(' ', $target);
+            $origin = $this->collapseSelectors($origin);
+
+            $this->sourceLine = $block[Parser::SOURCE_LINE];
+            $this->throwError("\"$origin\" failed to @extend \"$target\". The selector \"$target\" was not found.");
+        }
     }
 
     /**
      * Flatten selectors
      *
-     * @param \stdClass $block
-     * @parent string   $parentKey
+     * @param \Leafo\ScssPhp\Formatter\OutputBlock $block
+     * @param string                               $parentKey
      */
-    protected function flattenSelectors($block, $parentKey = null)
+    protected function flattenSelectors(OutputBlock $block, $parentKey = null)
     {
         if ($block->selectors) {
-            $selectors = array();
+            $selectors = [];
 
             foreach ($block->selectors as $s) {
                 $selectors[] = $s;
@@ -274,16 +346,18 @@ class Compiler
 
                     // remove duplicates
                     array_walk($selectors, function (&$value) {
-                        $value = json_encode($value);
+                        $value = serialize($value);
                     });
+
                     $selectors = array_unique($selectors);
+
                     array_walk($selectors, function (&$value) {
-                        $value = json_decode($value);
+                        $value = unserialize($value);
                     });
                 }
             }
 
-            $block->selectors = array();
+            $block->selectors = [];
             $placeholderSelector = false;
 
             foreach ($selectors as $selector) {
@@ -323,23 +397,42 @@ class Compiler
             }
 
             if ($this->matchExtendsSingle($part, $origin)) {
-                $before = array_slice($selector, 0, $i);
                 $after = array_slice($selector, $i + 1);
-                $s = count($before);
+                $before = array_slice($selector, 0, $i);
+
+                list($before, $nonBreakableBefore) = $this->extractRelationshipFromFragment($before);
 
                 foreach ($origin as $new) {
                     $k = 0;
 
                     // remove shared parts
                     if ($initial) {
-                        while ($k < $s && isset($new[$k]) && $before[$k] === $new[$k]) {
+                        while ($k < $i && isset($new[$k]) && $selector[$k] === $new[$k]) {
                             $k++;
                         }
                     }
 
+                    $replacement = [];
+                    $tempReplacement = $k > 0 ? array_slice($new, $k) : $new;
+
+                    for ($l = count($tempReplacement) - 1; $l >= 0; $l--) {
+                        $slice = $tempReplacement[$l];
+                        array_unshift($replacement, $slice);
+
+                        if (! $this->isImmediateRelationshipCombinator(end($slice))) {
+                            break;
+                        }
+                    }
+
+                    $afterBefore = $l != 0 ? array_slice($tempReplacement, 0, $l) : [];
+
+                    // Merge shared direct relationships.
+                    $mergedBefore = $this->mergeDirectRelationships($afterBefore, $nonBreakableBefore);
+
                     $result = array_merge(
                         $before,
-                        $k > 0 ? array_slice($new, $k) : $new,
+                        $mergedBefore,
+                        $replacement,
                         $after
                     );
 
@@ -350,14 +443,22 @@ class Compiler
                     $out[] = $result;
 
                     // recursively check for more matches
-                    $this->matchExtends($result, $out, $i, false);
+                    $this->matchExtends($result, $out, count($before) + count($mergedBefore), false);
 
                     // selector sequence merging
                     if (! empty($before) && count($new) > 1) {
+                        $sharedParts = $k > 0 ? array_slice($before, 0, $k) : [];
+                        $postSharedParts = $k > 0 ? array_slice($before, $k) : $before;
+
+                        list($injectBetweenSharedParts, $nonBreakable2) = $this->extractRelationshipFromFragment($afterBefore);
+
                         $result2 = array_merge(
-                            array_slice($new, 0, -1),
-                            $k > 0 ? array_slice($before, $k) : $before,
-                            array_slice($new, -1),
+                            $sharedParts,
+                            $injectBetweenSharedParts,
+                            $postSharedParts,
+                            $nonBreakable2,
+                            $nonBreakableBefore,
+                            $replacement,
                             $after
                         );
 
@@ -378,8 +479,8 @@ class Compiler
      */
     protected function matchExtendsSingle($rawSingle, &$outOrigin)
     {
-        $counts = array();
-        $single = array();
+        $counts = [];
+        $single = [];
 
         foreach ($rawSingle as $part) {
             // matches Number
@@ -394,6 +495,13 @@ class Compiler
             }
         }
 
+        $extendingDecoratedTag = false;
+
+        if (count($single) > 1) {
+            $matches = null;
+            $extendingDecoratedTag = preg_match('/^[a-z0-9]+$/i', $single[0], $matches) ? $matches[0] : false;
+        }
+
         foreach ($single as $part) {
             if (isset($this->extendsMap[$part])) {
                 foreach ($this->extendsMap[$part] as $idx) {
@@ -402,17 +510,19 @@ class Compiler
             }
         }
 
-        $outOrigin = array();
+        $outOrigin = [];
         $found = false;
 
         foreach ($counts as $idx => $count) {
-            list($target, $origin) = $this->extends[$idx];
+            list($target, $origin, /* $block */) = $this->extends[$idx];
 
             // check count
             if ($count !== count($target)) {
                 continue;
             }
 
+            $this->extends[$idx][3] = true;
+
             $rem = array_diff($single, $target);
 
             foreach ($origin as $j => $new) {
@@ -421,7 +531,21 @@ class Compiler
                     return false;
                 }
 
-                $origin[$j][count($origin[$j]) - 1] = $this->combineSelectorSingle(end($new), $rem);
+                $replacement = end($new);
+
+                // Extending a decorated tag with another tag is not possible.
+                if ($extendingDecoratedTag && $replacement[0] != $extendingDecoratedTag &&
+                    preg_match('/^[a-z0-9]+$/i', $replacement[0])
+                ) {
+                    unset($origin[$j]);
+                    continue;
+                }
+
+                $combined = $this->combineSelectorSingle($replacement, $rem);
+
+                if (count(array_diff($combined, $origin[$j][count($origin[$j]) - 1]))) {
+                    $origin[$j][count($origin[$j]) - 1] = $combined;
+                }
             }
 
             $outOrigin = array_merge($outOrigin, $origin);
@@ -432,6 +556,39 @@ class Compiler
         return $found;
     }
 
+
+    /**
+     * Extract a relationship from the fragment.
+     *
+     * When extracting the last portion of a selector we will be left with a
+     * fragment which may end with a direction relationship combinator. This
+     * method will extract the relationship fragment and return it along side
+     * the rest.
+     *
+     * @param array $fragment The selector fragment maybe ending with a direction relationship combinator.
+     * @return array The selector without the relationship fragment if any, the relationship fragment.
+     */
+    protected function extractRelationshipFromFragment(array $fragment)
+    {
+        $parents = [];
+        $children = [];
+        $j = $i = count($fragment);
+
+        for (;;) {
+            $children = $j != $i ? array_slice($fragment, $j, $i - $j) : [];
+            $parents = array_slice($fragment, 0, $j);
+            $slice = end($parents);
+
+            if (empty($slice) || ! $this->isImmediateRelationshipCombinator($slice[0])) {
+                break;
+            }
+
+            $j -= 2;
+        }
+
+        return [$parents, $children];
+    }
+
     /**
      * Combine selector single
      *
@@ -442,21 +599,28 @@ class Compiler
      */
     protected function combineSelectorSingle($base, $other)
     {
-        $tag = null;
-        $out = array();
+        $tag = [];
+        $out = [];
+        $wasTag = true;
 
-        foreach (array($base, $other) as $single) {
+        foreach ([$base, $other] as $single) {
             foreach ($single as $part) {
-                if (preg_match('/^[^\[.#:]/', $part)) {
-                    $tag = $part;
-                } else {
+                if (preg_match('/^[\[.:#]/', $part)) {
                     $out[] = $part;
+                    $wasTag = false;
+                } elseif (preg_match('/^[^_-]/', $part)) {
+                    $tag[] = $part;
+                    $wasTag = true;
+                } elseif ($wasTag) {
+                    $tag[count($tag) - 1] .= $part;
+                } else {
+                    $out[count($out) - 1] .= $part;
                 }
             }
         }
 
-        if ($tag) {
-            array_unshift($out, $tag);
+        if (count($tag)) {
+            array_unshift($out, $tag[0]);
         }
 
         return $out;
@@ -465,16 +629,16 @@ class Compiler
     /**
      * Compile media
      *
-     * @param \stdClass $media
+     * @param \Leafo\ScssPhp\Block $media
      */
-    protected function compileMedia($media)
+    protected function compileMedia(Block $media)
     {
         $this->pushEnv($media);
 
         $mediaQuery = $this->compileMediaQuery($this->multiplyMedia($this->env));
 
         if (! empty($mediaQuery)) {
-            $this->scope = $this->makeOutputBlock('media', array($mediaQuery));
+            $this->scope = $this->makeOutputBlock(Type::T_MEDIA, [$mediaQuery]);
 
             $parentScope = $this->mediaParent($this->scope);
             $parentScope->children[] = $this->scope;
@@ -485,21 +649,30 @@ class Compiler
             foreach ($media->children as $child) {
                 $type = $child[0];
 
-                if ($type !== 'block' && $type !== 'media' && $type !== 'directive') {
+                if ($type !== Type::T_BLOCK &&
+                    $type !== Type::T_MEDIA &&
+                    $type !== Type::T_DIRECTIVE &&
+                    $type !== Type::T_IMPORT
+                ) {
                     $needsWrap = true;
                     break;
                 }
             }
 
             if ($needsWrap) {
-                $wrapped = (object)array(
-                    'selectors' => array(),
-                    'children' => $media->children
-                );
-                $media->children = array(array('block', $wrapped));
+                $wrapped = new Block;
+                $wrapped->sourceIndex  = $media->sourceIndex;
+                $wrapped->sourceLine   = $media->sourceLine;
+                $wrapped->sourceColumn = $media->sourceColumn;
+                $wrapped->selectors    = [];
+                $wrapped->comments     = [];
+                $wrapped->parent       = $media;
+                $wrapped->children     = $media->children;
+
+                $media->children = [[Type::T_BLOCK, $wrapped]];
             }
 
-            $this->compileChildren($media->children, $this->scope);
+            $this->compileChildrenNoReturn($media->children, $this->scope);
 
             $this->scope = $this->scope->parent;
         }
@@ -510,14 +683,14 @@ class Compiler
     /**
      * Media parent
      *
-     * @param \stdClass $scope
+     * @param \Leafo\ScssPhp\Formatter\OutputBlock $scope
      *
-     * @return \stdClass
+     * @return \Leafo\ScssPhp\Formatter\OutputBlock
      */
-    protected function mediaParent($scope)
+    protected function mediaParent(OutputBlock $scope)
     {
         while (! empty($scope->parent)) {
-            if (! empty($scope->type) && $scope->type !== 'media') {
+            if (! empty($scope->type) && $scope->type !== Type::T_MEDIA) {
                 break;
             }
 
@@ -528,21 +701,280 @@ class Compiler
     }
 
     /**
-     * Compile nested block
+     * Compile directive
      *
-     * @todo refactor compileNestedBlock and compileMedia into same thing?
+     * @param \Leafo\ScssPhp\Block $block
+     */
+    protected function compileDirective(Block $block)
+    {
+        $s = '@' . $block->name;
+
+        if (! empty($block->value)) {
+            $s .= ' ' . $this->compileValue($block->value);
+        }
+
+        if ($block->name === 'keyframes' || substr($block->name, -10) === '-keyframes') {
+            $this->compileKeyframeBlock($block, [$s]);
+        } else {
+            $this->compileNestedBlock($block, [$s]);
+        }
+    }
+
+    /**
+     * Compile at-root
      *
-     * @param \stdClass $block
-     * @param array     $selectors
+     * @param \Leafo\ScssPhp\Block $block
+     */
+    protected function compileAtRoot(Block $block)
+    {
+        $env     = $this->pushEnv($block);
+        $envs    = $this->compactEnv($env);
+        $without = isset($block->with) ? $this->compileWith($block->with) : self::WITH_RULE;
+
+        // wrap inline selector
+        if ($block->selector) {
+            $wrapped = new Block;
+            $wrapped->sourceIndex  = $block->sourceIndex;
+            $wrapped->sourceLine   = $block->sourceLine;
+            $wrapped->sourceColumn = $block->sourceColumn;
+            $wrapped->selectors    = $block->selector;
+            $wrapped->comments     = [];
+            $wrapped->parent       = $block;
+            $wrapped->children     = $block->children;
+
+            $block->children = [[Type::T_BLOCK, $wrapped]];
+        }
+
+        $this->env = $this->filterWithout($envs, $without);
+        $newBlock  = $this->spliceTree($envs, $block, $without);
+
+        $saveScope   = $this->scope;
+        $this->scope = $this->rootBlock;
+
+        $this->compileChild($newBlock, $this->scope);
+
+        $this->scope = $saveScope;
+        $this->env   = $this->extractEnv($envs);
+
+        $this->popEnv();
+    }
+
+    /**
+     * Splice parse tree
+     *
+     * @param array                $envs
+     * @param \Leafo\ScssPhp\Block $block
+     * @param integer              $without
+     *
+     * @return array
+     */
+    private function spliceTree($envs, Block $block, $without)
+    {
+        $newBlock = null;
+
+        foreach ($envs as $e) {
+            if (! isset($e->block)) {
+                continue;
+            }
+
+            if ($e->block === $block) {
+                continue;
+            }
+
+            if (isset($e->block->type) && $e->block->type === Type::T_AT_ROOT) {
+                continue;
+            }
+
+            if ($e->block && $this->isWithout($without, $e->block)) {
+                continue;
+            }
+
+            $b = new Block;
+            $b->sourceIndex  = $e->block->sourceIndex;
+            $b->sourceLine   = $e->block->sourceLine;
+            $b->sourceColumn = $e->block->sourceColumn;
+            $b->selectors    = [];
+            $b->comments     = $e->block->comments;
+            $b->parent       = null;
+
+            if ($newBlock) {
+                $type = isset($newBlock->type) ? $newBlock->type : Type::T_BLOCK;
+
+                $b->children = [[$type, $newBlock]];
+
+                $newBlock->parent = $b;
+            } elseif (count($block->children)) {
+                foreach ($block->children as $child) {
+                    if ($child[0] === Type::T_BLOCK) {
+                        $child[1]->parent = $b;
+                    }
+                }
+
+                $b->children = $block->children;
+            }
+
+            if (isset($e->block->type)) {
+                $b->type = $e->block->type;
+            }
+
+            if (isset($e->block->name)) {
+                $b->name = $e->block->name;
+            }
+
+            if (isset($e->block->queryList)) {
+                $b->queryList = $e->block->queryList;
+            }
+
+            if (isset($e->block->value)) {
+                $b->value = $e->block->value;
+            }
+
+            $newBlock = $b;
+        }
+
+        $type = isset($newBlock->type) ? $newBlock->type : Type::T_BLOCK;
+
+        return [$type, $newBlock];
+    }
+
+    /**
+     * Compile @at-root's with: inclusion / without: exclusion into filter flags
+     *
+     * @param array $with
+     *
+     * @return integer
+     */
+    private function compileWith($with)
+    {
+        static $mapping = [
+            'rule'     => self::WITH_RULE,
+            'media'    => self::WITH_MEDIA,
+            'supports' => self::WITH_SUPPORTS,
+            'all'      => self::WITH_ALL,
+        ];
+
+        // exclude selectors by default
+        $without = self::WITH_RULE;
+
+        if ($this->libMapHasKey([$with, self::$with])) {
+            $without = self::WITH_ALL;
+
+            $list = $this->coerceList($this->libMapGet([$with, self::$with]));
+
+            foreach ($list[2] as $item) {
+                $keyword = $this->compileStringContent($this->coerceString($item));
+
+                if (array_key_exists($keyword, $mapping)) {
+                    $without &= ~($mapping[$keyword]);
+                }
+            }
+        }
+
+        if ($this->libMapHasKey([$with, self::$without])) {
+            $without = 0;
+
+            $list = $this->coerceList($this->libMapGet([$with, self::$without]));
+
+            foreach ($list[2] as $item) {
+                $keyword = $this->compileStringContent($this->coerceString($item));
+
+                if (array_key_exists($keyword, $mapping)) {
+                    $without |= $mapping[$keyword];
+                }
+            }
+        }
+
+        return $without;
+    }
+
+    /**
+     * Filter env stack
+     *
+     * @param array   $envs
+     * @param integer $without
+     *
+     * @return \Leafo\ScssPhp\Compiler\Environment
+     */
+    private function filterWithout($envs, $without)
+    {
+        $filtered = [];
+
+        foreach ($envs as $e) {
+            if ($e->block && $this->isWithout($without, $e->block)) {
+                continue;
+            }
+
+            $filtered[] = $e;
+        }
+
+        return $this->extractEnv($filtered);
+    }
+
+    /**
+     * Filter WITH rules
+     *
+     * @param integer              $without
+     * @param \Leafo\ScssPhp\Block $block
+     *
+     * @return boolean
+     */
+    private function isWithout($without, Block $block)
+    {
+        if ((($without & self::WITH_RULE) && isset($block->selectors)) ||
+            (($without & self::WITH_MEDIA) &&
+                isset($block->type) && $block->type === Type::T_MEDIA) ||
+            (($without & self::WITH_SUPPORTS) &&
+                isset($block->type) && $block->type === Type::T_DIRECTIVE &&
+                isset($block->name) && $block->name === 'supports')
+        ) {
+            return true;
+        }
+
+        return false;
+    }
+
+    /**
+     * Compile keyframe block
+     *
+     * @param \Leafo\ScssPhp\Block $block
+     * @param array                $selectors
      */
-    protected function compileNestedBlock($block, $selectors)
+    protected function compileKeyframeBlock(Block $block, $selectors)
+    {
+        $env = $this->pushEnv($block);
+
+        $envs = $this->compactEnv($env);
+
+        $this->env = $this->extractEnv(array_filter($envs, function (Environment $e) {
+            return ! isset($e->block->selectors);
+        }));
+
+        $this->scope = $this->makeOutputBlock($block->type, $selectors);
+        $this->scope->depth = 1;
+        $this->scope->parent->children[] = $this->scope;
+
+        $this->compileChildrenNoReturn($block->children, $this->scope);
+
+        $this->scope = $this->scope->parent;
+        $this->env   = $this->extractEnv($envs);
+
+        $this->popEnv();
+    }
+
+    /**
+     * Compile nested block
+     *
+     * @param \Leafo\ScssPhp\Block $block
+     * @param array                $selectors
+     */
+    protected function compileNestedBlock(Block $block, $selectors)
     {
         $this->pushEnv($block);
 
         $this->scope = $this->makeOutputBlock($block->type, $selectors);
         $this->scope->parent->children[] = $this->scope;
 
-        $this->compileChildren($block->children, $this->scope);
+        $this->compileChildrenNoReturn($block->children, $this->scope);
 
         $this->scope = $this->scope->parent;
 
@@ -565,22 +997,21 @@ class Compiler
      *
      * @see Compiler::compileChild()
      *
-     * @param \stdClass $block
+     * @param \Leafo\ScssPhp\Block $block
      */
-    protected function compileBlock($block)
+    protected function compileBlock(Block $block)
     {
         $env = $this->pushEnv($block);
-
         $env->selectors = $this->evalSelectors($block->selectors);
 
-        $out = $this->makeOutputBlock(null, $this->multiplySelectors($env));
+        $out = $this->makeOutputBlock(null);
 
         if (isset($this->lineNumberStyle) && count($env->selectors) && count($block->children)) {
-            $annotation = $this->makeOutputBlock('comment');
+            $annotation = $this->makeOutputBlock(Type::T_COMMENT);
             $annotation->depth = 0;
 
-            $file = $block->sourceParser->getSourceName();
-            $line = $block->sourceParser->getLineNo($block->sourcePosition);
+            $file = $this->sourceNames[$block->sourceIndex];
+            $line = $block->sourceLine;
 
             switch ($this->lineNumberStyle) {
                 case self::LINE_COMMENTS:
@@ -598,7 +1029,11 @@ class Compiler
 
         $this->scope->children[] = $out;
 
-        $this->compileChildren($block->children, $out);
+        if (count($block->children)) {
+            $out->selectors = $this->multiplySelectors($env);
+
+            $this->compileChildrenNoReturn($block->children, $out);
+        }
 
         $this->formatter->stripSemicolon($out->lines);
 
@@ -612,7 +1047,7 @@ class Compiler
      */
     protected function compileComment($block)
     {
-        $out = $this->makeOutputBlock('comment');
+        $out = $this->makeOutputBlock(Type::T_COMMENT);
         $out->lines[] = $block[1];
         $this->scope->children[] = $out;
     }
@@ -628,15 +1063,15 @@ class Compiler
     {
         $this->shouldEvaluate = false;
 
-        $selectors = array_map(array($this, 'evalSelector'), $selectors);
+        $selectors = array_map([$this, 'evalSelector'], $selectors);
 
         // after evaluating interpolates, we might need a second pass
         if ($this->shouldEvaluate) {
             $buffer = $this->collapseSelectors($selectors);
-            $parser = new Parser(__METHOD__, false);
+            $parser = $this->parserFactory(__METHOD__);
 
             if ($parser->parseSelector($buffer, $newSelectors)) {
-                $selectors = array_map(array($this, 'evalSelector'), $newSelectors);
+                $selectors = array_map([$this, 'evalSelector'], $newSelectors);
             }
         }
 
@@ -652,7 +1087,7 @@ class Compiler
      */
     protected function evalSelector($selector)
     {
-        return array_map(array($this, 'evalSelectorPart'), $selector);
+        return array_map([$this, 'evalSelectorPart'], $selector);
     }
 
     /**
@@ -665,7 +1100,7 @@ class Compiler
     protected function evalSelectorPart($part)
     {
         foreach ($part as &$p) {
-            if (is_array($p) && ($p[0] === 'interpolate' || $p[0] === 'string')) {
+            if (is_array($p) && ($p[0] === Type::T_INTERPOLATE || $p[0] === Type::T_STRING)) {
                 $p = $this->compileValue($p);
 
                 // force re-evaluation
@@ -692,7 +1127,7 @@ class Compiler
      */
     protected function collapseSelectors($selectors)
     {
-        $parts = array();
+        $parts = [];
 
         foreach ($selectors as $selector) {
             $output = '';
@@ -719,7 +1154,7 @@ class Compiler
      */
     protected function flattenSelectorSingle($single)
     {
-        $joined = array();
+        $joined = [];
 
         foreach ($single as $part) {
             if (empty($joined) ||
@@ -756,7 +1191,7 @@ class Compiler
         return implode(
             ' ',
             array_map(
-                array($this, 'compileSelectorPart'),
+                [$this, 'compileSelectorPart'],
                 $selector
             )
         );
@@ -777,7 +1212,7 @@ class Compiler
             }
 
             switch ($p[0]) {
-                case 'self':
+                case Type::T_SELF:
                     $p = '&';
                     break;
 
@@ -805,7 +1240,7 @@ class Compiler
 
         foreach ($selector as $parts) {
             foreach ($parts as $part) {
-                if ('%' === $part[0]) {
+                if (strlen($part) && '%' === $part[0]) {
                     return true;
                 }
             }
@@ -815,14 +1250,14 @@ class Compiler
     }
 
     /**
-     * Compile children
+     * Compile children and return result
      *
-     * @param array $stms
-     * @param array $out
+     * @param array                                $stms
+     * @param \Leafo\ScssPhp\Formatter\OutputBlock $out
      *
      * @return array
      */
-    protected function compileChildren($stms, $out)
+    protected function compileChildren($stms, OutputBlock $out)
     {
         foreach ($stms as $stm) {
             $ret = $this->compileChild($stm, $out);
@@ -833,6 +1268,27 @@ class Compiler
         }
     }
 
+    /**
+     * Compile children and throw exception if unexpected @return
+     *
+     * @param array                                $stms
+     * @param \Leafo\ScssPhp\Formatter\OutputBlock $out
+     *
+     * @throws \Exception
+     */
+    protected function compileChildrenNoReturn($stms, OutputBlock $out)
+    {
+        foreach ($stms as $stm) {
+            $ret = $this->compileChild($stm, $out);
+
+            if (isset($ret)) {
+                $this->throwError('@return may only be used within a function');
+
+                return;
+            }
+        }
+    }
+
     /**
      * Compile media query
      *
@@ -847,26 +1303,26 @@ class Compiler
 
         foreach ($queryList as $query) {
             $type = null;
-            $parts = array();
+            $parts = [];
 
             foreach ($query as $q) {
                 switch ($q[0]) {
-                    case 'mediaType':
+                    case Type::T_MEDIA_TYPE:
                         if ($type) {
                             $type = $this->mergeMediaTypes(
                                 $type,
-                                array_map(array($this, 'compileValue'), array_slice($q, 1))
+                                array_map([$this, 'compileValue'], array_slice($q, 1))
                             );
 
                             if (empty($type)) { // merge failed
                                 return null;
                             }
                         } else {
-                            $type = array_map(array($this, 'compileValue'), array_slice($q, 1));
+                            $type = array_map([$this, 'compileValue'], array_slice($q, 1));
                         }
                         break;
 
-                    case 'mediaExp':
+                    case Type::T_MEDIA_EXPRESSION:
                         if (isset($q[2])) {
                             $parts[] = '('
                                 . $this->compileValue($q[1])
@@ -879,6 +1335,10 @@ class Compiler
                                 . ')';
                         }
                         break;
+
+                    case Type::T_MEDIA_VALUE:
+                        $parts[] = $this->compileValue($q[1]);
+                        break;
                 }
             }
 
@@ -901,6 +1361,37 @@ class Compiler
         return $out;
     }
 
+    protected function mergeDirectRelationships($selectors1, $selectors2)
+    {
+        if (empty($selectors1) || empty($selectors2)) {
+            return array_merge($selectors1, $selectors2);
+        }
+
+        $part1 = end($selectors1);
+        $part2 = end($selectors2);
+
+        if (! $this->isImmediateRelationshipCombinator($part1[0]) || $part1 !== $part2) {
+            return array_merge($selectors1, $selectors2);
+        }
+
+        $merged = [];
+
+        do {
+            $part1 = array_pop($selectors1);
+            $part2 = array_pop($selectors2);
+
+            if ($this->isImmediateRelationshipCombinator($part1[0]) && $part1 !== $part2) {
+                $merged = array_merge($selectors1, [$part1], $selectors2, [$part2], $merged);
+                break;
+            }
+
+            array_unshift($merged, $part1);
+            array_unshift($merged, [array_pop($selectors1)[0] . array_pop($selectors2)[0]]);
+        } while (! empty($selectors1) && ! empty($selectors2));
+
+        return $merged;
+    }
+
     /**
      * Merge media types
      *
@@ -939,24 +1430,24 @@ class Compiler
             $t2 = strtolower($type2[0]);
         }
 
-        if (($m1 === 'not') ^ ($m2 === 'not')) {
+        if (($m1 === Type::T_NOT) ^ ($m2 === Type::T_NOT)) {
             if ($t1 === $t2) {
                 return null;
             }
 
-            return array(
-                $m1 === 'not' ? $m2 : $m1,
-                $m1 === 'not' ? $t2 : $t1
-            );
+            return [
+                $m1 === Type::T_NOT ? $m2 : $m1,
+                $m1 === Type::T_NOT ? $t2 : $t1,
+            ];
         }
 
-        if ($m1 === 'not' && $m2 === 'not') {
+        if ($m1 === Type::T_NOT && $m2 === Type::T_NOT) {
             // CSS has no way of representing "neither screen nor print"
             if ($t1 !== $t2) {
                 return null;
             }
 
-            return array('not', $t1);
+            return [Type::T_NOT, $t1];
         }
 
         if ($t1 !== $t2) {
@@ -964,24 +1455,28 @@ class Compiler
         }
 
         // t1 == t2, neither m1 nor m2 are "not"
-        return array(empty($m1)? $m2 : $m1, $t1);
+        return [empty($m1)? $m2 : $m1, $t1];
     }
 
     /**
      * Compile import; returns true if the value was something that could be imported
      *
-     * @param array $rawPath
-     * @param array $out
+     * @param array   $rawPath
+     * @param array   $out
+     * @param boolean $once
      *
      * @return boolean
      */
-    protected function compileImport($rawPath, $out)
+    protected function compileImport($rawPath, $out, $once = false)
     {
-        if ($rawPath[0] === 'string') {
+        if ($rawPath[0] === Type::T_STRING) {
             $path = $this->compileStringContent($rawPath);
 
             if ($path = $this->findImport($path)) {
-                $this->importFile($path, $out);
+                if (! $once || ! in_array($path, $this->importedFiles)) {
+                    $this->importFile($path, $out);
+                    $this->importedFiles[] = $path;
+                }
 
                 return true;
             }
@@ -989,14 +1484,14 @@ class Compiler
             return false;
         }
 
-        if ($rawPath[0] === 'list') {
+        if ($rawPath[0] === Type::T_LIST) {
             // handle a list of strings
             if (count($rawPath[2]) === 0) {
                 return false;
             }
 
             foreach ($rawPath[2] as $path) {
-                if ($path[0] !== 'string') {
+                if ($path[0] !== Type::T_STRING) {
                     return false;
                 }
             }
@@ -1014,48 +1509,55 @@ class Compiler
     /**
      * Compile child; returns a value to halt execution
      *
-     * @param array     $child
-     * @param \stdClass $out
+     * @param array                                $child
+     * @param \Leafo\ScssPhp\Formatter\OutputBlock $out
      *
      * @return array
      */
-    protected function compileChild($child, $out)
+    protected function compileChild($child, OutputBlock $out)
     {
-        $this->sourcePos = isset($child[Parser::SOURCE_POSITION]) ? $child[Parser::SOURCE_POSITION] : -1;
-        $this->sourceParser = isset($child[Parser::SOURCE_PARSER]) ? $child[Parser::SOURCE_PARSER] : $this->parser;
+        $this->sourceIndex  = isset($child[Parser::SOURCE_INDEX]) ? $child[Parser::SOURCE_INDEX] : null;
+        $this->sourceLine   = isset($child[Parser::SOURCE_LINE]) ? $child[Parser::SOURCE_LINE] : -1;
+        $this->sourceColumn = isset($child[Parser::SOURCE_COLUMN]) ? $child[Parser::SOURCE_COLUMN] : -1;
 
         switch ($child[0]) {
-            case 'import':
+            case Type::T_SCSSPHP_IMPORT_ONCE:
                 list(, $rawPath) = $child;
 
                 $rawPath = $this->reduce($rawPath);
 
-                if (! $this->compileImport($rawPath, $out)) {
+                if (! $this->compileImport($rawPath, $out, true)) {
                     $out->lines[] = '@import ' . $this->compileValue($rawPath) . ';';
                 }
                 break;
 
-            case 'directive':
-                list(, $directive) = $child;
+            case Type::T_IMPORT:
+                list(, $rawPath) = $child;
 
-                $s = '@' . $directive->name;
+                $rawPath = $this->reduce($rawPath);
 
-                if (! empty($directive->value)) {
-                    $s .= ' ' . $this->compileValue($directive->value);
+                if (! $this->compileImport($rawPath, $out)) {
+                    $out->lines[] = '@import ' . $this->compileValue($rawPath) . ';';
                 }
+                break;
+
+            case Type::T_DIRECTIVE:
+                $this->compileDirective($child[1]);
+                break;
 
-                $this->compileNestedBlock($directive, array($s));
+            case Type::T_AT_ROOT:
+                $this->compileAtRoot($child[1]);
                 break;
 
-            case 'media':
+            case Type::T_MEDIA:
                 $this->compileMedia($child[1]);
                 break;
 
-            case 'block':
+            case Type::T_BLOCK:
                 $this->compileBlock($child[1]);
                 break;
 
-            case 'charset':
+            case Type::T_CHARSET:
                 if (! $this->charsetSeen) {
                     $this->charsetSeen = true;
 
@@ -1063,13 +1565,13 @@ class Compiler
                 }
                 break;
 
-            case 'assign':
+            case Type::T_ASSIGN:
                 list(, $name, $value) = $child;
 
-                if ($name[0] === 'var') {
-                    $flag = isset($child[3]) ? $child[3] : null;
-                    $isDefault = $flag === '!default';
-                    $isGlobal = $flag === '!global';
+                if ($name[0] === Type::T_VARIABLE) {
+                    $flags = isset($child[3]) ? $child[3] : [];
+                    $isDefault = in_array('!default', $flags);
+                    $isGlobal = in_array('!global', $flags);
 
                     if ($isGlobal) {
                         $this->set($name[1], $this->reduce($value), false, $this->rootEnv);
@@ -1090,11 +1592,11 @@ class Compiler
 
                 // handle shorthand syntax: size / line-height
                 if ($compiledName === 'font') {
-                    if ($value[0] === 'exp' && $value[1] === '/') {
+                    if ($value[0] === Type::T_EXPRESSION && $value[1] === '/') {
                         $value = $this->expToString($value);
-                    } elseif ($value[0] === 'list') {
+                    } elseif ($value[0] === Type::T_LIST) {
                         foreach ($value[2] as &$item) {
-                            if ($item[0] === 'exp' && $item[1] === '/') {
+                            if ($item[0] === Type::T_EXPRESSION && $item[1] === '/') {
                                 $item = $this->expToString($item);
                             }
                         }
@@ -1103,10 +1605,10 @@ class Compiler
 
                 // if the value reduces to null from something else then
                 // the property should be discarded
-                if ($value[0] !== 'null') {
+                if ($value[0] !== Type::T_NULL) {
                     $value = $this->reduce($value);
 
-                    if ($value[0] === 'null') {
+                    if ($value[0] === Type::T_NULL || $value === self::$nullString) {
                         break;
                     }
                 }
@@ -1119,8 +1621,8 @@ class Compiler
                 );
                 break;
 
-            case 'comment':
-                if ($out->type === 'root') {
+            case Type::T_COMMENT:
+                if ($out->type === Type::T_ROOT) {
                     $this->compileComment($child);
                     break;
                 }
@@ -1128,26 +1630,29 @@ class Compiler
                 $out->lines[] = $child[1];
                 break;
 
-            case 'mixin':
-            case 'function':
+            case Type::T_MIXIN:
+            case Type::T_FUNCTION:
                 list(, $block) = $child;
 
                 $this->set(self::$namespaces[$block->type] . $block->name, $block);
                 break;
 
-            case 'extend':
+            case Type::T_EXTEND:
                 list(, $selectors) = $child;
 
                 foreach ($selectors as $sel) {
-                    // only use the first one
-                    $result = $this->evalSelectors(array($sel));
-                    $result = current($result[0]);
+                    $results = $this->evalSelectors([$sel]);
 
-                    $this->pushExtends($result, $out->selectors);
+                    foreach ($results as $result) {
+                        // only use the first one
+                        $result = current($result);
+
+                        $this->pushExtends($result, $out->selectors, $child);
+                    }
                 }
                 break;
 
-            case 'if':
+            case Type::T_IF:
                 list(, $if) = $child;
 
                 if ($this->isTruthy($this->reduce($if->cond, true))) {
@@ -1155,18 +1660,15 @@ class Compiler
                 }
 
                 foreach ($if->cases as $case) {
-                    if ($case->type === 'else' ||
-                        $case->type === 'elseif' && $this->isTruthy($this->reduce($case->cond))
+                    if ($case->type === Type::T_ELSE ||
+                        $case->type === Type::T_ELSEIF && $this->isTruthy($this->reduce($case->cond))
                     ) {
                         return $this->compileChildren($case->children, $out);
                     }
                 }
                 break;
 
-            case 'return':
-                return $this->reduce($child[1], true);
-
-            case 'each':
+            case Type::T_EACH:
                 list(, $each) = $child;
 
                 $list = $this->coerceList($this->reduce($each->list));
@@ -1187,28 +1689,40 @@ class Compiler
                     $ret = $this->compileChildren($each->children, $out);
 
                     if ($ret) {
-                        $this->popEnv();
+                        if ($ret[0] !== Type::T_CONTROL) {
+                            $this->popEnv();
+
+                            return $ret;
+                        }
 
-                        return $ret;
+                        if ($ret[1]) {
+                            break;
+                        }
                     }
                 }
 
                 $this->popEnv();
                 break;
 
-            case 'while':
+            case Type::T_WHILE:
                 list(, $while) = $child;
 
                 while ($this->isTruthy($this->reduce($while->cond, true))) {
                     $ret = $this->compileChildren($while->children, $out);
 
                     if ($ret) {
-                        return $ret;
+                        if ($ret[0] !== Type::T_CONTROL) {
+                            return $ret;
+                        }
+
+                        if ($ret[1]) {
+                            break;
+                        }
                     }
                 }
                 break;
 
-            case 'for':
+            case Type::T_FOR:
                 list(, $for) = $child;
 
                 $start = $this->reduce($for->start, true);
@@ -1217,46 +1731,63 @@ class Compiler
                 $end = $end[1];
                 $d = $start < $end ? 1 : -1;
 
-                while (true) {
+                for (;;) {
                     if ((! $for->until && $start - $d == $end) ||
                         ($for->until && $start == $end)
                     ) {
                         break;
                     }
 
-                    $this->set($for->var, array('number', $start, ''));
+                    $this->set($for->var, new Node\Number($start, ''));
                     $start += $d;
 
                     $ret = $this->compileChildren($for->children, $out);
 
                     if ($ret) {
-                        return $ret;
+                        if ($ret[0] !== Type::T_CONTROL) {
+                            return $ret;
+                        }
+
+                        if ($ret[1]) {
+                            break;
+                        }
                     }
                 }
                 break;
 
-            case 'nestedprop':
+            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);
+
+            case Type::T_NESTED_PROPERTY:
                 list(, $prop) = $child;
 
-                $prefixed = array();
+                $prefixed = [];
                 $prefix = $this->compileValue($prop->prefix) . '-';
 
                 foreach ($prop->children as $child) {
-                    if ($child[0] === 'assign') {
-                        array_unshift($child[1][2], $prefix);
-                    }
-
-                    if ($child[0] === 'nestedprop') {
-                        array_unshift($child[1]->prefix[2], $prefix);
+                    switch ($child[0]) {
+                        case Type::T_ASSIGN:
+                            array_unshift($child[1][2], $prefix);
+                            break;
+
+                        case Type::T_NESTED_PROPERTY:
+                            array_unshift($child[1]->prefix[2], $prefix);
+                            break;
                     }
 
                     $prefixed[] = $child;
                 }
 
-                $this->compileChildren($prefixed, $out);
+                $this->compileChildrenNoReturn($prefixed, $out);
                 break;
 
-            case 'include':
+            case Type::T_INCLUDE:
                 // including a mixin
                 list(, $name, $argValues, $content) = $child;
 
@@ -1264,18 +1795,22 @@ class Compiler
 
                 if (! $mixin) {
                     $this->throwError("Undefined mixin $name");
+                    break;
                 }
 
-                $callingScope = $this->env;
+                $callingScope = $this->getStoreEnv();
 
                 // push scope, apply args
                 $this->pushEnv();
                 $this->env->depth--;
 
+                $storeEnv = $this->storeEnv;
+                $this->storeEnv = $this->env;
+
                 if (isset($content)) {
                     $content->scope = $callingScope;
 
-                    $this->setRaw(self::$namespaces['special'] . 'content', $content);
+                    $this->setRaw(self::$namespaces['special'] . 'content', $content, $this->env);
                 }
 
                 if (isset($mixin->args)) {
@@ -1284,58 +1819,60 @@ class Compiler
 
                 $this->env->marker = 'mixin';
 
-                foreach ($mixin->children as $child) {
-                    $this->compileChild($child, $out);
-                }
+                $this->compileChildrenNoReturn($mixin->children, $out);
+
+                $this->storeEnv = $storeEnv;
 
                 $this->popEnv();
                 break;
 
-            case 'mixin_content':
-                $content = $this->get(self::$namespaces['special'] . 'content', false);
+            case Type::T_MIXIN_CONTENT:
+                $content = $this->get(self::$namespaces['special'] . 'content', false, $this->getStoreEnv())
+                         ?: $this->get(self::$namespaces['special'] . 'content', false, $this->env);
 
                 if (! $content) {
-                    $this->throwError('Expected @content inside of mixin');
-                }
-
-                if (! isset($content->children)) {
+                    $content = new \stdClass();
+                    $content->scope = new \stdClass();
+                    $content->children = $this->storeEnv->parent->block->children;
                     break;
                 }
 
+                $storeEnv = $this->storeEnv;
                 $this->storeEnv = $content->scope;
 
-                foreach ($content->children as $child) {
-                    $this->compileChild($child, $out);
-                }
-
-                $this->storeEnv = null;
+                $this->compileChildrenNoReturn($content->children, $out);
 
+                $this->storeEnv = $storeEnv;
                 break;
 
-            case 'debug':
+            case Type::T_DEBUG:
                 list(, $value) = $child;
 
-                $line = $this->parser->getLineNo($this->sourcePos);
+                $line = $this->sourceLine;
                 $value = $this->compileValue($this->reduce($value, true));
                 fwrite($this->stderr, "Line $line DEBUG: $value\n");
                 break;
 
-            case 'warn':
+            case Type::T_WARN:
                 list(, $value) = $child;
 
-                $line = $this->parser->getLineNo($this->sourcePos);
+                $line = $this->sourceLine;
                 $value = $this->compileValue($this->reduce($value, true));
-                echo "Line $line WARN: $value\n";
+                fwrite($this->stderr, "Line $line WARN: $value\n");
                 break;
 
-            case 'error':
+            case Type::T_ERROR:
                 list(, $value) = $child;
 
-                $line = $this->parser->getLineNo($this->sourcePos);
+                $line = $this->sourceLine;
                 $value = $this->compileValue($this->reduce($value, true));
                 $this->throwError("Line $line ERROR: $value\n");
                 break;
 
+            case Type::T_CONTROL:
+                $this->throwError('@break/@continue not permitted in this scope');
+                break;
+
             default:
                 $this->throwError("unknown child type: $child[0]");
         }
@@ -1350,9 +1887,9 @@ class Compiler
      */
     protected function expToString($exp)
     {
-        list(, $op, $left, $right, $inParens, $whiteLeft, $whiteRight) = $exp;
+        list(, $op, $left, $right, /* $inParens */, $whiteLeft, $whiteRight) = $exp;
 
-        $content = array($this->reduce($left));
+        $content = [$this->reduce($left)];
 
         if ($whiteLeft) {
             $content[] = ' ';
@@ -1366,7 +1903,7 @@ class Compiler
 
         $content[] = $this->reduce($right);
 
-        return array('string', '', $content);
+        return [Type::T_STRING, '', $content];
     }
 
     /**
@@ -1381,6 +1918,18 @@ class Compiler
         return $value !== self::$false && $value !== self::$null;
     }
 
+    /**
+     * Is the value a direct relationship combinator?
+     *
+     * @param string $value
+     *
+     * @return bool
+     */
+    protected function isImmediateRelationshipCombinator($value)
+    {
+        return $value === '>' || $value === '+' || $value === '~';
+    }
+
     /**
      * Should $value cause its operand to eval
      *
@@ -1391,14 +1940,14 @@ class Compiler
     protected function shouldEval($value)
     {
         switch ($value[0]) {
-            case 'exp':
+            case Type::T_EXPRESSION:
                 if ($value[1] === '/') {
                     return $this->shouldEval($value[2], $value[3]);
                 }
 
                 // fall-thru
-            case 'var':
-            case 'fncall':
+            case Type::T_VARIABLE:
+            case Type::T_FUNCTION_CALL:
                 return true;
         }
 
@@ -1418,17 +1967,23 @@ class Compiler
         list($type) = $value;
 
         switch ($type) {
-            case 'exp':
+            case Type::T_EXPRESSION:
                 list(, $op, $left, $right, $inParens) = $value;
 
                 $opName = isset(self::$operatorNames[$op]) ? self::$operatorNames[$op] : $op;
                 $inExp = $inExp || $this->shouldEval($left) || $this->shouldEval($right);
 
                 $left = $this->reduce($left, true);
-                $right = $this->reduce($right, true);
 
-                // special case: looks like css short-hand
-                if ($opName === 'div' && ! $inParens && ! $inExp && isset($right[2]) && $right[2] !== '') {
+                if ($op !== 'and' && $op !== 'or') {
+                    $right = $this->reduce($right, true);
+                }
+
+                // special case: looks like css shorthand
+                if ($opName == 'div' && ! $inParens && ! $inExp && isset($right[2])
+                    && (($right[0] !== Type::T_NUMBER && $right[2] != '')
+                    || ($right[0] === Type::T_NUMBER && ! $right->unitless()))
+                ) {
                     return $this->expToString($value);
                 }
 
@@ -1448,47 +2003,49 @@ class Compiler
                 // 3. op[op name]
                 $fn = "op${ucOpName}${ucLType}${ucRType}";
 
-                if (is_callable(array($this, $fn)) ||
+                if (is_callable([$this, $fn]) ||
                     (($fn = "op${ucLType}${ucRType}") &&
-                        is_callable(array($this, $fn)) &&
+                        is_callable([$this, $fn]) &&
                         $passOp = true) ||
                     (($fn = "op${ucOpName}") &&
-                        is_callable(array($this, $fn)) &&
+                        is_callable([$this, $fn]) &&
                         $genOp = true)
                 ) {
-                    $unitChange = false;
+                    $coerceUnit = false;
 
                     if (! isset($genOp) &&
-                        $left[0] === 'number' && $right[0] === 'number'
+                        $left[0] === Type::T_NUMBER && $right[0] === Type::T_NUMBER
                     ) {
-                        if ($opName === 'mod' && $right[2] !== '') {
-                            $this->throwError("Cannot modulo by a number with units: $right[1]$right[2].");
-                        }
+                        $coerceUnit = true;
 
-                        $unitChange = true;
-                        $emptyUnit = $left[2] === '' || $right[2] === '';
-                        $targetUnit = '' !== $left[2] ? $left[2] : $right[2];
+                        switch ($opName) {
+                            case 'mul':
+                                $targetUnit = $left[2];
 
-                        if ($opName !== 'mul') {
-                            $left[2] = '' !== $left[2] ? $left[2] : $targetUnit;
-                            $right[2] = '' !== $right[2] ? $right[2] : $targetUnit;
-                        }
+                                foreach ($right[2] as $unit => $exp) {
+                                    $targetUnit[$unit] = (isset($targetUnit[$unit]) ? $targetUnit[$unit] : 0) + $exp;
+                                }
+                                break;
 
-                        if ($opName !== 'mod') {
-                            $left = $this->normalizeNumber($left);
-                            $right = $this->normalizeNumber($right);
-                        }
+                            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;
 
-                        if ($opName === 'div' && ! $emptyUnit && $left[2] === $right[2]) {
-                            $targetUnit = '';
+                            default:
+                                $targetUnit = $left->unitless() ? $right[2] : $left[2];
                         }
 
-                        if ($opName === 'mul') {
-                            $left[2] = '' !== $left[2] ? $left[2] : $right[2];
-                            $right[2] = '' !== $right[2] ? $right[2] : $left[2];
-                        } elseif ($opName === 'div' && $left[2] === $right[2]) {
-                            $left[2] = '';
-                            $right[2] = '';
+                        if (! $left->unitless() && ! $right->unitless()) {
+                            $left = $left->normalize();
+                            $right = $right->normalize();
                         }
                     }
 
@@ -1501,8 +2058,8 @@ class Compiler
                     }
 
                     if (isset($out)) {
-                        if ($unitChange && $out[0] === 'number') {
-                            $out = $this->coerceUnit($out, $targetUnit);
+                        if ($coerceUnit && $out[0] === Type::T_NUMBER) {
+                            $out = $out->coerce($targetUnit);
                         }
 
                         return $out;
@@ -1511,27 +2068,25 @@ class Compiler
 
                 return $this->expToString($value);
 
-            case 'unary':
+            case Type::T_UNARY:
                 list(, $op, $exp, $inParens) = $value;
 
                 $inExp = $inExp || $this->shouldEval($exp);
                 $exp = $this->reduce($exp);
 
-                if ($exp[0] === 'number') {
+                if ($exp[0] === Type::T_NUMBER) {
                     switch ($op) {
                         case '+':
-                            return $exp;
+                            return new Node\Number($exp[1], $exp[2]);
 
                         case '-':
-                            $exp[1] *= -1;
-
-                            return $exp;
+                            return new Node\Number(-$exp[1], $exp[2]);
                     }
                 }
 
                 if ($op === 'not') {
                     if ($inExp || $inParens) {
-                        if ($exp === self::$false) {
+                        if ($exp === self::$false || $exp === self::$null) {
                             return self::$true;
                         }
 
@@ -1541,21 +2096,21 @@ class Compiler
                     $op = $op . ' ';
                 }
 
-                return array('string', '', array($op, $exp));
+                return [Type::T_STRING, '', [$op, $exp]];
 
-            case 'var':
+            case Type::T_VARIABLE:
                 list(, $name) = $value;
 
                 return $this->reduce($this->get($name));
 
-            case 'list':
+            case Type::T_LIST:
                 foreach ($value[2] as &$item) {
                     $item = $this->reduce($item);
                 }
 
                 return $value;
 
-            case 'map':
+            case Type::T_MAP:
                 foreach ($value[1] as &$item) {
                     $item = $this->reduce($item);
                 }
@@ -1566,66 +2121,60 @@ class Compiler
 
                 return $value;
 
-            case 'string':
+            case Type::T_STRING:
                 foreach ($value[2] as &$item) {
-                    if (is_array($item)) {
+                    if (is_array($item) || $item instanceof \ArrayAccess) {
                         $item = $this->reduce($item);
                     }
                 }
 
                 return $value;
 
-            case 'interpolate':
+            case Type::T_INTERPOLATE:
                 $value[1] = $this->reduce($value[1]);
 
                 return $value;
 
-            case 'fncall':
+            case Type::T_FUNCTION_CALL:
                 list(, $name, $argValues) = $value;
 
-                // user defined function?
-                $func = $this->get(self::$namespaces['function'] . $name, false);
-
-                if ($func) {
-                    $this->pushEnv();
-
-                    // set the args
-                    if (isset($func->args)) {
-                        $this->applyArguments($func->args, $argValues);
-                    }
-
-                    // throw away lines and children
-                    $tmp = (object)array(
-                        'lines' => array(),
-                        'children' => array()
-                    );
+                return $this->fncall($name, $argValues);
 
-                    $ret = $this->compileChildren($func->children, $tmp);
-
-                    $this->popEnv();
-
-                    return ! isset($ret) ? self::$defaultValue : $ret;
-                }
-
-                // built in function
-                if ($this->callBuiltin($name, $argValues, $returnValue)) {
-                    return $returnValue;
-                }
+            default:
+                return $value;
+        }
+    }
 
-                // need to flatten the arguments into a list
-                $listArgs = array();
+    /**
+     * Function caller
+     *
+     * @param string $name
+     * @param array  $argValues
+     *
+     * @return array|null
+     */
+    private function fncall($name, $argValues)
+    {
+        // SCSS @function
+        if ($this->callScssFunction($name, $argValues, $returnValue)) {
+            return $returnValue;
+        }
 
-                foreach ((array)$argValues as $arg) {
-                    if (empty($arg[0])) {
-                        $listArgs[] = $this->reduce($arg[1]);
-                    }
-                }
+        // native PHP functions
+        if ($this->callNativeFunction($name, $argValues, $returnValue)) {
+            return $returnValue;
+        }
 
-                return array('function', $name, array('list', ',', $listArgs));
+        // for CSS functions, simply flatten the arguments into a list
+        $listArgs = [];
 
-            default:
-                return $value;
+        foreach ((array) $argValues as $arg) {
+            if (empty($arg[0])) {
+                $listArgs[] = $this->reduce($arg[1]);
+            }
         }
+
+        return [Type::T_FUNCTION, $name, [Type::T_LIST, ',', $listArgs]];
     }
 
     /**
@@ -1653,11 +2202,11 @@ class Compiler
         list($type) = $value;
 
         switch ($type) {
-            case 'list':
+            case Type::T_LIST:
                 $value = $this->extractInterpolation($value);
 
-                if ($value[0] !== 'list') {
-                    return array('keyword', $this->compileValue($value));
+                if ($value[0] !== Type::T_LIST) {
+                    return [Type::T_KEYWORD, $this->compileValue($value)];
                 }
 
                 foreach ($value[2] as $key => $item) {
@@ -1666,37 +2215,20 @@ class Compiler
 
                 return $value;
 
-            case 'string':
-                return array($type, '"', $this->compileStringContent($value));
+            case Type::T_STRING:
+                return [$type, '"', [$this->compileStringContent($value)]];
+
+            case Type::T_NUMBER:
+                return $value->normalize();
 
-            case 'number':
-                return $this->normalizeNumber($value);
+            case Type::T_INTERPOLATE:
+                return [Type::T_KEYWORD, $this->compileValue($value)];
 
             default:
                 return $value;
         }
     }
 
-    /**
-     * Normalize number; just does physical lengths for now
-     *
-     * @param array $number
-     *
-     * @return array
-     */
-    protected function normalizeNumber($number)
-    {
-        list(, $value, $unit) = $number;
-
-        if (isset(self::$unitTable['in'][$unit])) {
-            $conv = self::$unitTable['in'][$unit];
-
-            return array('number', $value / $conv, 'in');
-        }
-
-        return $number;
-    }
-
     /**
      * Add numbers
      *
@@ -1707,7 +2239,7 @@ class Compiler
      */
     protected function opAddNumberNumber($left, $right)
     {
-        return array('number', $left[1] + $right[1], $left[2]);
+        return new Node\Number($left[1] + $right[1], $left[2]);
     }
 
     /**
@@ -1720,7 +2252,7 @@ class Compiler
      */
     protected function opMulNumberNumber($left, $right)
     {
-        return array('number', $left[1] * $right[1], $left[2]);
+        return new Node\Number($left[1] * $right[1], $left[2]);
     }
 
     /**
@@ -1733,7 +2265,7 @@ class Compiler
      */
     protected function opSubNumberNumber($left, $right)
     {
-        return array('number', $left[1] - $right[1], $left[2]);
+        return new Node\Number($left[1] - $right[1], $left[2]);
     }
 
     /**
@@ -1747,10 +2279,10 @@ class Compiler
     protected function opDivNumberNumber($left, $right)
     {
         if ($right[1] == 0) {
-            $this->throwError('Division by zero');
+            return [Type::T_STRING, '', [$left[1] . $left[2] . '/' . $right[1] . $right[2]]];
         }
 
-        return array('number', $left[1] / $right[1], $left[2]);
+        return new Node\Number($left[1] / $right[1], $left[2]);
     }
 
     /**
@@ -1763,7 +2295,7 @@ class Compiler
      */
     protected function opModNumberNumber($left, $right)
     {
-        return array('number', $left[1] % $right[1], $left[2]);
+        return new Node\Number($left[1] % $right[1], $left[2]);
     }
 
     /**
@@ -1777,7 +2309,7 @@ class Compiler
     protected function opAdd($left, $right)
     {
         if ($strLeft = $this->coerceString($left)) {
-            if ($right[0] === 'string') {
+            if ($right[0] === Type::T_STRING) {
                 $right[1] = '';
             }
 
@@ -1787,7 +2319,7 @@ class Compiler
         }
 
         if ($strRight = $this->coerceString($right)) {
-            if ($left[0] === 'string') {
+            if ($left[0] === Type::T_STRING) {
                 $left[1] = '';
             }
 
@@ -1812,8 +2344,8 @@ class Compiler
             return;
         }
 
-        if ($left !== self::$false) {
-            return $right;
+        if ($left !== self::$false and $left !== self::$null) {
+            return $this->reduce($right, true);
         }
 
         return $left;
@@ -1834,11 +2366,11 @@ class Compiler
             return;
         }
 
-        if ($left !== self::$false) {
+        if ($left !== self::$false and $left !== self::$null) {
             return $left;
         }
 
-        return $right;
+        return $this->reduce($right, true);
     }
 
     /**
@@ -1852,9 +2384,9 @@ class Compiler
      */
     protected function opColorColor($op, $left, $right)
     {
-        $out = array('color');
+        $out = [Type::T_COLOR];
 
-        foreach (range(1, 3) as $i) {
+        foreach ([1, 2, 3] as $i) {
             $lval = isset($left[$i]) ? $left[$i] : 0;
             $rval = isset($right[$i]) ? $right[$i] : 0;
 
@@ -1878,6 +2410,7 @@ class Compiler
                 case '/':
                     if ($rval == 0) {
                         $this->throwError("color: Can't divide by zero");
+                        break 2;
                     }
 
                     $out[] = (int) ($lval / $rval);
@@ -1891,6 +2424,7 @@ class Compiler
 
                 default:
                     $this->throwError("color: unknown op $op");
+                    break 2;
             }
         }
 
@@ -1919,7 +2453,7 @@ class Compiler
         return $this->opColorColor(
             $op,
             $left,
-            array('color', $value, $value, $value)
+            [Type::T_COLOR, $value, $value, $value]
         );
     }
 
@@ -1938,7 +2472,7 @@ class Compiler
 
         return $this->opColorColor(
             $op,
-            array('color', $value, $value, $value),
+            [Type::T_COLOR, $value, $value, $value],
             $right
         );
     }
@@ -2037,6 +2571,21 @@ class Compiler
         return $this->toBool($left[1] < $right[1]);
     }
 
+    /**
+     * Three-way comparison, aka spaceship operator
+     *
+     * @param array $left
+     * @param array $right
+     *
+     * @return array
+     */
+    protected function opCmpNumberNumber($left, $right)
+    {
+        $n = $left[1] - $right[1];
+
+        return new Node\Number($n ? $n / abs($n) : 0, '');
+    }
+
     /**
      * Cast to boolean
      *
@@ -2075,10 +2624,10 @@ class Compiler
         list($type) = $value;
 
         switch ($type) {
-            case 'keyword':
+            case Type::T_KEYWORD:
                 return $value[1];
 
-            case 'color':
+            case Type::T_COLOR:
                 // [1] - red component (either number for a %)
                 // [2] - green component
                 // [3] - blue component
@@ -2102,42 +2651,46 @@ class Compiler
 
                 return $h;
 
-            case 'number':
-                return round($value[1], $this->numberPrecision) . $value[2];
+            case Type::T_NUMBER:
+                return $value->output($this);
 
-            case 'string':
+            case Type::T_STRING:
                 return $value[1] . $this->compileStringContent($value) . $value[1];
 
-            case 'function':
+            case Type::T_FUNCTION:
                 $args = ! empty($value[2]) ? $this->compileValue($value[2]) : '';
 
                 return "$value[1]($args)";
 
-            case 'list':
+            case Type::T_LIST:
                 $value = $this->extractInterpolation($value);
 
-                if ($value[0] !== 'list') {
+                if ($value[0] !== Type::T_LIST) {
                     return $this->compileValue($value);
                 }
 
                 list(, $delim, $items) = $value;
 
-                $filtered = array();
+                if ($delim !== ' ') {
+                    $delim .= ' ';
+                }
+
+                $filtered = [];
 
                 foreach ($items as $item) {
-                    if ($item[0] === 'null') {
+                    if ($item[0] === Type::T_NULL) {
                         continue;
                     }
 
                     $filtered[] = $this->compileValue($item);
                 }
 
-                return implode("$delim ", $filtered);
+                return implode("$delim", $filtered);
 
-            case 'map':
+            case Type::T_MAP:
                 $keys = $value[1];
                 $values = $value[2];
-                $filtered = array();
+                $filtered = [];
 
                 for ($i = 0, $s = count($keys); $i < $s; $i++) {
                     $filtered[$this->compileValue($keys[$i])] = $this->compileValue($values[$i]);
@@ -2149,7 +2702,7 @@ class Compiler
 
                 return '(' . implode(', ', $filtered) . ')';
 
-            case 'interpolated':
+            case Type::T_INTERPOLATED:
                 // node created by extractInterpolation
                 list(, $interpolate, $left, $right) = $value;
                 list(,, $whiteLeft, $whiteRight) = $interpolate;
@@ -2162,24 +2715,25 @@ class Compiler
 
                 return $left . $this->compileValue($interpolate) . $right;
 
-            case 'interpolate':
+            case Type::T_INTERPOLATE:
                 // raw parse node
                 list(, $exp) = $value;
 
                 // strip quotes if it's a string
                 $reduced = $this->reduce($exp);
+
                 switch ($reduced[0]) {
-                    case 'string':
-                        $reduced = array('keyword', $this->compileStringContent($reduced));
+                    case Type::T_STRING:
+                        $reduced = [Type::T_KEYWORD, $this->compileStringContent($reduced)];
                         break;
 
-                    case 'null':
-                        $reduced = array('keyword', '');
+                    case Type::T_NULL:
+                        $reduced = [Type::T_KEYWORD, ''];
                 }
 
                 return $this->compileValue($reduced);
 
-            case 'null':
+            case Type::T_NULL:
                 return 'null';
 
             default:
@@ -2208,10 +2762,10 @@ class Compiler
      */
     protected function compileStringContent($string)
     {
-        $parts = array();
+        $parts = [];
 
         foreach ($string[2] as $part) {
-            if (is_array($part)) {
+            if (is_array($part) || $part instanceof \ArrayAccess) {
                 $parts[] = $this->compileValue($part);
             } else {
                 $parts[] = $part;
@@ -2233,11 +2787,11 @@ class Compiler
         $items = $list[2];
 
         foreach ($items as $i => $item) {
-            if ($item[0] === 'interpolate') {
-                $before = array('list', $list[1], array_slice($items, 0, $i));
-                $after = array('list', $list[1], array_slice($items, $i + 1));
+            if ($item[0] === Type::T_INTERPOLATE) {
+                $before = [Type::T_LIST, $list[1], array_slice($items, 0, $i)];
+                $after  = [Type::T_LIST, $list[1], array_slice($items, $i + 1)];
 
-                return array('interpolated', $item, $before, $after);
+                return [Type::T_INTERPOLATED, $item, $before, $after];
             }
         }
 
@@ -2247,27 +2801,22 @@ class Compiler
     /**
      * Find the final set of selectors
      *
-     * @param \stdClass $env
+     * @param \Leafo\ScssPhp\Compiler\Environment $env
      *
      * @return array
      */
-    protected function multiplySelectors($env)
+    protected function multiplySelectors(Environment $env)
     {
-        $envs = array();
+        $envs            = $this->compactEnv($env);
+        $selectors       = [];
+        $parentSelectors = [[]];
 
-        while (null !== $env) {
-            if (empty($env->selectors)) {
-                $envs[] = $env;
+        while ($env = array_pop($envs)) {
+            if (empty($env->selectors)) {
+                continue;
             }
 
-            $env = $env->parent;
-        };
-
-        $selectors = array();
-        $parentSelectors = array(array());
-
-        while ($env = array_pop($envs)) {
-            $selectors = array();
+            $selectors = [];
 
             foreach ($env->selectors as $selector) {
                 foreach ($parentSelectors as $parent) {
@@ -2292,10 +2841,10 @@ class Compiler
     protected function joinSelectors($parent, $child)
     {
         $setSelf = false;
-        $out = array();
+        $out = [];
 
         foreach ($child as $part) {
-            $newPart = array();
+            $newPart = [];
 
             foreach ($part as $p) {
                 if ($p === self::$selfSelector) {
@@ -2304,7 +2853,7 @@ class Compiler
                     foreach ($parent as $i => $parentPart) {
                         if ($i > 0) {
                             $out[] = $newPart;
-                            $newPart = array();
+                            $newPart = [];
                         }
 
                         foreach ($parentPart as $pp) {
@@ -2325,15 +2874,15 @@ class Compiler
     /**
      * Multiply media
      *
-     * @param \stdClass $env
-     * @param array     $childQueries
+     * @param \Leafo\ScssPhp\Compiler\Environment $env
+     * @param array                               $childQueries
      *
      * @return array
      */
-    protected function multiplyMedia($env, $childQueries = null)
+    protected function multiplyMedia(Environment $env = null, $childQueries = null)
     {
         if (! isset($env) ||
-            ! empty($env->block->type) && $env->block->type !== 'media'
+            ! empty($env->block->type) && $env->block->type !== Type::T_MEDIA
         ) {
             return $childQueries;
         }
@@ -2343,12 +2892,15 @@ class Compiler
             return $this->multiplyMedia($env->parent, $childQueries);
         }
 
-        $parentQueries = $env->block->queryList;
+        $parentQueries = isset($env->block->queryList)
+            ? $env->block->queryList
+            : [[[Type::T_MEDIA_VALUE, $env->block->value]]];
+
         if ($childQueries === null) {
             $childQueries = $parentQueries;
         } else {
             $originalQueries = $childQueries;
-            $childQueries = array();
+            $childQueries = [];
 
             foreach ($parentQueries as $parentQuery) {
                 foreach ($originalQueries as $childQuery) {
@@ -2360,20 +2912,53 @@ class Compiler
         return $this->multiplyMedia($env->parent, $childQueries);
     }
 
+    /**
+     * Convert env linked list to stack
+     *
+     * @param \Leafo\ScssPhp\Compiler\Environment $env
+     *
+     * @return array
+     */
+    private function compactEnv(Environment $env)
+    {
+        for ($envs = []; $env; $env = $env->parent) {
+            $envs[] = $env;
+        }
+
+        return $envs;
+    }
+
+    /**
+     * Convert env stack to singly linked list
+     *
+     * @param array $envs
+     *
+     * @return \Leafo\ScssPhp\Compiler\Environment
+     */
+    private function extractEnv($envs)
+    {
+        for ($env = null; $e = array_pop($envs);) {
+            $e->parent = $env;
+            $env = $e;
+        }
+
+        return $env;
+    }
+
     /**
      * Push environment
      *
-     * @param \stdClass $block
+     * @param \Leafo\ScssPhp\Block $block
      *
-     * @return \stdClass
+     * @return \Leafo\ScssPhp\Compiler\Environment
      */
-    protected function pushEnv($block = null)
+    protected function pushEnv(Block $block = null)
     {
-        $env = new \stdClass;
+        $env = new Environment;
         $env->parent = $this->env;
-        $env->store = array();
-        $env->block = $block;
-        $env->depth = isset($this->env->depth) ? $this->env->depth + 1 : 0;
+        $env->store  = [];
+        $env->block  = $block;
+        $env->depth  = isset($this->env->depth) ? $this->env->depth + 1 : 0;
 
         $this->env = $env;
 
@@ -2385,16 +2970,13 @@ class Compiler
      */
     protected function popEnv()
     {
-        $env = $this->env;
         $this->env = $this->env->parent;
-
-        return $env;
     }
 
     /**
      * Get store environment
      *
-     * @return \stdClass
+     * @return \Leafo\ScssPhp\Compiler\Environment
      */
     protected function getStoreEnv()
     {
@@ -2404,15 +2986,19 @@ class Compiler
     /**
      * Set variable
      *
-     * @param string    $name
-     * @param mixed     $value
-     * @param boolean   $shadow
-     * @param \stdClass $env
+     * @param string                              $name
+     * @param mixed                               $value
+     * @param boolean                             $shadow
+     * @param \Leafo\ScssPhp\Compiler\Environment $env
      */
-    protected function set($name, $value, $shadow = false, $env = null)
+    protected function set($name, $value, $shadow = false, Environment $env = null)
     {
         $name = $this->normalizeName($name);
 
+        if (! isset($env)) {
+            $env = $this->getStoreEnv();
+        }
+
         if ($shadow) {
             $this->setRaw($name, $value, $env);
         } else {
@@ -2423,16 +3009,12 @@ class Compiler
     /**
      * Set existing variable
      *
-     * @param string    $name
-     * @param mixed     $value
-     * @param \stdClass $env
+     * @param string                              $name
+     * @param mixed                               $value
+     * @param \Leafo\ScssPhp\Compiler\Environment $env
      */
-    protected function setExisting($name, $value, $env = null)
+    protected function setExisting($name, $value, Environment $env)
     {
-        if (! isset($env)) {
-            $env = $this->getStoreEnv();
-        }
-
         $storeEnv = $env;
 
         $hasNamespace = $name[0] === '^' || $name[0] === '@' || $name[0] === '%';
@@ -2461,16 +3043,12 @@ class Compiler
     /**
      * Set raw variable
      *
-     * @param string    $name
-     * @param mixed     $value
-     * @param \stdClass $env
+     * @param string                              $name
+     * @param mixed                               $value
+     * @param \Leafo\ScssPhp\Compiler\Environment $env
      */
-    protected function setRaw($name, $value, $env = null)
+    protected function setRaw($name, $value, Environment $env)
     {
-        if (! isset($env)) {
-            $env = $this->getStoreEnv();
-        }
-
         $env->store[$name] = $value;
     }
 
@@ -2479,28 +3057,36 @@ class Compiler
      *
      * @api
      *
-     * @param string    $name
-     * @param boolean   $shouldThrow
-     * @param \stdClass $env
+     * @param string                              $name
+     * @param boolean                             $shouldThrow
+     * @param \Leafo\ScssPhp\Compiler\Environment $env
      *
      * @return mixed
      */
-    public function get($name, $shouldThrow = true, $env = null)
+    public function get($name, $shouldThrow = true, Environment $env = null)
     {
-        $name = $this->normalizeName($name);
+        $normalizedName = $this->normalizeName($name);
+        $specialContentKey = self::$namespaces['special'] . 'content';
 
         if (! isset($env)) {
             $env = $this->getStoreEnv();
         }
 
-        $hasNamespace = $name[0] === '^' || $name[0] === '@' || $name[0] === '%';
+        $nextIsRoot = false;
+        $hasNamespace = $normalizedName[0] === '^' || $normalizedName[0] === '@' || $normalizedName[0] === '%';
 
         for (;;) {
-            if (array_key_exists($name, $env->store)) {
-                return $env->store[$name];
+            if (array_key_exists($normalizedName, $env->store)) {
+                return $env->store[$normalizedName];
             }
 
             if (! $hasNamespace && isset($env->marker)) {
+                if (! $nextIsRoot && ! empty($env->store[$specialContentKey])) {
+                    $env = $env->store[$specialContentKey]->scope;
+                    $nextIsRoot = true;
+                    continue;
+                }
+
                 $env = $this->rootEnv;
                 continue;
             }
@@ -2522,12 +3108,12 @@ class Compiler
     /**
      * Has variable?
      *
-     * @param string    $name
-     * @param \stdClass $env
+     * @param string                              $name
+     * @param \Leafo\ScssPhp\Compiler\Environment $env
      *
      * @return boolean
      */
-    protected function has($name, $env = null)
+    protected function has($name, Environment $env = null)
     {
         return $this->get($name, false, $env) !== null;
     }
@@ -2543,7 +3129,7 @@ class Compiler
             return;
         }
 
-        $parser = new Parser(__METHOD__, false);
+        $parser = $this->parserFactory(__METHOD__);
 
         foreach ($args as $name => $strValue) {
             if ($name[0] === '$') {
@@ -2582,6 +3168,32 @@ class Compiler
         unset($this->registeredVars[$name]);
     }
 
+    /**
+     * Returns list of variables
+     *
+     * @api
+     *
+     * @return array
+     */
+    public function getVariables()
+    {
+        return $this->registeredVars;
+    }
+
+    /**
+     * Adds to list of parsed files
+     *
+     * @api
+     *
+     * @param string $path
+     */
+    public function addParsedFile($path)
+    {
+        if (isset($path) && file_exists($path)) {
+            $this->parsedFiles[realpath($path)] = filemtime($path);
+        }
+    }
+
     /**
      * Returns list of parsed files
      *
@@ -2617,7 +3229,7 @@ class Compiler
      */
     public function setImportPaths($path)
     {
-        $this->importPaths = (array)$path;
+        $this->importPaths = (array) $path;
     }
 
     /**
@@ -2629,7 +3241,7 @@ class Compiler
      */
     public function setNumberPrecision($numberPrecision)
     {
-        $this->numberPrecision = $numberPrecision;
+        Node\Number::$precision = $numberPrecision;
     }
 
     /**
@@ -2663,10 +3275,11 @@ class Compiler
      *
      * @param string   $name
      * @param callable $func
+     * @param array    $prototype
      */
-    public function registerFunction($name, $func)
+    public function registerFunction($name, $func, $prototype = null)
     {
-        $this->userFunctions[$this->normalizeName($name)] = $func;
+        $this->userFunctions[$this->normalizeName($name)] = [$func, $prototype];
     }
 
     /**
@@ -2681,6 +3294,18 @@ class Compiler
         unset($this->userFunctions[$this->normalizeName($name)]);
     }
 
+    /**
+     * Add feature
+     *
+     * @api
+     *
+     * @param string $name
+     */
+    public function addFeature($name)
+    {
+        $this->registeredFeatures[$name] = true;
+    }
+
     /**
      * Import file
      *
@@ -2697,17 +3322,16 @@ class Compiler
 
             $tree = $this->importCache[$realPath];
         } else {
-            $code = file_get_contents($path);
-            $parser = new Parser($path, false);
-            $tree = $parser->parse($code);
+            $code   = file_get_contents($path);
+            $parser = $this->parserFactory($path);
+            $tree   = $parser->parse($code);
 
-            $this->parsedFiles[$realPath] = filemtime($path);
             $this->importCache[$realPath] = $tree;
         }
 
         $pi = pathinfo($path);
         array_unshift($this->importPaths, $pi['dirname']);
-        $this->compileChildren($tree->children, $out);
+        $this->compileChildrenNoReturn($tree->children, $out);
         array_shift($this->importPaths);
     }
 
@@ -2722,21 +3346,21 @@ class Compiler
      */
     public function findImport($url)
     {
-        $urls = array();
+        $urls = [];
 
         // for "normal" scss imports (ignore vanilla css and external requests)
         if (! preg_match('/\.css$|^https?:\/\//', $url)) {
             // try both normal and the _partial filename
-            $urls = array($url, preg_replace('/[^\/]+$/', '_\0', $url));
+            $urls = [$url, preg_replace('/[^\/]+$/', '_\0', $url)];
         }
 
         foreach ($this->importPaths as $dir) {
             if (is_string($dir)) {
                 // check urls for normal import paths
                 foreach ($urls as $full) {
-                    $full = $dir .
-                        (! empty($dir) && substr($dir, -1) !== '/' ? '/' : '') .
-                        $full;
+                    $full = $dir
+                        . (! empty($dir) && substr($dir, -1) !== '/' ? '/' : '')
+                        $full;
 
                     if ($this->fileExists($file = $full . '.scss') ||
                         $this->fileExists($file = $full)
@@ -2746,7 +3370,7 @@ class Compiler
                 }
             } elseif (is_callable($dir)) {
                 // check custom callback for import path
-                $file = call_user_func($dir, $url, $this);
+                $file = call_user_func($dir, $url);
 
                 if ($file !== null) {
                     return $file;
@@ -2757,6 +3381,32 @@ class Compiler
         return null;
     }
 
+    /**
+     * Set encoding
+     *
+     * @api
+     *
+     * @param string $encoding
+     */
+    public function setEncoding($encoding)
+    {
+        $this->encoding = $encoding;
+    }
+
+    /**
+     * Ignore errors?
+     *
+     * @api
+     *
+     * @param boolean $ignoreErrors
+     *
+     * @return \Leafo\ScssPhp\Compiler
+     */
+    public function setIgnoreErrors($ignoreErrors)
+    {
+        $this->ignoreErrors = $ignoreErrors;
+    }
+
     /**
      * Throw error (exception)
      *
@@ -2764,19 +3414,22 @@ class Compiler
      *
      * @param string $msg Message with optional sprintf()-style vararg parameters
      *
-     * @throws \Exception
+     * @throws \Leafo\ScssPhp\Exception\CompilerException
      */
     public function throwError($msg)
     {
+        if ($this->ignoreErrors) {
+            return;
+        }
+
         if (func_num_args() > 1) {
             $msg = call_user_func_array('sprintf', func_get_args());
         }
 
-        if ($this->sourcePos >= 0 && isset($this->sourceParser)) {
-            $this->sourceParser->throwParseError($msg, $this->sourcePos);
-        }
+        $line = $this->sourceLine;
+        $msg = "$msg: line: $line";
 
-        throw new \Exception($msg);
+        throw new CompilerException($msg);
     }
 
     /**
@@ -2786,17 +3439,14 @@ class Compiler
      *
      * @throws \Exception
      */
-    private function handleImportLoop($name)
+    protected function handleImportLoop($name)
     {
         for ($env = $this->env; $env; $env = $env->parent) {
-            $file = $env->block->sourceParser->getSourceName();
+            $file = $this->sourceNames[$env->block->sourceIndex];
 
             if (realpath($file) === $name) {
-                $this->throwError(
-                    'An @import loop has been found: %s imports %s',
-                    $this->env->block->sourceParser->getSourceName(),
-                    basename($file)
-                );
+                $this->throwError('An @import loop has been found: %s imports %s', $file, basename($file));
+                break;
             }
         }
     }
@@ -2813,6 +3463,51 @@ class Compiler
         return is_file($name);
     }
 
+    /**
+     * Call SCSS @function
+     *
+     * @param string $name
+     * @param array  $args
+     * @param array  $returnValue
+     *
+     * @return boolean Returns true if returnValue is set; otherwise, false
+     */
+    protected function callScssFunction($name, $argValues, &$returnValue)
+    {
+        $func = $this->get(self::$namespaces['function'] . $name, false);
+
+        if (! $func) {
+            return false;
+        }
+
+        $this->pushEnv();
+
+        $storeEnv = $this->storeEnv;
+        $this->storeEnv = $this->env;
+
+        // set the args
+        if (isset($func->args)) {
+            $this->applyArguments($func->args, $argValues);
+        }
+
+        // throw away lines and children
+        $tmp = new OutputBlock;
+        $tmp->lines    = [];
+        $tmp->children = [];
+
+        $this->env->marker = 'function';
+
+        $ret = $this->compileChildren($func->children, $tmp);
+
+        $this->storeEnv = $storeEnv;
+
+        $this->popEnv();
+
+        $returnValue = ! isset($ret) ? self::$defaultValue : $ret;
+
+        return true;
+    }
+
     /**
      * Call built-in and registered (PHP) functions
      *
@@ -2822,44 +3517,38 @@ class Compiler
      *
      * @return boolean Returns true if returnValue is set; otherwise, false
      */
-    protected function callBuiltin($name, $args, &$returnValue)
+    protected function callNativeFunction($name, $args, &$returnValue)
     {
         // try a lib function
         $name = $this->normalizeName($name);
 
         if (isset($this->userFunctions[$name])) {
             // see if we can find a user function
-            $fn = $this->userFunctions[$name];
-
-            foreach ($args as &$val) {
-                $val = $this->reduce($val[1], true);
-            }
-
-            $returnValue = call_user_func($fn, $args, $this);
+            list($f, $prototype) = $this->userFunctions[$name];
+        } elseif (($f = $this->getBuiltinFunction($name)) && is_callable($f)) {
+            $libName   = $f[1];
+            $prototype = isset(self::$$libName) ? self::$$libName : null;
         } else {
-            $f = $this->getBuiltinFunction($name);
-
-            if (is_callable($f)) {
-                $libName = $f[1];
-
-                $prototype = isset(self::$$libName) ? self::$$libName : null;
-                $sorted = $this->sortArgs($prototype, $args);
+            return false;
+        }
 
-                foreach ($sorted as &$val) {
-                    $val = $this->reduce($val, true);
-                }
+        list($sorted, $kwargs) = $this->sortArgs($prototype, $args);
 
-                $returnValue = call_user_func($f, $sorted, $this);
+        if ($name !== 'if' && $name !== 'call') {
+            foreach ($sorted as &$val) {
+                $val = $this->reduce($val, true);
             }
         }
 
-        if (isset($returnValue)) {
-            $returnValue = $this->coerceValue($returnValue);
+        $returnValue = call_user_func($f, $sorted, $kwargs);
 
-            return true;
+        if (! isset($returnValue)) {
+            return false;
         }
 
-        return false;
+        $returnValue = $this->coerceValue($returnValue);
+
+        return true;
     }
 
     /**
@@ -2879,14 +3568,12 @@ class Compiler
             ucfirst($name)
         );
 
-        return array($this, $libName);
+        return [$this, $libName];
     }
 
     /**
      * Sorts keyword arguments
      *
-     * @todo Merge with applyArguments()?
-     *
      * @param array $prototype
      * @param array $args
      *
@@ -2894,9 +3581,10 @@ class Compiler
      */
     protected function sortArgs($prototype, $args)
     {
-        $keyArgs = array();
-        $posArgs = array();
+        $keyArgs = [];
+        $posArgs = [];
 
+        // separate positional and keyword arguments
         foreach ($args as $arg) {
             list($key, $value) = $arg;
 
@@ -2910,33 +3598,22 @@ class Compiler
         }
 
         if (! isset($prototype)) {
-            return $posArgs;
+            return [$posArgs, $keyArgs];
         }
 
-        $finalArgs = array();
+        // copy positional args
+        $finalArgs = array_pad($posArgs, count($prototype), null);
 
+        // overwrite positional args with keyword args
         foreach ($prototype as $i => $names) {
-            if (isset($posArgs[$i])) {
-                $finalArgs[] = $posArgs[$i];
-                continue;
-            }
-
-            $set = false;
-
-            foreach ((array)$names as $name) {
+            foreach ((array) $names as $name) {
                 if (isset($keyArgs[$name])) {
-                    $finalArgs[] = $keyArgs[$name];
-                    $set = true;
-                    break;
+                    $finalArgs[$i] = $keyArgs[$name];
                 }
             }
-
-            if (! $set) {
-                $finalArgs[] = null;
-            }
         }
 
-        return $finalArgs;
+        return [$finalArgs, $keyArgs];
     }
 
     /**
@@ -2951,22 +3628,22 @@ class Compiler
     {
         $storeEnv = $this->getStoreEnv();
 
-        $env = new \stdClass;
+        $env = new Environment;
         $env->store = $storeEnv->store;
 
         $hasVariable = false;
-        $args = array();
+        $args = [];
 
         foreach ($argDef as $i => $arg) {
             list($name, $default, $isVariable) = $argDef[$i];
 
-            $args[$name] = array($i, $name, $default, $isVariable);
+            $args[$name] = [$i, $name, $default, $isVariable];
             $hasVariable |= $isVariable;
         }
 
-        $keywordArgs = array();
-        $deferredKeywordArgs = array();
-        $remaining = array();
+        $keywordArgs = [];
+        $deferredKeywordArgs = [];
+        $remaining = [];
 
         // assign the keyword args
         foreach ((array) $argValues as $arg) {
@@ -2976,19 +3653,33 @@ class Compiler
                         $deferredKeywordArgs[$arg[0][1]] = $arg[1];
                     } else {
                         $this->throwError("Mixin or function doesn't have an argument named $%s.", $arg[0][1]);
+                        break;
                     }
                 } elseif ($args[$arg[0][1]][0] < count($remaining)) {
                     $this->throwError("The argument $%s was passed both by position and by name.", $arg[0][1]);
+                    break;
                 } else {
                     $keywordArgs[$arg[0][1]] = $arg[1];
                 }
             } elseif (count($keywordArgs)) {
                 $this->throwError('Positional arguments must come before keyword arguments.');
+                break;
             } elseif ($arg[2] === true) {
                 $val = $this->reduce($arg[1], true);
 
-                if ($val[0] === 'list') {
+                if ($val[0] === Type::T_LIST) {
                     foreach ($val[2] as $name => $item) {
+                        if (! is_numeric($name)) {
+                            $keywordArgs[$name] = $item;
+                        } else {
+                            $remaining[] = $item;
+                        }
+                    }
+                } elseif ($val[0] === Type::T_MAP) {
+                    foreach ($val[1] as $i => $name) {
+                        $name = $this->compileStringContent($this->coerceString($name));
+                        $item = $val[2][$i];
+
                         if (! is_numeric($name)) {
                             $keywordArgs[$name] = $item;
                         } else {
@@ -3007,7 +3698,7 @@ class Compiler
             list($i, $name, $default, $isVariable) = $arg;
 
             if ($isVariable) {
-                $val = array('list', ',', array(), $isVariable);
+                $val = [Type::T_LIST, ',', [], $isVariable];
 
                 for ($count = count($remaining); $i < $count; $i++) {
                     $val[2][] = $remaining[$i];
@@ -3024,6 +3715,7 @@ class Compiler
                 continue;
             } else {
                 $this->throwError("Missing argument $name");
+                break;
             }
 
             $this->set($name, $this->reduce($val, true), true, $env);
@@ -3051,46 +3743,50 @@ class Compiler
      */
     private function coerceValue($value)
     {
-        if (is_array($value)) {
+        if (is_array($value) || $value instanceof \ArrayAccess) {
             return $value;
         }
 
         if (is_bool($value)) {
-            return $value ? self::$true : self::$false;
+            return $this->toBool($value);
         }
 
         if ($value === null) {
-            $value = self::$null;
+            return self::$null;
         }
 
         if (is_numeric($value)) {
-            return array('number', $value, '');
+            return new Node\Number($value, '');
         }
 
         if ($value === '') {
             return self::$emptyString;
         }
 
-        return array('keyword', $value);
-    }
+        if (preg_match('/^(#([0-9a-f]{6})|#([0-9a-f]{3}))$/i', $value, $m)) {
+            $color = [Type::T_COLOR];
 
-    /**
-     * Coerce unit on number to be normalized
-     *
-     * @param array  $number
-     * @param string $unit
-     *
-     * @return array
-     */
-    protected function coerceUnit($number, $unit)
-    {
-        list(, $value, $baseUnit) = $number;
+            if (isset($m[3])) {
+                $num = hexdec($m[3]);
 
-        if (isset(self::$unitTable[$baseUnit][$unit])) {
-            $value = $value * self::$unitTable[$baseUnit][$unit];
+                foreach ([3, 2, 1] as $i) {
+                    $t = $num & 0xf;
+                    $color[$i] = $t << 4 | $t;
+                    $num >>= 4;
+                }
+            } else {
+                $num = hexdec($m[2]);
+
+                foreach ([3, 2, 1] as $i) {
+                    $color[$i] = $num & 0xff;
+                    $num >>= 8;
+                }
+            }
+
+            return $color;
         }
 
-        return array('number', $value, $unit);
+        return [Type::T_KEYWORD, $value];
     }
 
     /**
@@ -3102,7 +3798,7 @@ class Compiler
      */
     protected function coerceMap($item)
     {
-        if ($item[0] === 'map') {
+        if ($item[0] === Type::T_MAP) {
             return $item;
         }
 
@@ -3110,7 +3806,7 @@ class Compiler
             return self::$emptyMap;
         }
 
-        return array('map', array($item), array(self::$null));
+        return [Type::T_MAP, [$item], [self::$null]];
     }
 
     /**
@@ -3122,26 +3818,30 @@ class Compiler
      */
     protected function coerceList($item, $delim = ',')
     {
-        if (isset($item) && $item[0] === 'list') {
+        if (isset($item) && $item[0] === Type::T_LIST) {
             return $item;
         }
 
-        if (isset($item) && $item[0] === 'map') {
+        if (isset($item) && $item[0] === Type::T_MAP) {
             $keys = $item[1];
             $values = $item[2];
-            $list = array();
+            $list = [];
 
             for ($i = 0, $s = count($keys); $i < $s; $i++) {
                 $key = $keys[$i];
                 $value = $values[$i];
 
-                $list[] = array('list', '', array(array('keyword', $this->compileValue($key)), $value));
+                $list[] = [
+                    Type::T_LIST,
+                    '',
+                    [[Type::T_KEYWORD, $this->compileStringContent($this->coerceString($key))], $value]
+                ];
             }
 
-            return array('list', ',', $list);
+            return [Type::T_LIST, ',', $list];
         }
 
-        return array('list', $delim, ! isset($item) ? array(): array($item));
+        return [Type::T_LIST, $delim, ! isset($item) ? []: [$item]];
     }
 
     /**
@@ -3170,18 +3870,18 @@ class Compiler
     protected function coerceColor($value)
     {
         switch ($value[0]) {
-            case 'color':
+            case Type::T_COLOR:
                 return $value;
 
-            case 'keyword':
+            case Type::T_KEYWORD:
                 $name = strtolower($value[1]);
 
                 if (isset(Colors::$cssColors[$name])) {
                     $rgba = explode(',', Colors::$cssColors[$name]);
 
                     return isset($rgba[3])
-                        ? array('color', (int) $rgba[0], (int) $rgba[1], (int) $rgba[2], (int) $rgba[3])
-                        : array('color', (int) $rgba[0], (int) $rgba[1], (int) $rgba[2]);
+                        ? [Type::T_COLOR, (int) $rgba[0], (int) $rgba[1], (int) $rgba[2], (int) $rgba[3]]
+                        : [Type::T_COLOR, (int) $rgba[0], (int) $rgba[1], (int) $rgba[2]];
                 }
 
                 return null;
@@ -3199,11 +3899,11 @@ class Compiler
      */
     protected function coerceString($value)
     {
-        if ($value[0] === 'string') {
+        if ($value[0] === Type::T_STRING) {
             return $value;
         }
 
-        return array('string', '', array($this->compileValue($value)));
+        return [Type::T_STRING, '', [$this->compileValue($value)]];
     }
 
     /**
@@ -3215,8 +3915,8 @@ class Compiler
      */
     protected function coercePercent($value)
     {
-        if ($value[0] === 'number') {
-            if ($value[2] === '%') {
+        if ($value[0] === Type::T_NUMBER) {
+            if (! empty($value[2]['%'])) {
                 return $value[1] / 100;
             }
 
@@ -3241,7 +3941,7 @@ class Compiler
     {
         $value = $this->coerceMap($value);
 
-        if ($value[0] !== 'map') {
+        if ($value[0] !== Type::T_MAP) {
             $this->throwError('expecting map');
         }
 
@@ -3261,7 +3961,7 @@ class Compiler
      */
     public function assertList($value)
     {
-        if ($value[0] !== 'list') {
+        if ($value[0] !== Type::T_LIST) {
             $this->throwError('expecting list');
         }
 
@@ -3301,7 +4001,7 @@ class Compiler
      */
     public function assertNumber($value)
     {
-        if ($value[0] !== 'number') {
+        if ($value[0] !== Type::T_NUMBER) {
             $this->throwError('expecting number');
         }
 
@@ -3317,7 +4017,7 @@ class Compiler
      */
     protected function fixColor($c)
     {
-        foreach (range(1, 3) as $i) {
+        foreach ([1, 2, 3] as $i) {
             if ($c[$i] < 0) {
                 $c[$i] = 0;
             }
@@ -3367,7 +4067,7 @@ class Compiler
             }
         }
 
-        return array('hsl', fmod($h, 360), $s * 100, $l / 5.1);
+        return [Type::T_HSL, fmod($h, 360), $s * 100, $l / 5.1];
     }
 
     /**
@@ -3430,43 +4130,68 @@ class Compiler
         $g = $this->hueToRGB($m1, $m2, $h) * 255;
         $b = $this->hueToRGB($m1, $m2, $h - 1/3) * 255;
 
-        $out = array('color', $r, $g, $b);
+        $out = [Type::T_COLOR, $r, $g, $b];
 
         return $out;
     }
 
     // Built in functions
 
-    protected static $libIf = array('condition', 'if-true', 'if-false');
+    //protected static $libCall = ['name', 'args...'];
+    protected function libCall($args, $kwargs)
+    {
+        $name = $this->compileStringContent($this->coerceString($this->reduce(array_shift($args), true)));
+
+        $args = array_map(
+            function ($a) {
+                return [null, $a, false];
+            },
+            $args
+        );
+
+        if (count($kwargs)) {
+            foreach ($kwargs as $key => $value) {
+                $args[] = [[Type::T_VARIABLE, $key], $value, false];
+            }
+        }
+
+        return $this->reduce([Type::T_FUNCTION_CALL, $name, $args]);
+    }
+
+    protected static $libIf = ['condition', 'if-true', 'if-false'];
     protected function libIf($args)
     {
         list($cond, $t, $f) = $args;
 
-        if (! $this->isTruthy($cond)) {
-            return $f;
+        if (! $this->isTruthy($this->reduce($cond, true))) {
+            return $this->reduce($f, true);
         }
 
-        return $t;
+        return $this->reduce($t, true);
     }
 
-    protected static $libIndex = array('list', 'value');
+    protected static $libIndex = ['list', 'value'];
     protected function libIndex($args)
     {
         list($list, $value) = $args;
 
-        if ($value[0] === 'map') {
+        if ($value[0] === Type::T_MAP) {
             return self::$null;
         }
 
-        if ($list[0] === 'map') {
+        if ($list[0] === Type::T_MAP ||
+            $list[0] === Type::T_STRING ||
+            $list[0] === Type::T_KEYWORD ||
+            $list[0] === Type::T_INTERPOLATE
+        ) {
             $list = $this->coerceList($list, ' ');
         }
 
-        if ($list[0] !== 'list') {
+        if ($list[0] !== Type::T_LIST) {
             return self::$null;
         }
 
-        $values = array();
+        $values = [];
 
         foreach ($list[2] as $item) {
             $values[] = $this->normalizeValue($item);
@@ -3477,23 +4202,20 @@ class Compiler
         return false === $key ? self::$null : $key + 1;
     }
 
-    protected static $libRgb = array('red', 'green', 'blue');
+    protected static $libRgb = ['red', 'green', 'blue'];
     protected function libRgb($args)
     {
         list($r, $g, $b) = $args;
 
-        return array('color', $r[1], $g[1], $b[1]);
+        return [Type::T_COLOR, $r[1], $g[1], $b[1]];
     }
 
-    protected static $libRgba = array(
-        array('red', 'color'),
-        'green', 'blue', 'alpha');
+    protected static $libRgba = [
+        ['red', 'color'],
+        'green', 'blue', 'alpha'];
     protected function libRgba($args)
     {
         if ($color = $this->coerceColor($args[0])) {
-            // workaround https://github.com/facebook/hhvm/issues/5457
-            reset($args);
-
             $num = ! isset($args[1]) ? $args[3] : $args[1];
             $alpha = $this->assertNumber($num);
             $color[4] = $alpha;
@@ -3503,7 +4225,7 @@ class Compiler
 
         list($r, $g, $b, $a) = $args;
 
-        return array('color', $r[1], $g[1], $b[1], $a[1]);
+        return [Type::T_COLOR, $r[1], $g[1], $b[1], $a[1]];
     }
 
     // helper function for adjust_color, change_color, and scale_color
@@ -3511,10 +4233,7 @@ class Compiler
     {
         $color = $this->assertColor($args[0]);
 
-        // workaround https://github.com/facebook/hhvm/issues/5457
-        reset($args);
-
-        foreach (array(1, 2, 3, 7) as $i) {
+        foreach ([1, 2, 3, 7] as $i) {
             if (isset($args[$i])) {
                 $val = $this->assertNumber($args[$i]);
                 $ii = $i === 7 ? 4 : $i; // alpha
@@ -3525,7 +4244,7 @@ class Compiler
         if (isset($args[4]) || isset($args[5]) || isset($args[6])) {
             $hsl = $this->toHSL($color[1], $color[2], $color[3]);
 
-            foreach (array(4, 5, 6) as $i) {
+            foreach ([4, 5, 6] as $i) {
                 if (isset($args[$i])) {
                     $val = $this->assertNumber($args[$i]);
                     $hsl[$i - 3] = call_user_func($fn, $hsl[$i - 3], $val, $i);
@@ -3544,10 +4263,10 @@ class Compiler
         return $color;
     }
 
-    protected static $libAdjustColor = array(
+    protected static $libAdjustColor = [
         'color', 'red', 'green', 'blue',
         'hue', 'saturation', 'lightness', 'alpha'
-    );
+    ];
     protected function libAdjustColor($args)
     {
         return $this->alterColor($args, function ($base, $alter, $i) {
@@ -3555,10 +4274,10 @@ class Compiler
         });
     }
 
-    protected static $libChangeColor = array(
+    protected static $libChangeColor = [
         'color', 'red', 'green', 'blue',
         'hue', 'saturation', 'lightness', 'alpha'
-    );
+    ];
     protected function libChangeColor($args)
     {
         return $this->alterColor($args, function ($base, $alter, $i) {
@@ -3566,10 +4285,10 @@ class Compiler
         });
     }
 
-    protected static $libScaleColor = array(
+    protected static $libScaleColor = [
         'color', 'red', 'green', 'blue',
         'hue', 'saturation', 'lightness', 'alpha'
-    );
+    ];
     protected function libScaleColor($args)
     {
         return $this->alterColor($args, function ($base, $scale, $i) {
@@ -3605,16 +4324,16 @@ class Compiler
         });
     }
 
-    protected static $libIeHexStr = array('color');
+    protected static $libIeHexStr = ['color'];
     protected function libIeHexStr($args)
     {
         $color = $this->coerceColor($args[0]);
-        $color[4] = isset($color[4]) ? round(255*$color[4]) : 255;
+        $color[4] = isset($color[4]) ? round(255 * $color[4]) : 255;
 
         return sprintf('#%02X%02X%02X%02X', $color[4], $color[1], $color[2], $color[3]);
     }
 
-    protected static $libRed = array('color');
+    protected static $libRed = ['color'];
     protected function libRed($args)
     {
         $color = $this->coerceColor($args[0]);
@@ -3622,7 +4341,7 @@ class Compiler
         return $color[1];
     }
 
-    protected static $libGreen = array('color');
+    protected static $libGreen = ['color'];
     protected function libGreen($args)
     {
         $color = $this->coerceColor($args[0]);
@@ -3630,7 +4349,7 @@ class Compiler
         return $color[2];
     }
 
-    protected static $libBlue = array('color');
+    protected static $libBlue = ['color'];
     protected function libBlue($args)
     {
         $color = $this->coerceColor($args[0]);
@@ -3638,7 +4357,7 @@ class Compiler
         return $color[3];
     }
 
-    protected static $libAlpha = array('color');
+    protected static $libAlpha = ['color'];
     protected function libAlpha($args)
     {
         if ($color = $this->coerceColor($args[0])) {
@@ -3649,12 +4368,12 @@ class Compiler
         return null;
     }
 
-    protected static $libOpacity = array('color');
+    protected static $libOpacity = ['color'];
     protected function libOpacity($args)
     {
         $value = $args[0];
 
-        if ($value[0] === 'number') {
+        if ($value[0] === Type::T_NUMBER) {
             return null;
         }
 
@@ -3662,7 +4381,7 @@ class Compiler
     }
 
     // mix two colors
-    protected static $libMix = array('color-1', 'color-2', 'weight');
+    protected static $libMix = ['color-1', 'color-2', 'weight'];
     protected function libMix($args)
     {
         list($first, $second, $weight) = $args;
@@ -3685,11 +4404,11 @@ class Compiler
         $w1 = (($w * $a === -1 ? $w : ($w + $a) / (1 + $w * $a)) + 1) / 2.0;
         $w2 = 1.0 - $w1;
 
-        $new = array('color',
+        $new = [Type::T_COLOR,
             $w1 * $first[1] + $w2 * $second[1],
             $w1 * $first[2] + $w2 * $second[2],
             $w1 * $first[3] + $w2 * $second[3],
-        );
+        ];
 
         if ($firstAlpha != 1.0 || $secondAlpha != 1.0) {
             $new[] = $firstAlpha * $weight + $secondAlpha * ($weight - 1);
@@ -3698,7 +4417,7 @@ class Compiler
         return $this->fixColor($new);
     }
 
-    protected static $libHsl = array('hue', 'saturation', 'lightness');
+    protected static $libHsl = ['hue', 'saturation', 'lightness'];
     protected function libHsl($args)
     {
         list($h, $s, $l) = $args;
@@ -3706,8 +4425,7 @@ class Compiler
         return $this->toRGB($h[1], $s[1], $l[1]);
     }
 
-    protected static $libHsla = array('hue', 'saturation',
-        'lightness', 'alpha');
+    protected static $libHsla = ['hue', 'saturation', 'lightness', 'alpha'];
     protected function libHsla($args)
     {
         list($h, $s, $l, $a) = $args;
@@ -3718,31 +4436,31 @@ class Compiler
         return $color;
     }
 
-    protected static $libHue = array('color');
+    protected static $libHue = ['color'];
     protected function libHue($args)
     {
         $color = $this->assertColor($args[0]);
         $hsl = $this->toHSL($color[1], $color[2], $color[3]);
 
-        return array('number', $hsl[1], 'deg');
+        return new Node\Number($hsl[1], 'deg');
     }
 
-    protected static $libSaturation = array('color');
+    protected static $libSaturation = ['color'];
     protected function libSaturation($args)
     {
         $color = $this->assertColor($args[0]);
         $hsl = $this->toHSL($color[1], $color[2], $color[3]);
 
-        return array('number', $hsl[2], '%');
+        return new Node\Number($hsl[2], '%');
     }
 
-    protected static $libLightness = array('color');
+    protected static $libLightness = ['color'];
     protected function libLightness($args)
     {
         $color = $this->assertColor($args[0]);
         $hsl = $this->toHSL($color[1], $color[2], $color[3]);
 
-        return array('number', $hsl[3], '%');
+        return new Node\Number($hsl[3], '%');
     }
 
     protected function adjustHsl($color, $idx, $amount)
@@ -3758,7 +4476,7 @@ class Compiler
         return $out;
     }
 
-    protected static $libAdjustHue = array('color', 'degrees');
+    protected static $libAdjustHue = ['color', 'degrees'];
     protected function libAdjustHue($args)
     {
         $color = $this->assertColor($args[0]);
@@ -3767,7 +4485,7 @@ class Compiler
         return $this->adjustHsl($color, 1, $degrees);
     }
 
-    protected static $libLighten = array('color', 'amount');
+    protected static $libLighten = ['color', 'amount'];
     protected function libLighten($args)
     {
         $color = $this->assertColor($args[0]);
@@ -3776,7 +4494,7 @@ class Compiler
         return $this->adjustHsl($color, 3, $amount);
     }
 
-    protected static $libDarken = array('color', 'amount');
+    protected static $libDarken = ['color', 'amount'];
     protected function libDarken($args)
     {
         $color = $this->assertColor($args[0]);
@@ -3785,54 +4503,54 @@ class Compiler
         return $this->adjustHsl($color, 3, -$amount);
     }
 
-    protected static $libSaturate = array('color', 'amount');
+    protected static $libSaturate = ['color', 'amount'];
     protected function libSaturate($args)
     {
         $value = $args[0];
 
-        if ($value[0] === 'number') {
+        if ($value[0] === Type::T_NUMBER) {
             return null;
         }
 
         $color = $this->assertColor($value);
-        $amount = 100*$this->coercePercent($args[1]);
+        $amount = 100 * $this->coercePercent($args[1]);
 
         return $this->adjustHsl($color, 2, $amount);
     }
 
-    protected static $libDesaturate = array('color', 'amount');
+    protected static $libDesaturate = ['color', 'amount'];
     protected function libDesaturate($args)
     {
         $color = $this->assertColor($args[0]);
-        $amount = 100*$this->coercePercent($args[1]);
+        $amount = 100 * $this->coercePercent($args[1]);
 
         return $this->adjustHsl($color, 2, -$amount);
     }
 
-    protected static $libGrayscale = array('color');
+    protected static $libGrayscale = ['color'];
     protected function libGrayscale($args)
     {
         $value = $args[0];
 
-        if ($value[0] === 'number') {
+        if ($value[0] === Type::T_NUMBER) {
             return null;
         }
 
         return $this->adjustHsl($this->assertColor($value), 2, -100);
     }
 
-    protected static $libComplement = array('color');
+    protected static $libComplement = ['color'];
     protected function libComplement($args)
     {
         return $this->adjustHsl($this->assertColor($args[0]), 1, 180);
     }
 
-    protected static $libInvert = array('color');
+    protected static $libInvert = ['color'];
     protected function libInvert($args)
     {
         $value = $args[0];
 
-        if ($value[0] === 'number') {
+        if ($value[0] === Type::T_NUMBER) {
             return null;
         }
 
@@ -3845,7 +4563,7 @@ class Compiler
     }
 
     // increases opacity by amount
-    protected static $libOpacify = array('color', 'amount');
+    protected static $libOpacify = ['color', 'amount'];
     protected function libOpacify($args)
     {
         $color = $this->assertColor($args[0]);
@@ -3857,14 +4575,14 @@ class Compiler
         return $color;
     }
 
-    protected static $libFadeIn = array('color', 'amount');
+    protected static $libFadeIn = ['color', 'amount'];
     protected function libFadeIn($args)
     {
         return $this->libOpacify($args);
     }
 
     // decreases opacity by amount
-    protected static $libTransparentize = array('color', 'amount');
+    protected static $libTransparentize = ['color', 'amount'];
     protected function libTransparentize($args)
     {
         $color = $this->assertColor($args[0]);
@@ -3876,45 +4594,43 @@ class Compiler
         return $color;
     }
 
-    protected static $libFadeOut = array('color', 'amount');
+    protected static $libFadeOut = ['color', 'amount'];
     protected function libFadeOut($args)
     {
         return $this->libTransparentize($args);
     }
 
-    protected static $libUnquote = array('string');
+    protected static $libUnquote = ['string'];
     protected function libUnquote($args)
     {
         $str = $args[0];
 
-        if ($str[0] === 'string') {
+        if ($str[0] === Type::T_STRING) {
             $str[1] = '';
         }
 
         return $str;
     }
 
-    protected static $libQuote = array('string');
+    protected static $libQuote = ['string'];
     protected function libQuote($args)
     {
         $value = $args[0];
 
-        if ($value[0] === 'string' && ! empty($value[1])) {
+        if ($value[0] === Type::T_STRING && ! empty($value[1])) {
             return $value;
         }
 
-        return array('string', '"', array($value));
+        return [Type::T_STRING, '"', [$value]];
     }
 
-    protected static $libPercentage = array('value');
+    protected static $libPercentage = ['value'];
     protected function libPercentage($args)
     {
-        return array('number',
-            $this->coercePercent($args[0]) * 100,
-            '%');
+        return new Node\Number($this->coercePercent($args[0]) * 100, '%');
     }
 
-    protected static $libRound = array('value');
+    protected static $libRound = ['value'];
     protected function libRound($args)
     {
         $num = $args[0];
@@ -3923,7 +4639,7 @@ class Compiler
         return $num;
     }
 
-    protected static $libFloor = array('value');
+    protected static $libFloor = ['value'];
     protected function libFloor($args)
     {
         $num = $args[0];
@@ -3932,7 +4648,7 @@ class Compiler
         return $num;
     }
 
-    protected static $libCeil = array('value');
+    protected static $libCeil = ['value'];
     protected function libCeil($args)
     {
         $num = $args[0];
@@ -3941,7 +4657,7 @@ class Compiler
         return $num;
     }
 
-    protected static $libAbs = array('value');
+    protected static $libAbs = ['value'];
     protected function libAbs($args)
     {
         $num = $args[0];
@@ -3957,7 +4673,7 @@ class Compiler
 
         foreach ($numbers as $key => $number) {
             if (null === $min || $number[1] <= $min[1]) {
-                $min = array($key, $number[1]);
+                $min = [$key, $number[1]];
             }
         }
 
@@ -3971,7 +4687,7 @@ class Compiler
 
         foreach ($numbers as $key => $number) {
             if (null === $max || $number[1] >= $max[1]) {
-                $max = array($key, $number[1]);
+                $max = [$key, $number[1]];
             }
         }
 
@@ -3989,20 +4705,22 @@ class Compiler
     {
         $unit = null;
         $originalUnit = null;
-        $numbers = array();
+        $numbers = [];
 
         foreach ($args as $key => $item) {
-            if ('number' !== $item[0]) {
+            if ($item[0] !== Type::T_NUMBER) {
                 $this->throwError('%s is not a number', $item[0]);
+                break;
             }
 
-            $number = $this->normalizeNumber($item);
+            $number = $item->normalize();
 
             if (null === $unit) {
                 $unit = $number[2];
-                $originalUnit = $item[2];
+                $originalUnit = $item->unitStr();
             } elseif ($unit !== $number[2]) {
-                $this->throwError('Incompatible units: "%s" and "%s".', $originalUnit, $item[2]);
+                $this->throwError('Incompatible units: "%s" and "%s".', $originalUnit, $item->unitStr());
+                break;
             }
 
             $numbers[$key] = $number;
@@ -4011,7 +4729,7 @@ class Compiler
         return $numbers;
     }
 
-    protected static $libLength = array('list');
+    protected static $libLength = ['list'];
     protected function libLength($args)
     {
         $list = $this->coerceList($args[0]);
@@ -4019,8 +4737,7 @@ class Compiler
         return count($list[2]);
     }
 
-    // TODO: need a way to declare this built-in as varargs
-    //protected static $libListSeparator = array('list...');
+    //protected static $libListSeparator = ['list...'];
     protected function libListSeparator($args)
     {
         if (count($args) > 1) {
@@ -4040,23 +4757,37 @@ class Compiler
         return 'space';
     }
 
-    protected static $libNth = array('list', 'n');
+    protected static $libNth = ['list', 'n'];
     protected function libNth($args)
     {
         $list = $this->coerceList($args[0]);
-        $n = $this->assertNumber($args[1]) - 1;
+        $n = $this->assertNumber($args[1]);
+
+        if ($n > 0) {
+            $n--;
+        } elseif ($n < 0) {
+            $n += count($list[2]);
+        }
 
         return isset($list[2][$n]) ? $list[2][$n] : self::$defaultValue;
     }
 
-    protected static $libSetNth = array('list', 'n', 'value');
+    protected static $libSetNth = ['list', 'n', 'value'];
     protected function libSetNth($args)
     {
         $list = $this->coerceList($args[0]);
-        $n = $this->assertNumber($args[1]) - 1;
+        $n = $this->assertNumber($args[1]);
+
+        if ($n > 0) {
+            $n--;
+        } elseif ($n < 0) {
+            $n += count($list[2]);
+        }
 
         if (! isset($list[2][$n])) {
             $this->throwError('Invalid argument for "n"');
+
+            return;
         }
 
         $list[2][$n] = $args[2];
@@ -4064,15 +4795,14 @@ class Compiler
         return $list;
     }
 
-    protected static $libMapGet = array('map', 'key');
+    protected static $libMapGet = ['map', 'key'];
     protected function libMapGet($args)
     {
         $map = $this->assertMap($args[0]);
-
         $key = $this->compileStringContent($this->coerceString($args[1]));
 
         for ($i = count($map[1]) - 1; $i >= 0; $i--) {
-            if ($key === $this->compileValue($map[1][$i])) {
+            if ($key === $this->compileStringContent($this->coerceString($map[1][$i]))) {
                 return $map[2][$i];
             }
         }
@@ -4080,35 +4810,32 @@ class Compiler
         return self::$null;
     }
 
-    protected static $libMapKeys = array('map');
+    protected static $libMapKeys = ['map'];
     protected function libMapKeys($args)
     {
         $map = $this->assertMap($args[0]);
-
         $keys = $map[1];
 
-        return array('list', ',', $keys);
+        return [Type::T_LIST, ',', $keys];
     }
 
-    protected static $libMapValues = array('map');
+    protected static $libMapValues = ['map'];
     protected function libMapValues($args)
     {
         $map = $this->assertMap($args[0]);
-
         $values = $map[2];
 
-        return array('list', ',', $values);
+        return [Type::T_LIST, ',', $values];
     }
 
-    protected static $libMapRemove = array('map', 'key');
+    protected static $libMapRemove = ['map', 'key'];
     protected function libMapRemove($args)
     {
         $map = $this->assertMap($args[0]);
-
         $key = $this->compileStringContent($this->coerceString($args[1]));
 
         for ($i = count($map[1]) - 1; $i >= 0; $i--) {
-            if ($key === $this->compileValue($map[1][$i])) {
+            if ($key === $this->compileStringContent($this->coerceString($map[1][$i]))) {
                 array_splice($map[1], $i, 1);
                 array_splice($map[2], $i, 1);
             }
@@ -4117,29 +4844,44 @@ class Compiler
         return $map;
     }
 
-    protected static $libMapHasKey = array('map', 'key');
+    protected static $libMapHasKey = ['map', 'key'];
     protected function libMapHasKey($args)
     {
         $map = $this->assertMap($args[0]);
-
         $key = $this->compileStringContent($this->coerceString($args[1]));
 
         for ($i = count($map[1]) - 1; $i >= 0; $i--) {
-            if ($key === $this->compileValue($map[1][$i])) {
-                return self::$true;
+            if ($key === $this->compileStringContent($this->coerceString($map[1][$i]))) {
+                return true;
             }
         }
 
-        return self::$false;
+        return false;
     }
 
-    protected static $libMapMerge = array('map-1', 'map-2');
+    protected static $libMapMerge = ['map-1', 'map-2'];
     protected function libMapMerge($args)
     {
         $map1 = $this->assertMap($args[0]);
         $map2 = $this->assertMap($args[1]);
 
-        return array('map', array_merge($map1[1], $map2[1]), array_merge($map1[2], $map2[2]));
+        return [Type::T_MAP, array_merge($map1[1], $map2[1]), array_merge($map1[2], $map2[2])];
+    }
+
+    protected static $libKeywords = ['args'];
+    protected function libKeywords($args)
+    {
+        $this->assertList($args[0]);
+
+        $keys = [];
+        $values = [];
+
+        foreach ($args[0][2] as $name => $arg) {
+            $keys[] = [Type::T_KEYWORD, $name];
+            $values[] = $arg;
+        }
+
+        return [Type::T_MAP, $keys, $values];
     }
 
     protected function listSeparatorForJoin($list1, $sep)
@@ -4160,7 +4902,7 @@ class Compiler
         }
     }
 
-    protected static $libJoin = array('list1', 'list2', 'separator');
+    protected static $libJoin = ['list1', 'list2', 'separator'];
     protected function libJoin($args)
     {
         list($list1, $list2, $sep) = $args;
@@ -4169,10 +4911,10 @@ class Compiler
         $list2 = $this->coerceList($list2, ' ');
         $sep = $this->listSeparatorForJoin($list1, $sep);
 
-        return array('list', $sep, array_merge($list1[2], $list2[2]));
+        return [Type::T_LIST, $sep, array_merge($list1[2], $list2[2])];
     }
 
-    protected static $libAppend = array('list', 'val', 'separator');
+    protected static $libAppend = ['list', 'val', 'separator'];
     protected function libAppend($args)
     {
         list($list1, $value, $sep) = $args;
@@ -4180,7 +4922,7 @@ class Compiler
         $list1 = $this->coerceList($list1, ' ');
         $sep = $this->listSeparatorForJoin($list1, $sep);
 
-        return array('list', $sep, array_merge($list1[2], array($value)));
+        return [Type::T_LIST, $sep, array_merge($list1[2], [$value])];
     }
 
     protected function libZip($args)
@@ -4189,11 +4931,11 @@ class Compiler
             $this->assertList($arg);
         }
 
-        $lists = array();
+        $lists = [];
         $firstList = array_shift($args);
 
         foreach ($firstList[2] as $key => $item) {
-            $list = array('list', '', array($item));
+            $list = [Type::T_LIST, '', [$item]];
 
             foreach ($args as $arg) {
                 if (isset($arg[2][$key])) {
@@ -4202,19 +4944,20 @@ class Compiler
                     break 2;
                 }
             }
+
             $lists[] = $list;
         }
 
-        return array('list', ',', $lists);
+        return [Type::T_LIST, ',', $lists];
     }
 
-    protected static $libTypeOf = array('value');
+    protected static $libTypeOf = ['value'];
     protected function libTypeOf($args)
     {
         $value = $args[0];
 
         switch ($value[0]) {
-            case 'keyword':
+            case Type::T_KEYWORD:
                 if ($value === self::$true || $value === self::$false) {
                     return 'bool';
                 }
@@ -4224,10 +4967,10 @@ class Compiler
                 }
 
                 // fall-thru
-            case 'function':
+            case Type::T_FUNCTION:
                 return 'string';
 
-            case 'list':
+            case Type::T_LIST:
                 if (isset($value[3]) && $value[3]) {
                     return 'arglist';
                 }
@@ -4238,42 +4981,46 @@ class Compiler
         }
     }
 
-    protected static $libUnit = array('number');
+    protected static $libUnit = ['number'];
     protected function libUnit($args)
     {
         $num = $args[0];
 
-        if ($num[0] === 'number') {
-            return array('string', '"', array($num[2]));
+        if ($num[0] === Type::T_NUMBER) {
+            return [Type::T_STRING, '"', [$num->unitStr()]];
         }
 
         return '';
     }
 
-    protected static $libUnitless = array('number');
+    protected static $libUnitless = ['number'];
     protected function libUnitless($args)
     {
         $value = $args[0];
 
-        return $value[0] === 'number' && empty($value[2]);
+        return $value[0] === Type::T_NUMBER && $value->unitless();
     }
 
-    protected static $libComparable = array('number-1', 'number-2');
+    protected static $libComparable = ['number-1', 'number-2'];
     protected function libComparable($args)
     {
         list($number1, $number2) = $args;
 
-        if (! isset($number1[0]) || $number1[0] !== 'number' || ! isset($number2[0]) || $number2[0] !== 'number') {
+        if (! isset($number1[0]) || $number1[0] !== Type::T_NUMBER ||
+            ! isset($number2[0]) || $number2[0] !== Type::T_NUMBER
+        ) {
             $this->throwError('Invalid argument(s) for "comparable"');
+
+            return;
         }
 
-        $number1 = $this->normalizeNumber($number1);
-        $number2 = $this->normalizeNumber($number2);
+        $number1 = $number1->normalize();
+        $number2 = $number2->normalize();
 
-        return $number1[2] === $number2[2] || $number1[2] === '' || $number2[2] === '';
+        return $number1[2] === $number2[2] || $number1->unitless() || $number2->unitless();
     }
 
-    protected static $libStrIndex = array('string', 'substring');
+    protected static $libStrIndex = ['string', 'substring'];
     protected function libStrIndex($args)
     {
         $string = $this->coerceString($args[0]);
@@ -4284,10 +5031,10 @@ class Compiler
 
         $result = strpos($stringContent, $substringContent);
 
-        return $result === false ? self::$null : array('number', $result + 1, '');
+        return $result === false ? self::$null : new Node\Number($result + 1, '');
     }
 
-    protected static $libStrInsert = array('string', 'insert', 'index');
+    protected static $libStrInsert = ['string', 'insert', 'index'];
     protected function libStrInsert($args)
     {
         $string = $this->coerceString($args[0]);
@@ -4298,74 +5045,80 @@ class Compiler
 
         list(, $index) = $args[2];
 
-        $string[2] = array(substr_replace($stringContent, $insertContent, $index - 1, 0));
+        $string[2] = [substr_replace($stringContent, $insertContent, $index - 1, 0)];
 
         return $string;
     }
 
-    protected static $libStrLength = array('string');
+    protected static $libStrLength = ['string'];
     protected function libStrLength($args)
     {
         $string = $this->coerceString($args[0]);
         $stringContent = $this->compileStringContent($string);
 
-        return array('number', strlen($stringContent), '');
+        return new Node\Number(strlen($stringContent), '');
     }
 
-    protected static $libStrSlice = array('string', 'start-at', 'end-at');
+    protected static $libStrSlice = ['string', 'start-at', 'end-at'];
     protected function libStrSlice($args)
     {
-        if ($args[2][1] == 0) {
-            return self::$emptyString;
+        if (isset($args[2]) && $args[2][1] == 0) {
+            return self::$nullString;
         }
 
         $string = $this->coerceString($args[0]);
         $stringContent = $this->compileStringContent($string);
 
-        $start = (int) $args[1][1] ?: 1;
-        $end = (int) $args[2][1];
+        $start = (int) $args[1][1];
 
-        $string[2] = array(substr($stringContent, $start - 1, $end < 0 ? $end : $end - $start + 1));
+        if ($start > 0) {
+            $start--;
+        }
+
+        $end    = (int) $args[2][1];
+        $length = $end < 0 ? $end + 1 : ($end > 0 ? $end - $start : $end);
+
+        $string[2] = $length
+            ? [substr($stringContent, $start, $length)]
+            : [substr($stringContent, $start)];
 
         return $string;
     }
 
-    protected static $libToLowerCase = array('string');
+    protected static $libToLowerCase = ['string'];
     protected function libToLowerCase($args)
     {
         $string = $this->coerceString($args[0]);
         $stringContent = $this->compileStringContent($string);
 
-        $string[2] = array(mb_strtolower($stringContent));
+        $string[2] = [mb_strtolower($stringContent)];
 
         return $string;
     }
 
-    protected static $libToUpperCase = array('string');
+    protected static $libToUpperCase = ['string'];
     protected function libToUpperCase($args)
     {
         $string = $this->coerceString($args[0]);
         $stringContent = $this->compileStringContent($string);
 
-        $string[2] = array(mb_strtoupper($stringContent));
+        $string[2] = [mb_strtoupper($stringContent)];
 
         return $string;
     }
 
-    protected static $libFeatureExists = array('feature');
+    protected static $libFeatureExists = ['feature'];
     protected function libFeatureExists($args)
     {
-        /*
-         * The following features not not (yet) supported:
-         * - global-variable-shadowing
-         * - extend-selector-pseudoclass
-         * - units-level-3
-         * - at-error
-         */
-        return self::$false;
+        $string = $this->coerceString($args[0]);
+        $name = $this->compileStringContent($string);
+
+        return $this->toBool(
+            array_key_exists($name, $this->registeredFeatures) ? $this->registeredFeatures[$name] : false
+        );
     }
 
-    protected static $libFunctionExists = array('name');
+    protected static $libFunctionExists = ['name'];
     protected function libFunctionExists($args)
     {
         $string = $this->coerceString($args[0]);
@@ -4373,13 +5126,13 @@ class Compiler
 
         // user defined functions
         if ($this->has(self::$namespaces['function'] . $name)) {
-            return self::$true;
+            return true;
         }
 
         $name = $this->normalizeName($name);
 
         if (isset($this->userFunctions[$name])) {
-            return self::$true;
+            return true;
         }
 
         // built-in functions
@@ -4388,31 +5141,31 @@ class Compiler
         return $this->toBool(is_callable($f));
     }
 
-    protected static $libGlobalVariableExists = array('name');
+    protected static $libGlobalVariableExists = ['name'];
     protected function libGlobalVariableExists($args)
     {
         $string = $this->coerceString($args[0]);
         $name = $this->compileStringContent($string);
 
-        return $this->has($name, $this->rootEnv) ? self::$true : self::$false;
+        return $this->has($name, $this->rootEnv);
     }
 
-    protected static $libMixinExists = array('name');
+    protected static $libMixinExists = ['name'];
     protected function libMixinExists($args)
     {
         $string = $this->coerceString($args[0]);
         $name = $this->compileStringContent($string);
 
-        return $this->has(self::$namespaces['mixin'] . $name) ? self::$true : self::$false;
+        return $this->has(self::$namespaces['mixin'] . $name);
     }
 
-    protected static $libVariableExists = array('name');
+    protected static $libVariableExists = ['name'];
     protected function libVariableExists($args)
     {
         $string = $this->coerceString($args[0]);
         $name = $this->compileStringContent($string);
 
-        return $this->has($name) ? self::$true : self::$false;
+        return $this->has($name);
     }
 
     /**
@@ -4422,11 +5175,12 @@ class Compiler
      */
     protected function libCounter($args)
     {
-        $list = array_map(array($this, 'compileValue'), $args);
+        $list = array_map([$this, 'compileValue'], $args);
 
-        return array('string', '', array('counter(' . implode(',', $list) . ')'));
+        return [Type::T_STRING, '', ['counter(' . implode(',', $list) . ')']];
     }
 
+    protected static $libRandom = ['limit'];
     protected function libRandom($args)
     {
         if (isset($args[0])) {
@@ -4434,12 +5188,14 @@ class Compiler
 
             if ($n < 1) {
                 $this->throwError("limit must be greater than or equal to 1");
+
+                return;
             }
 
-            return array('number', mt_rand(1, $n), '');
+            return new Node\Number(mt_rand(1, $n), '');
         }
 
-        return array('number', mt_rand(1, mt_getrandmax()), '');
+        return new Node\Number(mt_rand(1, mt_getrandmax()), '');
     }
 
     protected function libUniqueId()
@@ -4452,6 +5208,16 @@ class Compiler
 
         $id += mt_rand(0, 10) + 1;
 
-        return array('string', '', array('u' . str_pad(base_convert($id, 10, 36), 8, '0', STR_PAD_LEFT)));
+        return [Type::T_STRING, '', ['u' . str_pad(base_convert($id, 10, 36), 8, '0', STR_PAD_LEFT)]];
+    }
+
+    protected static $libInspect = ['value'];
+    protected function libInspect($args)
+    {
+        if ($args[0] === self::$null) {
+            return [Type::T_KEYWORD, 'null'];
+        }
+
+        return $args[0];
     }
 }
diff --git a/wcfsetup/install/files/lib/system/style/scssphp/src/Compiler/Environment.php b/wcfsetup/install/files/lib/system/style/scssphp/src/Compiler/Environment.php
new file mode 100644 (file)
index 0000000..d44bff7
--- /dev/null
@@ -0,0 +1,40 @@
+<?php
+/**
+ * SCSSPHP
+ *
+ * @copyright 2012-2015 Leaf Corcoran
+ *
+ * @license http://opensource.org/licenses/MIT MIT
+ *
+ * @link http://leafo.github.io/scssphp
+ */
+
+namespace Leafo\ScssPhp\Compiler;
+
+/**
+ * Compiler environment
+ *
+ * @author Anthon Pang <anthon.pang@gmail.com>
+ */
+class Environment
+{
+    /**
+     * @var \Leafo\ScssPhp\Block
+     */
+    public $block;
+
+    /**
+     * @var \Leafo\ScssPhp\Compiler\Environment
+     */
+    public $parent;
+
+    /**
+     * @var array
+     */
+    public $store;
+
+    /**
+     * @var integer
+     */
+    public $depth;
+}
diff --git a/wcfsetup/install/files/lib/system/style/scssphp/src/Exception/CompilerException.php b/wcfsetup/install/files/lib/system/style/scssphp/src/Exception/CompilerException.php
new file mode 100644 (file)
index 0000000..777e40a
--- /dev/null
@@ -0,0 +1,21 @@
+<?php
+/**
+ * SCSSPHP
+ *
+ * @copyright 2012-2015 Leaf Corcoran
+ *
+ * @license http://opensource.org/licenses/MIT MIT
+ *
+ * @link http://leafo.github.io/scssphp
+ */
+
+namespace Leafo\ScssPhp\Exception;
+
+/**
+ * Compiler exception
+ *
+ * @author Oleksandr Savchenko <traveltino@gmail.com>
+ */
+class CompilerException extends \Exception
+{
+}
diff --git a/wcfsetup/install/files/lib/system/style/scssphp/src/Exception/ParserException.php b/wcfsetup/install/files/lib/system/style/scssphp/src/Exception/ParserException.php
new file mode 100644 (file)
index 0000000..fbe6388
--- /dev/null
@@ -0,0 +1,21 @@
+<?php
+/**
+ * SCSSPHP
+ *
+ * @copyright 2012-2015 Leaf Corcoran
+ *
+ * @license http://opensource.org/licenses/MIT MIT
+ *
+ * @link http://leafo.github.io/scssphp
+ */
+
+namespace Leafo\ScssPhp\Exception;
+
+/**
+ * Parser Exception
+ *
+ * @author Oleksandr Savchenko <traveltino@gmail.com>
+ */
+class ParserException extends \Exception
+{
+}
diff --git a/wcfsetup/install/files/lib/system/style/scssphp/src/Exception/ServerException.php b/wcfsetup/install/files/lib/system/style/scssphp/src/Exception/ServerException.php
new file mode 100644 (file)
index 0000000..5a878d2
--- /dev/null
@@ -0,0 +1,21 @@
+<?php
+/**
+ * SCSSPHP
+ *
+ * @copyright 2012-2015 Leaf Corcoran
+ *
+ * @license http://opensource.org/licenses/MIT MIT
+ *
+ * @link http://leafo.github.io/scssphp
+ */
+
+namespace Leafo\ScssPhp\Exception;
+
+/**
+ * Server Exception
+ *
+ * @author Anthon Pang <anthon.pang@gmail.com>
+ */
+class ServerException extends \Exception
+{
+}
index 5b035e1c2b0088a07fea808ea66fe9886386c80c..2770bb2daa037f5fcd2d2a4b48838473e9989a68 100644 (file)
 
 namespace Leafo\ScssPhp;
 
+use Leafo\ScssPhp\Formatter\OutputBlock;
+
 /**
- * SCSS base formatter
+ * Base formatter
  *
  * @author Leaf Corcoran <leafot@gmail.com>
  */
@@ -53,23 +55,33 @@ abstract class Formatter
      */
     public $assignSeparator;
 
+    /**
+     * @var boolea
+     */
+    public $keepSemicolons;
+
+    /**
+     * Initialize formatter
+     *
+     * @api
+     */
     abstract public function __construct();
 
     /**
      * Return indentation (whitespace)
      *
-     * @param integer $n
-     *
      * @return string
      */
-    protected function indentStr($n = 0)
+    protected function indentStr()
     {
-        return str_repeat($this->indentChar, max($this->indentLevel + $n, 0));
+        return '';
     }
 
     /**
      * Return property assignment
      *
+     * @api
+     *
      * @param string $name
      * @param mixed  $value
      *
@@ -83,21 +95,34 @@ abstract class Formatter
     /**
      * Strip semi-colon appended by property(); it's a separator, not a terminator
      *
+     * @api
+     *
      * @param array $lines
      */
     public function stripSemicolon(&$lines)
     {
+        if ($this->keepSemicolons) {
+            return;
+        }
+
+        if (($count = count($lines))
+            && substr($lines[$count - 1], -1) === ';'
+        ) {
+            $lines[$count - 1] = substr($lines[$count - 1], 0, -1);
+        }
     }
 
     /**
      * Output lines inside a block
      *
-     * @param string    $inner
-     * @param \stdClass $block
+     * @param \Leafo\ScssPhp\Formatter\OutputBlock $block
      */
-    protected function blockLines($inner, $block)
+    protected function blockLines(OutputBlock $block)
     {
+        $inner = $this->indentStr();
+
         $glue = $this->break . $inner;
+
         echo $inner . implode($glue, $block->lines);
 
         if (! empty($block->children)) {
@@ -105,35 +130,57 @@ abstract class Formatter
         }
     }
 
+    /**
+     * Output block selectors
+     *
+     * @param \Leafo\ScssPhp\Formatter\OutputBlock $block
+     */
+    protected function blockSelectors(OutputBlock $block)
+    {
+        $inner = $this->indentStr();
+
+        echo $inner
+            . implode($this->tagSeparator, $block->selectors)
+            . $this->open . $this->break;
+    }
+
+    /**
+     * Output block children
+     *
+     * @param \Leafo\ScssPhp\Formatter\OutputBlock $block
+     */
+    protected function blockChildren(OutputBlock $block)
+    {
+        foreach ($block->children as $child) {
+            $this->block($child);
+        }
+    }
+
     /**
      * Output non-empty block
      *
-     * @param \stdClass $block
+     * @param \Leafo\ScssPhp\Formatter\OutputBlock $block
      */
-    protected function block($block)
+    protected function block(OutputBlock $block)
     {
         if (empty($block->lines) && empty($block->children)) {
             return;
         }
 
-        $inner = $pre = $this->indentStr();
+        $pre = $this->indentStr();
 
         if (! empty($block->selectors)) {
-            echo $pre
-                . implode($this->tagSeparator, $block->selectors)
-                . $this->open . $this->break;
+            $this->blockSelectors($block);
 
             $this->indentLevel++;
-
-            $inner = $this->indentStr();
         }
 
         if (! empty($block->lines)) {
-            $this->blockLines($inner, $block);
+            $this->blockLines($block);
         }
 
-        foreach ($block->children as $child) {
-            $this->block($child);
+        if (! empty($block->children)) {
+            $this->blockChildren($block);
         }
 
         if (! empty($block->selectors)) {
@@ -150,11 +197,13 @@ abstract class Formatter
     /**
      * Entry point to formatting a block
      *
-     * @param \stdClass $block An abstract syntax tree
+     * @api
+     *
+     * @param \Leafo\ScssPhp\Formatter\OutputBlock $block An abstract syntax tree
      *
      * @return string
      */
-    public function format($block)
+    public function format(OutputBlock $block)
     {
         ob_start();
 
index 29874c96c2e2c63e75615ef5e589c894784fe9e5..94abe329504c1974f5a4c08712e212845f5e5a0b 100644 (file)
@@ -14,7 +14,7 @@ namespace Leafo\ScssPhp\Formatter;
 use Leafo\ScssPhp\Formatter;
 
 /**
- * SCSS compact formatter
+ * Compact formatter
  *
  * @author Leaf Corcoran <leafot@gmail.com>
  */
@@ -32,12 +32,13 @@ class Compact extends Formatter
         $this->close = "}\n\n";
         $this->tagSeparator = ',';
         $this->assignSeparator = ':';
+        $this->keepSemicolons = true;
     }
 
     /**
      * {@inheritdoc}
      */
-    public function indentStr($n = 0)
+    public function indentStr()
     {
         return ' ';
     }
index c95b68b3246c5b873edc65a1f81eddb0e7e70b83..df7bc75fb38ac3bb2f68a2eaea30fd0dc33841e9 100644 (file)
 namespace Leafo\ScssPhp\Formatter;
 
 use Leafo\ScssPhp\Formatter;
+use Leafo\ScssPhp\Formatter\OutputBlock;
 
 /**
- * SCSS compressed formatter
+ * Compressed formatter
  *
  * @author Leaf Corcoran <leafot@gmail.com>
  */
@@ -32,33 +33,16 @@ class Compressed extends Formatter
         $this->close = '}';
         $this->tagSeparator = ',';
         $this->assignSeparator = ':';
+        $this->keepSemicolons = false;
     }
 
     /**
      * {@inheritdoc}
      */
-    public function indentStr($n = 0)
+    public function blockLines(OutputBlock $block)
     {
-        return '';
-    }
+        $inner = $this->indentStr();
 
-    /**
-     * {@inheritdoc}
-     */
-    public function stripSemicolon(&$lines)
-    {
-        if (($count = count($lines))
-            && substr($lines[$count - 1], -1) === ';'
-        ) {
-            $lines[$count - 1] = substr($lines[$count - 1], 0, -1);
-        }
-    }
-
-    /**
-     * {@inheritdoc}
-     */
-    public function blockLines($inner, $block)
-    {
         $glue = $this->break . $inner;
 
         foreach ($block->lines as $index => $line) {
@@ -75,15 +59,4 @@ class Compressed extends Formatter
             echo $this->break;
         }
     }
-
-    /**
-     * {@inherit}
-     */
-    public function format($block)
-    {
-        return parent::format($block);
-
-        // TODO: we need to fix the 2 "compressed" tests where the "close" is applied
-        return trim(str_replace(';}', '}', parent::format($block)));
-    }
 }
index 1227c12279c3a925adf422dff01f1b4fa2332729..ccba1333bf6d6fccce73ea7b069fad6f2277e599 100644 (file)
 namespace Leafo\ScssPhp\Formatter;
 
 use Leafo\ScssPhp\Formatter;
+use Leafo\ScssPhp\Formatter\OutputBlock;
 
 /**
- * SCSS crunched formatter
+ * Crunched formatter
  *
  * @author Anthon Pang <anthon.pang@gmail.com>
  */
@@ -32,33 +33,16 @@ class Crunched extends Formatter
         $this->close = '}';
         $this->tagSeparator = ',';
         $this->assignSeparator = ':';
+        $this->keepSemicolons = false;
     }
 
     /**
      * {@inheritdoc}
      */
-    public function indentStr($n = 0)
+    public function blockLines(OutputBlock $block)
     {
-        return '';
-    }
-
-    /**
-     * {@inheritdoc}
-     */
-    public function stripSemicolon(&$lines)
-    {
-        if (($count = count($lines))
-            && substr($lines[$count - 1], -1) === ';'
-        ) {
-            $lines[$count - 1] = substr($lines[$count - 1], 0, -1);
-        }
-    }
+        $inner = $this->indentStr();
 
-    /**
-     * {@inheritdoc}
-     */
-    public function blockLines($inner, $block)
-    {
         $glue = $this->break . $inner;
 
         foreach ($block->lines as $index => $line) {
diff --git a/wcfsetup/install/files/lib/system/style/scssphp/src/Formatter/Debug.php b/wcfsetup/install/files/lib/system/style/scssphp/src/Formatter/Debug.php
new file mode 100644 (file)
index 0000000..855742e
--- /dev/null
@@ -0,0 +1,119 @@
+<?php
+/**
+ * SCSSPHP
+ *
+ * @copyright 2012-2015 Leaf Corcoran
+ *
+ * @license http://opensource.org/licenses/MIT MIT
+ *
+ * @link http://leafo.github.io/scssphp
+ */
+
+namespace Leafo\ScssPhp\Formatter;
+
+use Leafo\ScssPhp\Formatter;
+use Leafo\ScssPhp\Formatter\OutputBlock;
+
+/**
+ * Debug formatter
+ *
+ * @author Anthon Pang <anthon.pang@gmail.com>
+ */
+class Debug extends Formatter
+{
+    /**
+     * {@inheritdoc}
+     */
+    public function __construct()
+    {
+        $this->indentLevel = 0;
+        $this->indentChar = '';
+        $this->break = "\n";
+        $this->open = ' {';
+        $this->close = ' }';
+        $this->tagSeparator = ', ';
+        $this->assignSeparator = ': ';
+        $this->keepSemicolons = true;
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    protected function indentStr()
+    {
+        return str_repeat('  ', $this->indentLevel);
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    protected function blockLines(OutputBlock $block)
+    {
+        $indent = $this->indentStr();
+
+        if (empty($block->lines)) {
+            echo "{$indent}block->lines: []\n";
+
+            return;
+        }
+
+        foreach ($block->lines as $index => $line) {
+            echo "{$indent}block->lines[{$index}]: $line\n";
+        }
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    protected function blockSelectors(OutputBlock $block)
+    {
+        $indent = $this->indentStr();
+
+        if (empty($block->selectors)) {
+            echo "{$indent}block->selectors: []\n";
+
+            return;
+        }
+
+        foreach ($block->selectors as $index => $selector) {
+            echo "{$indent}block->selectors[{$index}]: $selector\n";
+        }
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    protected function blockChildren(OutputBlock $block)
+    {
+        $indent = $this->indentStr();
+
+        if (empty($block->children)) {
+            echo "{$indent}block->children: []\n";
+
+            return;
+        }
+
+        $this->indentLevel++;
+
+        foreach ($block->children as $i => $child) {
+            $this->block($child);
+        }
+
+        $this->indentLevel--;
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    protected function block(OutputBlock $block)
+    {
+        $indent = $this->indentStr();
+
+        echo "{$indent}block->type: {$block->type}\n" .
+             "{$indent}block->depth: {$block->depth}\n";
+
+        $this->blockSelectors($block);
+        $this->blockLines($block);
+        $this->blockChildren($block);
+    }
+}
index fbf85c1e1875af7f9bac917e30e5e92a5817afe0..54db742ffdb874cb23d4b70b9307d6f190e38ad5 100644 (file)
 namespace Leafo\ScssPhp\Formatter;
 
 use Leafo\ScssPhp\Formatter;
+use Leafo\ScssPhp\Formatter\OutputBlock;
 
 /**
- * SCSS expanded formatter
+ * Expanded formatter
  *
  * @author Leaf Corcoran <leafot@gmail.com>
  */
@@ -32,13 +33,24 @@ class Expanded extends Formatter
         $this->close = '}';
         $this->tagSeparator = ', ';
         $this->assignSeparator = ': ';
+        $this->keepSemicolons = true;
     }
 
     /**
      * {@inheritdoc}
      */
-    protected function blockLines($inner, $block)
+    protected function indentStr()
     {
+        return str_repeat($this->indentChar, $this->indentLevel);
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    protected function blockLines(OutputBlock $block)
+    {
+        $inner = $this->indentStr();
+
         $glue = $this->break . $inner;
 
         foreach ($block->lines as $index => $line) {
index bd2c4d213645b4f60b869ddd6acb1883f367ebe8..9fdb4dd0ab6b10a34932b2fa1928b349d1723014 100644 (file)
 namespace Leafo\ScssPhp\Formatter;
 
 use Leafo\ScssPhp\Formatter;
+use Leafo\ScssPhp\Formatter\OutputBlock;
 
 /**
- * SCSS nested formatter
+ * Nested formatter
  *
  * @author Leaf Corcoran <leafot@gmail.com>
  */
 class Nested extends Formatter
 {
+    /**
+     * @var integer
+     */
+    private $depth;
+
     /**
      * {@inheritdoc}
      */
@@ -32,55 +38,26 @@ class Nested extends Formatter
         $this->close = ' }';
         $this->tagSeparator = ', ';
         $this->assignSeparator = ': ';
+        $this->keepSemicolons = true;
     }
 
     /**
-     * Adjust the depths of all children, depth first
-     *
-     * @param \stdClass $block
+     * {@inheritdoc}
      */
-    public function adjustAllChildren($block)
+    protected function indentStr()
     {
-        // flatten empty nested blocks
-        $children = array();
-        foreach ($block->children as $i => $child) {
-            if (empty($child->lines) && empty($child->children)) {
-                if (isset($block->children[$i + 1])) {
-                    $block->children[$i + 1]->depth = $child->depth;
-                }
-                continue;
-            }
-            $children[] = $child;
-        }
-
-        $count = count($children);
-        for ($i = 0; $i < $count; $i++) {
-            $depth = $children[$i]->depth;
-            $j = $i + 1;
-            if (isset($children[$j]) && $depth < $children[$j]->depth) {
-                $childDepth = $children[$j]->depth;
-                for (; $j < $count; $j++) {
-                    if ($depth < $children[$j]->depth && $childDepth >= $children[$j]->depth) {
-                        $children[$j]->depth = $depth + 1;
-                    }
-                }
-            }
-        }
-
-        $block->children = $children;
+        $n = $this->depth - 1;
 
-        // make relative to parent
-        foreach ($block->children as $child) {
-            $this->adjustAllChildren($child);
-            $child->depth = $child->depth - $block->depth;
-        }
+        return str_repeat($this->indentChar, max($this->indentLevel + $n, 0));
     }
 
     /**
      * {@inheritdoc}
      */
-    protected function blockLines($inner, $block)
+    protected function blockLines(OutputBlock $block)
     {
+        $inner = $this->indentStr();
+
         $glue = $this->break . $inner;
 
         foreach ($block->lines as $index => $line) {
@@ -99,25 +76,20 @@ class Nested extends Formatter
     /**
      * {@inheritdoc}
      */
-    protected function block($block)
+    protected function blockSelectors(OutputBlock $block)
     {
-        if ($block->type === 'root') {
-            $this->adjustAllChildren($block);
-        }
-
-        $inner = $pre = $this->indentStr($block->depth - 1);
-        if (! empty($block->selectors)) {
-            echo $pre .
-                implode($this->tagSeparator, $block->selectors) .
-                $this->open . $this->break;
-            $this->indentLevel++;
-            $inner = $this->indentStr($block->depth - 1);
-        }
+        $inner = $this->indentStr();
 
-        if (! empty($block->lines)) {
-            $this->blockLines($inner, $block);
-        }
+        echo $inner
+            . implode($this->tagSeparator, $block->selectors)
+            . $this->open . $this->break;
+    }
 
+    /**
+     * {@inheritdoc}
+     */
+    protected function blockChildren(OutputBlock $block)
+    {
         foreach ($block->children as $i => $child) {
             $this->block($child);
 
@@ -126,15 +98,47 @@ class Nested extends Formatter
 
                 if (isset($block->children[$i + 1])) {
                     $next = $block->children[$i + 1];
+
                     if ($next->depth === max($block->depth, 1) && $child->depth >= $next->depth) {
                         echo $this->break;
                     }
                 }
             }
         }
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    protected function block(OutputBlock $block)
+    {
+        if ($block->type === 'root') {
+            $this->adjustAllChildren($block);
+        }
+
+        if (empty($block->lines) && empty($block->children)) {
+            return;
+        }
+
+        $this->depth = $block->depth;
+
+        if (! empty($block->selectors)) {
+            $this->blockSelectors($block);
+
+            $this->indentLevel++;
+        }
+
+        if (! empty($block->lines)) {
+            $this->blockLines($block);
+        }
+
+        if (! empty($block->children)) {
+            $this->blockChildren($block);
+        }
 
         if (! empty($block->selectors)) {
             $this->indentLevel--;
+
             echo $this->close;
         }
 
@@ -142,4 +146,53 @@ class Nested extends Formatter
             echo $this->break;
         }
     }
+
+    /**
+     * Adjust the depths of all children, depth first
+     *
+     * @param \Leafo\ScssPhp\Formatter\OutputBlock $block
+     */
+    private function adjustAllChildren(OutputBlock $block)
+    {
+        // flatten empty nested blocks
+        $children = [];
+
+        foreach ($block->children as $i => $child) {
+            if (empty($child->lines) && empty($child->children)) {
+                if (isset($block->children[$i + 1])) {
+                    $block->children[$i + 1]->depth = $child->depth;
+                }
+
+                continue;
+            }
+
+            $children[] = $child;
+        }
+
+        $count = count($children);
+
+        for ($i = 0; $i < $count; $i++) {
+            $depth = $children[$i]->depth;
+            $j = $i + 1;
+
+            if (isset($children[$j]) && $depth < $children[$j]->depth) {
+                $childDepth = $children[$j]->depth;
+
+                for (; $j < $count; $j++) {
+                    if ($depth < $children[$j]->depth && $childDepth >= $children[$j]->depth) {
+                        $children[$j]->depth = $depth + 1;
+                    }
+                }
+            }
+        }
+
+        $block->children = $children;
+
+        // make relative to parent
+        foreach ($block->children as $child) {
+            $this->adjustAllChildren($child);
+
+            $child->depth = $child->depth - $block->depth;
+        }
+    }
 }
diff --git a/wcfsetup/install/files/lib/system/style/scssphp/src/Formatter/OutputBlock.php b/wcfsetup/install/files/lib/system/style/scssphp/src/Formatter/OutputBlock.php
new file mode 100644 (file)
index 0000000..bb8d99b
--- /dev/null
@@ -0,0 +1,50 @@
+<?php
+/**
+ * SCSSPHP
+ *
+ * @copyright 2012-2015 Leaf Corcoran
+ *
+ * @license http://opensource.org/licenses/MIT MIT
+ *
+ * @link http://leafo.github.io/scssphp
+ */
+
+namespace Leafo\ScssPhp\Formatter;
+
+/**
+ * Output block
+ *
+ * @author Anthon Pang <anthon.pang@gmail.com>
+ */
+class OutputBlock
+{
+    /**
+     * @var string
+     */
+    public $type;
+
+    /**
+     * @var integer
+     */
+    public $depth;
+
+    /**
+     * @var array
+     */
+    public $selectors;
+
+    /**
+     * @var array
+     */
+    public $lines;
+
+    /**
+     * @var array
+     */
+    public $children;
+
+    /**
+     * @var \Leafo\ScssPhp\Formatter\OutputBlock
+     */
+    public $parent;
+}
diff --git a/wcfsetup/install/files/lib/system/style/scssphp/src/Node.php b/wcfsetup/install/files/lib/system/style/scssphp/src/Node.php
new file mode 100644 (file)
index 0000000..e6ed178
--- /dev/null
@@ -0,0 +1,40 @@
+<?php
+/**
+ * SCSSPHP
+ *
+ * @copyright 2012-2015 Leaf Corcoran
+ *
+ * @license http://opensource.org/licenses/MIT MIT
+ *
+ * @link http://leafo.github.io/scssphp
+ */
+
+namespace Leafo\ScssPhp;
+
+/**
+ * Base node
+ *
+ * @author Anthon Pang <anthon.pang@gmail.com>
+ */
+abstract class Node
+{
+    /**
+     * @var string
+     */
+    public $type;
+
+    /**
+     * @var integer
+     */
+    public $sourceIndex;
+
+    /**
+     * @var integer
+     */
+    public $sourceLine;
+
+    /**
+     * @var integer
+     */
+    public $sourceColumn;
+}
diff --git a/wcfsetup/install/files/lib/system/style/scssphp/src/Node/Number.php b/wcfsetup/install/files/lib/system/style/scssphp/src/Node/Number.php
new file mode 100644 (file)
index 0000000..a803a6c
--- /dev/null
@@ -0,0 +1,329 @@
+<?php
+/**
+ * SCSSPHP
+ *
+ * @copyright 2012-2015 Leaf Corcoran
+ *
+ * @license http://opensource.org/licenses/MIT MIT
+ *
+ * @link http://leafo.github.io/scssphp
+ */
+
+namespace Leafo\ScssPhp\Node;
+
+use Leafo\ScssPhp\Compiler;
+use Leafo\ScssPhp\Node;
+use Leafo\ScssPhp\Type;
+
+/**
+ * Dimension + optional units
+ *
+ * {@internal
+ *     This is a work-in-progress.
+ *
+ *     The \ArrayAccess interface is temporary until the migration is complete.
+ * }}
+ *
+ * @author Anthon Pang <anthon.pang@gmail.com>
+ */
+class Number extends Node implements \ArrayAccess
+{
+    /**
+     * @var integer
+     */
+    static public $precision = 5;
+
+    /**
+     * @see http://www.w3.org/TR/2012/WD-css3-values-20120308/
+     *
+     * @var array
+     */
+    static protected $unitTable = [
+        'in' => [
+            'in' => 1,
+            'pc' => 6,
+            'pt' => 72,
+            'px' => 96,
+            'cm' => 2.54,
+            'mm' => 25.4,
+            'q'  => 101.6,
+        ],
+        'turn' => [
+            'deg'  => 360,
+            'grad' => 400,
+            'rad'  => 6.28318530717958647692528676, // 2 * M_PI
+            'turn' => 1,
+        ],
+        's' => [
+            's'  => 1,
+            'ms' => 1000,
+        ],
+        'Hz' => [
+            'Hz'  => 1,
+            'kHz' => 0.001,
+        ],
+        'dpi' => [
+            'dpi'  => 1,
+            'dpcm' => 2.54,
+            'dppx' => 96,
+        ],
+    ];
+
+    /**
+     * @var integer|float
+     */
+    public $dimension;
+
+    /**
+     * @var array
+     */
+    public $units;
+
+    /**
+     * Initialize number
+     *
+     * @param mixed $dimension
+     * @param mixed $initialUnit
+     */
+    public function __construct($dimension, $initialUnit)
+    {
+        $this->type      = Type::T_NUMBER;
+        $this->dimension = $dimension;
+        $this->units     = is_array($initialUnit)
+            ? $initialUnit
+            : ($initialUnit ? [$initialUnit => 1]
+                            : []);
+    }
+
+    /**
+     * Coerce number to target units
+     *
+     * @param array $units
+     *
+     * @return \Leafo\ScssPhp\Node\Number
+     */
+    public function coerce($units)
+    {
+        if ($this->unitless()) {
+            return new Number($this->dimension, $units);
+        }
+
+        $dimension = $this->dimension;
+
+        foreach (self::$unitTable['in'] 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);
+    }
+
+    /**
+     * Normalize number
+     *
+     * @return \Leafo\ScssPhp\Node\Number
+     */
+    public function normalize()
+    {
+        $dimension = $this->dimension;
+        $units     = [];
+
+        $this->normalizeUnits($dimension, $units, 'in');
+
+        return new Number($dimension, $units);
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function offsetExists($offset)
+    {
+        if ($offset === -3) {
+            return $this->sourceColumn !== null;
+        }
+
+        if ($offset === -2) {
+            return $this->sourceLine !== null;
+        }
+
+        if ($offset === -1
+            || $offset === 0
+            || $offset === 1
+            || $offset === 2
+        ) {
+            return true;
+        }
+
+        return false;
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function offsetGet($offset)
+    {
+        switch ($offset) {
+            case -3:
+                return $this->sourceColumn;
+
+            case -2:
+                return $this->sourceLine;
+
+            case -1:
+                return $this->sourceIndex;
+
+            case 0:
+                return $this->type;
+
+            case 1:
+                return $this->dimension;
+
+            case 2:
+                return $this->units;
+        }
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    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;
+        }
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    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;
+        }
+    }
+
+    /**
+     * Returns true if the number is unitless
+     *
+     * @return boolean
+     */
+    public function unitless()
+    {
+        return ! array_sum($this->units);
+    }
+
+    /**
+     * Returns unit(s) as the product of numerator units divided by the product of denominator units
+     *
+     * @return string
+     */
+    public function unitStr()
+    {
+        $numerators   = [];
+        $denominators = [];
+
+        foreach ($this->units as $unit => $unitSize) {
+            if ($unitSize > 0) {
+                $numerators = array_pad($numerators, count($numerators) + $unitSize, $unit);
+                continue;
+            }
+
+            if ($unitSize < 0) {
+                $denominators = array_pad($denominators, count($denominators) + $unitSize, $unit);
+                continue;
+            }
+        }
+
+        return implode('*', $numerators) . (count($denominators) ? '/' . implode('*', $denominators) : '');
+    }
+
+    /**
+     * Output number
+     *
+     * @param \Leafo\ScssPhp\Compiler $compiler
+     *
+     * @return string
+     */
+    public function output(Compiler $compiler = null)
+    {
+        $dimension = round($this->dimension, self::$precision);
+
+        $units = array_filter($this->units, function ($unitSize) {
+            return $unitSize;
+        });
+
+        if (count($units) > 1 && array_sum($units) === 0) {
+            $dimension = $this->dimension;
+            $units     = [];
+
+            $this->normalizeUnits($dimension, $units, 'in');
+
+            $dimension = round($dimension, self::$precision);
+            $units     = array_filter($units, function ($unitSize) {
+                return $unitSize;
+            });
+        }
+
+        $unitSize = array_sum($units);
+
+        if ($compiler && ($unitSize > 1 || $unitSize < 0 || count($units) > 1)) {
+            $compiler->throwError((string) $dimension . $this->unitStr() . " isn't a valid CSS value.");
+        }
+
+        reset($units);
+        list($unit, ) = each($units);
+
+        return (string) $dimension . $unit;
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function __toString()
+    {
+        return $this->output();
+    }
+
+    /**
+     * Normalize units
+     *
+     * @param integer|float $dimension
+     * @param array         $units
+     * @param string        $baseUnit
+     */
+    private function normalizeUnits(&$dimension, &$units, $baseUnit = 'in')
+    {
+        $dimension = $this->dimension;
+        $units     = [];
+
+        foreach ($this->units as $unit => $exp) {
+            if (isset(self::$unitTable[$baseUnit][$unit])) {
+                $factor = pow(self::$unitTable[$baseUnit][$unit], $exp);
+
+                $unit = $baseUnit;
+                $dimension /= $factor;
+            }
+
+            $units[$unit] = $exp + (isset($units[$unit]) ? $units[$unit] : 0);
+        }
+    }
+}
index 8b27a27e4ec0c1e540b0fa13fab6faace3d9c572..ed0621eae1b9d5682a3753b7577a51a9ddd5e82f 100644 (file)
 
 namespace Leafo\ScssPhp;
 
+use Leafo\ScssPhp\Block;
 use Leafo\ScssPhp\Compiler;
+use Leafo\ScssPhp\Exception\ParserException;
+use Leafo\ScssPhp\Node;
+use Leafo\ScssPhp\Type;
 
 /**
- * SCSS parser
+ * Parser
  *
  * @author Leaf Corcoran <leafot@gmail.com>
  */
 class Parser
 {
-    const SOURCE_POSITION = -1;
-    const SOURCE_PARSER   = -2;
+    const SOURCE_INDEX  = -1;
+    const SOURCE_LINE   = -2;
+    const SOURCE_COLUMN = -3;
 
     /**
      * @var array
      */
-    protected static $precedence = array(
-        '=' => 0,
-        'or' => 1,
+    protected static $precedence = [
+        '='   => 0,
+        'or'  => 1,
         'and' => 2,
-        '==' => 3,
-        '!=' => 3,
-        '<=' => 4,
-        '>=' => 4,
-        '<' => 4,
-        '>' => 4,
-        '+' => 5,
-        '-' => 5,
-        '*' => 6,
-        '/' => 6,
-        '%' => 6,
-    );
-
-    /**
-     * @var array
-     */
-    protected static $operators = array(
-        '+',
-        '-',
-        '*',
-        '/',
-        '%',
-        '==',
-        '!=',
-        '<=',
-        '>=',
-        '<',
-        '>',
-        'and',
-        'or',
-    );
-
-    protected static $operatorStr;
+        '=='  => 3,
+        '!='  => 3,
+        '<=>' => 3,
+        '<='  => 4,
+        '>='  => 4,
+        '<'   => 4,
+        '>'   => 4,
+        '+'   => 5,
+        '-'   => 5,
+        '*'   => 6,
+        '/'   => 6,
+        '%'   => 6,
+    ];
+
+    protected static $commentPattern;
+    protected static $operatorPattern;
     protected static $whitePattern;
-    protected static $commentMulti;
-
-    protected static $commentSingle = '//';
-    protected static $commentMultiLeft = '/*';
-    protected static $commentMultiRight = '*/';
 
     private $sourceName;
-    private $rootParser;
+    private $sourceIndex;
+    private $sourcePositions;
     private $charset;
     private $count;
     private $env;
     private $inParens;
     private $eatWhiteDefault;
     private $buffer;
+    private $utf8;
+    private $encoding;
+    private $patternModifiers;
 
     /**
      * Constructor
      *
+     * @api
+     *
      * @param string  $sourceName
-     * @param boolean $rootParser
+     * @param integer $sourceIndex
+     * @param string  $encoding
      */
-    public function __construct($sourceName = null, $rootParser = true)
+    public function __construct($sourceName, $sourceIndex = 0, $encoding = 'utf-8')
     {
-        $this->sourceName = $sourceName;
-        $this->rootParser = $rootParser;
-        $this->charset = null;
+        $this->sourceName       = $sourceName ?: '(stdin)';
+        $this->sourceIndex      = $sourceIndex;
+        $this->charset          = null;
+        $this->utf8             = ! $encoding || strtolower($encoding) === 'utf-8';
+        $this->patternModifiers = $this->utf8 ? 'Aisu' : 'Ais';
 
-        if (empty(self::$operatorStr)) {
-            self::$operatorStr = $this->makeOperatorStr(self::$operators);
+        if (empty(self::$operatorPattern)) {
+            self::$operatorPattern = '([*\/%+-]|[!=]\=|\>\=?|\<\=\>|\<\=?|and|or)';
 
-            $commentSingle = $this->pregQuote(self::$commentSingle);
-            $commentMultiLeft = $this->pregQuote(self::$commentMultiLeft);
-            $commentMultiRight = $this->pregQuote(self::$commentMultiRight);
-            self::$commentMulti = $commentMultiLeft . '.*?' . $commentMultiRight;
-            self::$whitePattern = '/' . $commentSingle . '[^\n]*\s*|(' . self::$commentMulti . ')\s*|\s+/Ais';
+            $commentSingle      = '\/\/';
+            $commentMultiLeft   = '\/\*';
+            $commentMultiRight  = '\*\/';
+
+            self::$commentPattern = $commentMultiLeft . '.*?' . $commentMultiRight;
+            self::$whitePattern = $this->utf8
+                ? '/' . $commentSingle . '[^\n]*\s*|(' . self::$commentPattern . ')\s*|\s+/AisuS'
+                : '/' . $commentSingle . '[^\n]*\s*|(' . self::$commentPattern . ')\s*|\s+/AisS';
         }
     }
 
     /**
-     * Make operator regex
+     * Get source file name
      *
-     * @param array $operators
+     * @api
      *
      * @return string
      */
-    protected static function makeOperatorStr($operators)
+    public function getSourceName()
+    {
+        return $this->sourceName;
+    }
+
+    /**
+     * Throw parser error
+     *
+     * @api
+     *
+     * @param string $msg
+     *
+     * @throws \Leafo\ScssPhp\Exception\ParserException
+     */
+    public function throwParseError($msg = 'parse error')
     {
-        return '('
-            . implode('|', array_map(array('Leafo\ScssPhp\Parser', 'pregQuote'), $operators))
-            . ')';
+        list($line, /* $column */) = $this->getSourcePosition($this->count);
+
+        $loc = empty($this->sourceName) ? "line: $line" : "$this->sourceName on line $line";
+
+        if ($this->peek("(.*?)(\n|$)", $m, $this->count)) {
+            throw new ParserException("$msg: failed at `$m[1]` $loc");
+        }
+
+        throw new ParserException("$msg: $loc");
     }
 
     /**
      * Parser buffer
      *
-     * @param string $buffer;
+     * @api
      *
-     * @return \stdClass
+     * @param string $buffer
+     *
+     * @return \Leafo\ScssPhp\Block
      */
     public function parse($buffer)
     {
+        // strip BOM (byte order marker)
+        if (substr($buffer, 0, 3) === "\xef\xbb\xbf") {
+            $buffer = substr($buffer, 3);
+        }
+
+        $this->buffer          = rtrim($buffer, "\x00..\x1f");
         $this->count           = 0;
         $this->env             = null;
         $this->inParens        = false;
         $this->eatWhiteDefault = true;
-        $this->buffer          = $buffer;
 
-        $this->pushBlock(null); // root block
+        $this->saveEncoding();
+        $this->extractLineNumbers($buffer);
 
+        $this->pushBlock(null); // root block
         $this->whitespace();
         $this->pushBlock(null);
         $this->popBlock();
 
-        while (false !== $this->parseChunk()) {
+        while ($this->parseChunk()) {
             ;
         }
 
@@ -155,12 +179,16 @@ class Parser
 
         $this->env->isRoot    = true;
 
+        $this->restoreEncoding();
+
         return $this->env;
     }
 
     /**
      * Parse a value or value list
      *
+     * @api
+     *
      * @param string $buffer
      * @param string $out
      *
@@ -174,12 +202,20 @@ class Parser
         $this->eatWhiteDefault = true;
         $this->buffer          = (string) $buffer;
 
-        return $this->valueList($out);
+        $this->saveEncoding();
+
+        $list = $this->valueList($out);
+
+        $this->restoreEncoding();
+
+        return $list;
     }
 
     /**
      * Parse a selector or selector list
      *
+     * @api
+     *
      * @param string $buffer
      * @param string $out
      *
@@ -193,7 +229,13 @@ class Parser
         $this->eatWhiteDefault = true;
         $this->buffer          = (string) $buffer;
 
-        return $this->selectors($out);
+        $this->saveEncoding();
+
+        $selector = $this->selectors($out);
+
+        $this->restoreEncoding();
+
+        return $selector;
     }
 
     /**
@@ -241,8 +283,22 @@ class Parser
 
         // the directives
         if (isset($this->buffer[$this->count]) && $this->buffer[$this->count] === '@') {
+            if ($this->literal('@at-root') &&
+                ($this->selectors($selector) || true) &&
+                ($this->map($with) || true) &&
+                $this->literal('{')
+            ) {
+                $atRoot = $this->pushSpecialBlock(Type::T_AT_ROOT, $s);
+                $atRoot->selector = $selector;
+                $atRoot->with = $with;
+
+                return true;
+            }
+
+            $this->seek($s);
+
             if ($this->literal('@media') && $this->mediaQueryList($mediaQueryList) && $this->literal('{')) {
-                $media = $this->pushSpecialBlock('media', $s);
+                $media = $this->pushSpecialBlock(Type::T_MEDIA, $s);
                 $media->queryList = $mediaQueryList[2];
 
                 return true;
@@ -255,7 +311,7 @@ class Parser
                 ($this->argumentDef($args) || true) &&
                 $this->literal('{')
             ) {
-                $mixin = $this->pushSpecialBlock('mixin', $s);
+                $mixin = $this->pushSpecialBlock(Type::T_MIXIN, $s);
                 $mixin->name = $mixinName;
                 $mixin->args = $args;
 
@@ -272,11 +328,10 @@ class Parser
                 ($this->end() ||
                     $this->literal('{') && $hasBlock = true)
             ) {
-                $child = array('include',
-                    $mixinName, isset($argValues) ? $argValues : null, null);
+                $child = [Type::T_INCLUDE, $mixinName, isset($argValues) ? $argValues : null, null];
 
                 if (! empty($hasBlock)) {
-                    $include = $this->pushSpecialBlock('include', $s);
+                    $include = $this->pushSpecialBlock(Type::T_INCLUDE, $s);
                     $include->child = $child;
                 } else {
                     $this->append($child, $s);
@@ -287,11 +342,22 @@ class Parser
 
             $this->seek($s);
 
+            if ($this->literal('@scssphp-import-once') &&
+                $this->valueList($importPath) &&
+                $this->end()
+            ) {
+                $this->append([Type::T_SCSSPHP_IMPORT_ONCE, $importPath], $s);
+
+                return true;
+            }
+
+            $this->seek($s);
+
             if ($this->literal('@import') &&
                 $this->valueList($importPath) &&
                 $this->end()
             ) {
-                $this->append(array('import', $importPath), $s);
+                $this->append([Type::T_IMPORT, $importPath], $s);
 
                 return true;
             }
@@ -302,7 +368,7 @@ class Parser
                 $this->url($importPath) &&
                 $this->end()
             ) {
-                $this->append(array('import', $importPath), $s);
+                $this->append([Type::T_IMPORT, $importPath], $s);
 
                 return true;
             }
@@ -310,10 +376,12 @@ class Parser
             $this->seek($s);
 
             if ($this->literal('@extend') &&
-                $this->selectors($selector) &&
+                $this->selectors($selectors) &&
                 $this->end()
             ) {
-                $this->append(array('extend', $selector), $s);
+                // check for '!flag'
+                $optional = $this->stripOptionalFlag($selectors);
+                $this->append([Type::T_EXTEND, $selectors, $optional], $s);
 
                 return true;
             }
@@ -325,7 +393,7 @@ class Parser
                 $this->argumentDef($args) &&
                 $this->literal('{')
             ) {
-                $func = $this->pushSpecialBlock('function', $s);
+                $func = $this->pushSpecialBlock(Type::T_FUNCTION, $s);
                 $func->name = $fnName;
                 $func->args = $args;
 
@@ -334,8 +402,25 @@ class Parser
 
             $this->seek($s);
 
-            if ($this->literal('@return') && $this->valueList($retVal) && $this->end()) {
-                $this->append(array('return', $retVal), $s);
+            if ($this->literal('@break') && $this->end()) {
+                $this->append([Type::T_BREAK], $s);
+
+                return true;
+            }
+
+            $this->seek($s);
+
+            if ($this->literal('@continue') && $this->end()) {
+                $this->append([Type::T_CONTINUE], $s);
+
+                return true;
+            }
+
+            $this->seek($s);
+
+
+            if ($this->literal('@return') && ($this->valueList($retVal) || true) && $this->end()) {
+                $this->append([Type::T_RETURN, isset($retVal) ? $retVal : [Type::T_NULL]], $s);
 
                 return true;
             }
@@ -348,7 +433,7 @@ class Parser
                 $this->valueList($list) &&
                 $this->literal('{')
             ) {
-                $each = $this->pushSpecialBlock('each', $s);
+                $each = $this->pushSpecialBlock(Type::T_EACH, $s);
 
                 foreach ($varNames[2] as $varName) {
                     $each->vars[] = $varName[1];
@@ -365,7 +450,7 @@ class Parser
                 $this->expression($cond) &&
                 $this->literal('{')
             ) {
-                $while = $this->pushSpecialBlock('while', $s);
+                $while = $this->pushSpecialBlock(Type::T_WHILE, $s);
                 $while->cond = $cond;
 
                 return true;
@@ -382,7 +467,7 @@ class Parser
                 $this->expression($end) &&
                 $this->literal('{')
             ) {
-                $for = $this->pushSpecialBlock('for', $s);
+                $for = $this->pushSpecialBlock(Type::T_FOR, $s);
                 $for->var = $varName[1];
                 $for->start = $start;
                 $for->end = $end;
@@ -394,9 +479,9 @@ class Parser
             $this->seek($s);
 
             if ($this->literal('@if') && $this->valueList($cond) && $this->literal('{')) {
-                $if = $this->pushSpecialBlock('if', $s);
+                $if = $this->pushSpecialBlock(Type::T_IF, $s);
                 $if->cond = $cond;
-                $if->cases = array();
+                $if->cases = [];
 
                 return true;
             }
@@ -407,7 +492,7 @@ class Parser
                 $this->valueList($value) &&
                 $this->end()
             ) {
-                $this->append(array('debug', $value), $s);
+                $this->append([Type::T_DEBUG, $value], $s);
 
                 return true;
             }
@@ -418,7 +503,7 @@ class Parser
                 $this->valueList($value) &&
                 $this->end()
             ) {
-                $this->append(array('warn', $value), $s);
+                $this->append([Type::T_WARN, $value], $s);
 
                 return true;
             }
@@ -429,7 +514,7 @@ class Parser
                 $this->valueList($value) &&
                 $this->end()
             ) {
-                $this->append(array('error', $value), $s);
+                $this->append([Type::T_ERROR, $value], $s);
 
                 return true;
             }
@@ -437,7 +522,7 @@ class Parser
             $this->seek($s);
 
             if ($this->literal('@content') && $this->end()) {
-                $this->append(array('mixin_content'), $s);
+                $this->append([Type::T_MIXIN_CONTENT], $s);
 
                 return true;
             }
@@ -446,14 +531,14 @@ class Parser
 
             $last = $this->last();
 
-            if (isset($last) && $last[0] === 'if') {
+            if (isset($last) && $last[0] === Type::T_IF) {
                 list(, $if) = $last;
 
                 if ($this->literal('@else')) {
                     if ($this->literal('{')) {
-                        $else = $this->pushSpecialBlock('else', $s);
+                        $else = $this->pushSpecialBlock(Type::T_ELSE, $s);
                     } elseif ($this->literal('if') && $this->valueList($cond) && $this->literal('{')) {
-                        $else = $this->pushSpecialBlock('elseif', $s);
+                        $else = $this->pushSpecialBlock(Type::T_ELSEIF, $s);
                         $else->cond = $cond;
                     }
 
@@ -470,16 +555,17 @@ class Parser
 
             // only retain the first @charset directive encountered
             if ($this->literal('@charset') &&
-                $this->valueList($charset) && $this->end()
+                $this->valueList($charset) &&
+                $this->end()
             ) {
                 if (! isset($this->charset)) {
-                    $statement = array('charset', $charset);
+                    $statement = [Type::T_CHARSET, $charset];
 
-                    $statement[self::SOURCE_POSITION] = $s;
+                    list($line, $column) = $this->getSourcePosition($s);
 
-                    if (! $this->rootParser) {
-                        $statement[self::SOURCE_PARSER] = $this;
-                    }
+                    $statement[self::SOURCE_LINE]   = $line;
+                    $statement[self::SOURCE_COLUMN] = $column;
+                    $statement[self::SOURCE_INDEX]  = $this->sourceIndex;
 
                     $this->charset = $statement;
                 }
@@ -490,12 +576,17 @@ class Parser
             $this->seek($s);
 
             // doesn't match built in directive, do generic one
-            if ($this->literal('@', false) && $this->keyword($dirName) &&
+            if ($this->literal('@', false) &&
+                $this->keyword($dirName) &&
                 ($this->variable($dirValue) || $this->openString('{', $dirValue) || true) &&
                 $this->literal('{')
             ) {
-                $directive = $this->pushSpecialBlock('directive', $s);
-                $directive->name = $dirName;
+                if ($dirName === 'media') {
+                    $directive = $this->pushSpecialBlock(Type::T_MEDIA, $s);
+                } else {
+                    $directive = $this->pushSpecialBlock(Type::T_DIRECTIVE, $s);
+                    $directive->name = $dirName;
+                }
 
                 if (isset($dirValue)) {
                     $directive->value = $dirValue;
@@ -516,8 +607,8 @@ class Parser
             $this->valueList($value) &&
             $this->end()
         ) {
-            $name = array('string', '', array($name));
-            $this->append(array('assign', $name, $value), $s);
+            $name = [Type::T_STRING, '', [$name]];
+            $this->append([Type::T_ASSIGN, $name, $value], $s);
 
             return true;
         }
@@ -527,11 +618,12 @@ class Parser
         // variable assigns
         if ($this->variable($name) &&
             $this->literal(':') &&
-            $this->valueList($value) && $this->end()
+            $this->valueList($value) &&
+            $this->end()
         ) {
             // check for '!flag'
-            $assignmentFlag = $this->stripAssignmentFlag($value);
-            $this->append(array('assign', $name, $value, $assignmentFlag), $s);
+            $assignmentFlags = $this->stripAssignmentFlags($value);
+            $this->append([Type::T_ASSIGN, $name, $value, $assignmentFlags], $s);
 
             return true;
         }
@@ -545,7 +637,7 @@ class Parser
 
         // opening css block
         if ($this->selectors($selectors) && $this->literal('{')) {
-            $b = $this->pushBlock($selectors, $s);
+            $this->pushBlock($selectors, $s);
 
             return true;
         }
@@ -557,12 +649,12 @@ class Parser
             $foundSomething = false;
 
             if ($this->valueList($value)) {
-                $this->append(array('assign', $name, $value), $s);
+                $this->append([Type::T_ASSIGN, $name, $value], $s);
                 $foundSomething = true;
             }
 
             if ($this->literal('{')) {
-                $propBlock = $this->pushSpecialBlock('nestedprop', $s);
+                $propBlock = $this->pushSpecialBlock(Type::T_NESTED_PROPERTY, $s);
                 $propBlock->prefix = $name;
                 $foundSomething = true;
             } elseif ($foundSomething) {
@@ -580,14 +672,14 @@ class Parser
         if ($this->literal('}')) {
             $block = $this->popBlock();
 
-            if (isset($block->type) && $block->type === 'include') {
+            if (isset($block->type) && $block->type === Type::T_INCLUDE) {
                 $include = $block->child;
                 unset($block->child);
                 $include[3] = $block;
                 $this->append($include, $s);
             } elseif (empty($block->dontAppend)) {
-                $type = isset($block->type) ? $block->type : 'block';
-                $this->append(array($type, $block), $s);
+                $type = isset($block->type) ? $block->type : Type::T_BLOCK;
+                $this->append([$type, $block], $s);
             }
 
             return true;
@@ -603,90 +695,35 @@ class Parser
         return false;
     }
 
-    /**
-     * Strip assignment flag from the list
-     *
-     * @param array $value
-     *
-     * @return string
-     */
-    protected function stripAssignmentFlag(&$value)
-    {
-        $token = &$value;
-
-        for ($token = &$value; $token[0] === 'list' && ($s = count($token[2])); $token = &$lastNode) {
-            $lastNode = &$token[2][$s - 1];
-
-            if ($lastNode[0] === 'keyword' && in_array($lastNode[1], array('!default', '!global'))) {
-                array_pop($token[2]);
-
-                $token = $this->flattenList($token);
-
-                return $lastNode[1];
-            }
-        }
-
-        return false;
-    }
-
-    /**
-     * Match literal string
-     *
-     * @param string  $what
-     * @param boolean $eatWhitespace
-     *
-     * @return boolean
-     */
-    protected function literal($what, $eatWhitespace = null)
-    {
-        if (! isset($eatWhitespace)) {
-            $eatWhitespace = $this->eatWhiteDefault;
-        }
-
-        // shortcut on single letter
-        if (! isset($what[1]) && isset($this->buffer[$this->count])) {
-            if ($this->buffer[$this->count] === $what) {
-                if (! $eatWhitespace) {
-                    $this->count++;
-
-                    return true;
-                }
-                // goes below...
-            } else {
-                return false;
-            }
-        }
-
-        return $this->match($this->pregQuote($what), $m, $eatWhitespace);
-    }
-
     /**
      * Push block onto parse tree
      *
      * @param array   $selectors
      * @param integer $pos
      *
-     * @return \stdClass
+     * @return \Leafo\ScssPhp\Block
      */
     protected function pushBlock($selectors, $pos = 0)
     {
-        $b = new \stdClass;
-        $b->parent = $this->env; // not sure if we need this yet
+        list($line, $column) = $this->getSourcePosition($pos);
 
-        $b->sourcePosition = $pos;
-        $b->sourceParser = $this;
-        $b->selectors = $selectors;
-        $b->comments = array();
+        $b = new Block;
+        $b->sourceLine   = $line;
+        $b->sourceColumn = $column;
+        $b->sourceIndex  = $this->sourceIndex;
+        $b->selectors    = $selectors;
+        $b->comments     = [];
+        $b->parent       = $this->env;
 
         if (! $this->env) {
-            $b->children = array();
+            $b->children = [];
         } elseif (empty($this->env->children)) {
             $this->env->children = $this->env->comments;
-            $b->children = array();
-            $this->env->comments = array();
+            $b->children = [];
+            $this->env->comments = [];
         } else {
             $b->children = $this->env->comments;
-            $this->env->comments = array();
+            $this->env->comments = [];
         }
 
         $this->env = $b;
@@ -700,7 +737,7 @@ class Parser
      * @param string  $type
      * @param integer $pos
      *
-     * @return \stdClass
+     * @return \Leafo\ScssPhp\Block
      */
     protected function pushSpecialBlock($type, $pos)
     {
@@ -713,7 +750,7 @@ class Parser
     /**
      * Pop scope and return last block
      *
-     * @return \stdClass
+     * @return \Leafo\ScssPhp\Block
      *
      * @throws \Exception
      */
@@ -738,150 +775,310 @@ class Parser
     }
 
     /**
-     * Append comment to current block
+     * Peek input stream
      *
-     * @param array $comment
-     */
-    protected function appendComment($comment)
-    {
-        $comment[1] = substr(preg_replace(array('/^\s+/m', '/^(.)/m'), array('', ' \1'), $comment[1]), 1);
-
-        $this->env->comments[] = $comment;
-    }
-
-    /**
-     * Append statement to current block
+     * @param string  $regex
+     * @param array   $out
+     * @param integer $from
      *
-     * @param array   $statement
-     * @param integer $pos
+     * @return integer
      */
-    protected function append($statement, $pos = null)
+    protected function peek($regex, &$out, $from = null)
     {
-        if ($pos !== null) {
-            $statement[self::SOURCE_POSITION] = $pos;
-
-            if (! $this->rootParser) {
-                $statement[self::SOURCE_PARSER] = $this;
-            }
+        if (! isset($from)) {
+            $from = $this->count;
         }
 
-        $this->env->children[] = $statement;
-
-        $comments = $this->env->comments;
+        $r = '/' . $regex . '/' . $this->patternModifiers;
+        $result = preg_match($r, $this->buffer, $out, null, $from);
 
-        if (count($comments)) {
-            $this->env->children = array_merge($this->env->children, $comments);
-            $this->env->comments = array();
-        }
+        return $result;
     }
 
     /**
-     * Returns last child was appended
+     * Seek to position in input stream (or return current position in input stream)
      *
-     * @return array|null
+     * @param integer $where
+     *
+     * @return integer
      */
-    protected function last()
+    protected function seek($where = null)
     {
-        $i = count($this->env->children) - 1;
-
-        if (isset($this->env->children[$i])) {
-            return $this->env->children[$i];
+        if ($where === null) {
+            return $this->count;
         }
-    }
 
-    /**
-     * Parse media query list
-     *
-     * @param array $out
-     *
-     * @return boolean
-     */
-    protected function mediaQueryList(&$out)
-    {
-        return $this->genericList($out, 'mediaQuery', ',', false);
+        $this->count = $where;
+
+        return true;
     }
 
     /**
-     * Parse media query
+     * Match string looking for either ending delim, escape, or string interpolation
      *
-     * @param array $out
+     * {@internal This is a workaround for preg_match's 250K string match limit. }}
      *
-     * @return boolean
+     * @param array  $m     Matches (passed by reference)
+     * @param string $delim Delimeter
+     *
+     * @return boolean True if match; false otherwise
      */
-    protected function mediaQuery(&$out)
+    protected function matchString(&$m, $delim)
     {
-        $s = $this->seek();
-
-        $expressions = null;
-        $parts = array();
-
-        if (($this->literal('only') && ($only = true) || $this->literal('not') && ($not = true) || true) &&
-            $this->mixedKeyword($mediaType)
-        ) {
-            $prop = array('mediaType');
-
-            if (isset($only)) {
-                $prop[] = array('keyword', 'only');
-            }
+        $token = null;
 
-            if (isset($not)) {
-                $prop[] = array('keyword', 'not');
-            }
+        $end = strlen($this->buffer);
 
-            $media = array('list', '', array());
+        // look for either ending delim, escape, or string interpolation
+        foreach (['#{', '\\', $delim] as $lookahead) {
+            $pos = strpos($this->buffer, $lookahead, $this->count);
 
-            foreach ((array)$mediaType as $type) {
-                if (is_array($type)) {
-                    $media[2][] = $type;
-                } else {
-                    $media[2][] = array('keyword', $type);
-                }
+            if ($pos !== false && $pos < $end) {
+                $end = $pos;
+                $token = $lookahead;
             }
-
-            $prop[] = $media;
-            $parts[] = $prop;
         }
 
-        if (empty($parts) || $this->literal('and')) {
-            $this->genericList($expressions, 'mediaExpression', 'and', false);
-
-            if (is_array($expressions)) {
-                $parts = array_merge($parts, $expressions[2]);
-            }
+        if (! isset($token)) {
+            return false;
         }
 
-        $out = $parts;
+        $match = substr($this->buffer, $this->count, $end - $this->count);
+        $m = [
+            $match . $token,
+            $match,
+            $token
+        ];
+        $this->count = $end + strlen($token);
 
         return true;
     }
 
     /**
-     * Parse media expression
+     * Try to match something on head of buffer
      *
-     * @param array $out
+     * @param string  $regex
+     * @param array   $out
+     * @param boolean $eatWhitespace
      *
      * @return boolean
      */
-    protected function mediaExpression(&$out)
+    protected function match($regex, &$out, $eatWhitespace = null)
     {
-        $s = $this->seek();
-        $value = null;
+        if (! isset($eatWhitespace)) {
+            $eatWhitespace = $this->eatWhiteDefault;
+        }
 
-        if ($this->literal('(') &&
-            $this->expression($feature) &&
-            ($this->literal(':') && $this->expression($value) || true) &&
-            $this->literal(')')
-        ) {
-            $out = array('mediaExp', $feature);
+        $r = '/' . $regex . '/' . $this->patternModifiers;
 
-            if ($value) {
-                $out[] = $value;
+        if (preg_match($r, $this->buffer, $out, null, $this->count)) {
+            $this->count += strlen($out[0]);
+
+            if ($eatWhitespace) {
+                $this->whitespace();
             }
 
             return true;
         }
 
-        $this->seek($s);
+        return false;
+    }
+
+    /**
+     * Match literal string
+     *
+     * @param string  $what
+     * @param boolean $eatWhitespace
+     *
+     * @return boolean
+     */
+    protected function literal($what, $eatWhitespace = null)
+    {
+        if (! isset($eatWhitespace)) {
+            $eatWhitespace = $this->eatWhiteDefault;
+        }
+
+        $len = strlen($what);
+
+        if (strcasecmp(substr($this->buffer, $this->count, $len), $what) === 0) {
+            $this->count += $len;
+
+            if ($eatWhitespace) {
+                $this->whitespace();
+            }
+
+            return true;
+        }
+
+        return false;
+    }
+
+    /**
+     * Match some whitespace
+     *
+     * @return boolean
+     */
+    protected function whitespace()
+    {
+        $gotWhite = false;
+
+        while (preg_match(self::$whitePattern, $this->buffer, $m, null, $this->count)) {
+            if (isset($m[1]) && empty($this->commentsSeen[$this->count])) {
+                $this->appendComment([Type::T_COMMENT, $m[1]]);
+
+                $this->commentsSeen[$this->count] = true;
+            }
+
+            $this->count += strlen($m[0]);
+            $gotWhite = true;
+        }
+
+        return $gotWhite;
+    }
+
+    /**
+     * Append comment to current block
+     *
+     * @param array $comment
+     */
+    protected function appendComment($comment)
+    {
+        $comment[1] = substr(preg_replace(['/^\s+/m', '/^(.)/m'], ['', ' \1'], $comment[1]), 1);
+
+        $this->env->comments[] = $comment;
+    }
+
+    /**
+     * Append statement to current block
+     *
+     * @param array   $statement
+     * @param integer $pos
+     */
+    protected function append($statement, $pos = null)
+    {
+        if ($pos !== null) {
+            list($line, $column) = $this->getSourcePosition($pos);
+
+            $statement[self::SOURCE_LINE]   = $line;
+            $statement[self::SOURCE_COLUMN] = $column;
+            $statement[self::SOURCE_INDEX]  = $this->sourceIndex;
+        }
+
+        $this->env->children[] = $statement;
+
+        $comments = $this->env->comments;
+
+        if (count($comments)) {
+            $this->env->children = array_merge($this->env->children, $comments);
+            $this->env->comments = [];
+        }
+    }
+
+    /**
+     * Returns last child was appended
+     *
+     * @return array|null
+     */
+    protected function last()
+    {
+        $i = count($this->env->children) - 1;
+
+        if (isset($this->env->children[$i])) {
+            return $this->env->children[$i];
+        }
+    }
+
+    /**
+     * Parse media query list
+     *
+     * @param array $out
+     *
+     * @return boolean
+     */
+    protected function mediaQueryList(&$out)
+    {
+        return $this->genericList($out, 'mediaQuery', ',', false);
+    }
+
+    /**
+     * Parse media query
+     *
+     * @param array $out
+     *
+     * @return boolean
+     */
+    protected function mediaQuery(&$out)
+    {
+        $expressions = null;
+        $parts = [];
+
+        if (($this->literal('only') && ($only = true) || $this->literal('not') && ($not = true) || true) &&
+            $this->mixedKeyword($mediaType)
+        ) {
+            $prop = [Type::T_MEDIA_TYPE];
+
+            if (isset($only)) {
+                $prop[] = [Type::T_KEYWORD, 'only'];
+            }
+
+            if (isset($not)) {
+                $prop[] = [Type::T_KEYWORD, 'not'];
+            }
+
+            $media = [Type::T_LIST, '', []];
+
+            foreach ((array) $mediaType as $type) {
+                if (is_array($type)) {
+                    $media[2][] = $type;
+                } else {
+                    $media[2][] = [Type::T_KEYWORD, $type];
+                }
+            }
+
+            $prop[]  = $media;
+            $parts[] = $prop;
+        }
+
+        if (empty($parts) || $this->literal('and')) {
+            $this->genericList($expressions, 'mediaExpression', 'and', false);
+
+            if (is_array($expressions)) {
+                $parts = array_merge($parts, $expressions[2]);
+            }
+        }
+
+        $out = $parts;
+
+        return true;
+    }
+
+    /**
+     * Parse media expression
+     *
+     * @param array $out
+     *
+     * @return boolean
+     */
+    protected function mediaExpression(&$out)
+    {
+        $s = $this->seek();
+        $value = null;
+
+        if ($this->literal('(') &&
+            $this->expression($feature) &&
+            ($this->literal(':') && $this->expression($value) || true) &&
+            $this->literal(')')
+        ) {
+            $out = [Type::T_MEDIA_EXPRESSION, $feature];
+
+            if ($value) {
+                $out[] = $value;
+            }
+
+            return true;
+        }
+
+        $this->seek($s);
 
         return false;
     }
@@ -923,7 +1120,7 @@ class Parser
         }
 
         if ($this->genericList($value, 'expression')) {
-            $out = array($keyword, $value, false);
+            $out = [$keyword, $value, false];
             $s = $this->seek();
 
             if ($this->literal('...')) {
@@ -939,7 +1136,7 @@ class Parser
     }
 
     /**
-     * Parse list
+     * Parse comma separated value list
      *
      * @param string $out
      *
@@ -951,7 +1148,7 @@ class Parser
     }
 
     /**
-     * Parse space list
+     * Parse space separated value list
      *
      * @param array $out
      *
@@ -975,7 +1172,7 @@ class Parser
     protected function genericList(&$out, $parseItem, $delim = '', $flatten = true)
     {
         $s = $this->seek();
-        $items = array();
+        $items = [];
 
         while ($this->$parseItem($value)) {
             $items[] = $value;
@@ -996,7 +1193,7 @@ class Parser
         if ($flatten && count($items) === 1) {
             $out = $items[0];
         } else {
-            $out = array('list', $delim, $items);
+            $out = [Type::T_LIST, $delim, $items];
         }
 
         return true;
@@ -1015,12 +1212,12 @@ class Parser
 
         if ($this->literal('(')) {
             if ($this->literal(')')) {
-                $out = array('list', '', array());
+                $out = [Type::T_LIST, '', []];
 
                 return true;
             }
 
-            if ($this->valueList($out) && $this->literal(')') && $out[0] === 'list') {
+            if ($this->valueList($out) && $this->literal(')') && $out[0] === Type::T_LIST) {
                 return true;
             }
 
@@ -1052,13 +1249,13 @@ class Parser
      */
     protected function expHelper($lhs, $minP)
     {
-        $opstr = self::$operatorStr;
+        $operators = self::$operatorPattern;
 
         $ss = $this->seek();
         $whiteBefore = isset($this->buffer[$this->count - 1]) &&
             ctype_space($this->buffer[$this->count - 1]);
 
-        while ($this->match($opstr, $m, false) && self::$precedence[$m[1]] >= $minP) {
+        while ($this->match($operators, $m, false) && self::$precedence[$m[1]] >= $minP) {
             $whiteAfter = isset($this->buffer[$this->count]) &&
                 ctype_space($this->buffer[$this->count]);
             $varAfter = isset($this->buffer[$this->count]) &&
@@ -1078,11 +1275,11 @@ class Parser
             }
 
             // peek and see if rhs belongs to next operator
-            if ($this->peek($opstr, $next) && self::$precedence[$next[1]] > self::$precedence[$op]) {
+            if ($this->peek($operators, $next) && self::$precedence[$next[1]] > self::$precedence[$op]) {
                 $rhs = $this->expHelper($rhs, self::$precedence[$next[1]]);
             }
 
-            $lhs = array('exp', $op, $lhs, $rhs, $this->inParens, $whiteBefore, $whiteAfter);
+            $lhs = [Type::T_EXPRESSION, $op, $lhs, $rhs, $this->inParens, $whiteBefore, $whiteAfter];
             $ss = $this->seek();
             $whiteBefore = isset($this->buffer[$this->count - 1]) &&
                 ctype_space($this->buffer[$this->count - 1]);
@@ -1105,7 +1302,15 @@ class Parser
         $s = $this->seek();
 
         if ($this->literal('not', false) && $this->whitespace() && $this->value($inner)) {
-            $out = array('unary', 'not', $inner, $this->inParens);
+            $out = [Type::T_UNARY, 'not', $inner, $this->inParens];
+
+            return true;
+        }
+
+        $this->seek($s);
+
+        if ($this->literal('not', false) && $this->parenValue($inner)) {
+            $out = [Type::T_UNARY, 'not', $inner, $this->inParens];
 
             return true;
         }
@@ -1113,7 +1318,7 @@ class Parser
         $this->seek($s);
 
         if ($this->literal('+') && $this->value($inner)) {
-            $out = array('unary', '+', $inner, $this->inParens);
+            $out = [Type::T_UNARY, '+', $inner, $this->inParens];
 
             return true;
         }
@@ -1126,7 +1331,7 @@ class Parser
             $this->unit($inner) ||
             $this->parenValue($inner))
         ) {
-            $out = array('unary', '-', $inner, $this->inParens);
+            $out = [Type::T_UNARY, '-', $inner, $this->inParens];
 
             return true;
         }
@@ -1147,9 +1352,9 @@ class Parser
 
         if ($this->keyword($keyword)) {
             if ($keyword === 'null') {
-                $out = array('null');
+                $out = [Type::T_NULL];
             } else {
-                $out = array('keyword', $keyword);
+                $out = [Type::T_KEYWORD, $keyword];
             }
 
             return true;
@@ -1173,7 +1378,7 @@ class Parser
 
         if ($this->literal('(')) {
             if ($this->literal(')')) {
-                $out = array('list', '', array());
+                $out = [Type::T_LIST, '', []];
 
                 return true;
             }
@@ -1212,9 +1417,9 @@ class Parser
             $this->openString(')', $args, '(');
 
             if ($this->literal(')')) {
-                $out = array('string', '', array(
+                $out = [Type::T_STRING, '', [
                     'progid:', $fn, '(', $args, ')'
-                ));
+                ]];
 
                 return true;
             }
@@ -1240,7 +1445,7 @@ class Parser
             $this->literal('(')
         ) {
             if ($name === 'alpha' && $this->argumentList($args)) {
-                $func = array('function', $name, array('string', '', $args));
+                $func = [Type::T_FUNCTION, $name, [Type::T_STRING, '', $args]];
 
                 return true;
             }
@@ -1249,7 +1454,7 @@ class Parser
                 $ss = $this->seek();
 
                 if ($this->argValues($args) && $this->literal(')')) {
-                    $func = array('fncall', $name, $args);
+                    $func = [Type::T_FUNCTION_CALL, $name, $args];
 
                     return true;
                 }
@@ -1257,16 +1462,16 @@ class Parser
                 $this->seek($ss);
             }
 
-            if (($this->openString(')', $str, '(') || true ) &&
+            if (($this->openString(')', $str, '(') || true) &&
                 $this->literal(')')
             ) {
-                $args = array();
+                $args = [];
 
                 if (! empty($str)) {
-                    $args[] = array(null, array('string', '', array($str)));
+                    $args[] = [null, [Type::T_STRING, '', [$str]]];
                 }
 
-                $func = array('fncall', $name, $args);
+                $func = [Type::T_FUNCTION_CALL, $name, $args];
 
                 return true;
             }
@@ -1289,13 +1494,11 @@ class Parser
         $s = $this->seek();
         $this->literal('(');
 
-        $args = array();
+        $args = [];
 
         while ($this->keyword($var)) {
-            $ss = $this->seek();
-
             if ($this->literal('=') && $this->expression($exp)) {
-                $args[] = array('string', '', array($var . '='));
+                $args[] = [Type::T_STRING, '', [$var . '=']];
                 $arg = $exp;
             } else {
                 break;
@@ -1307,7 +1510,7 @@ class Parser
                 break;
             }
 
-            $args[] = array('string', '', array(', '));
+            $args[] = [Type::T_STRING, '', [', ']];
         }
 
         if (! $this->literal(')') || ! count($args)) {
@@ -1333,10 +1536,10 @@ class Parser
         $s = $this->seek();
         $this->literal('(');
 
-        $args = array();
+        $args = [];
 
         while ($this->variable($var)) {
-            $arg = array($var[1], null, false);
+            $arg = [$var[1], null, false];
 
             $ss = $this->seek();
 
@@ -1390,13 +1593,15 @@ class Parser
     {
         $s = $this->seek();
 
-        $this->literal('(');
+        if (! $this->literal('(')) {
+            return false;
+        }
 
-        $keys = array();
-        $values = array();
+        $keys = [];
+        $values = [];
 
-        while ($this->genericList($key, 'expression') && $this->literal(':')
-            && $this->genericList($value, 'expression')
+        while ($this->genericList($key, 'expression') && $this->literal(':') &&
+            $this->genericList($value, 'expression')
         ) {
             $keys[] = $key;
             $values[] = $value;
@@ -1412,7 +1617,7 @@ class Parser
             return false;
         }
 
-        $out = array('map', $keys, $values);
+        $out = [Type::T_MAP, $keys, $values];
 
         return true;
     }
@@ -1426,24 +1631,24 @@ class Parser
      */
     protected function color(&$out)
     {
-        $color = array('color');
+        $color = [Type::T_COLOR];
 
         if ($this->match('(#([0-9a-f]{6})|#([0-9a-f]{3}))', $m)) {
             if (isset($m[3])) {
-                $num = $m[3];
-                $width = 16;
-            } else {
-                $num = $m[2];
-                $width = 256;
-            }
+                $num = hexdec($m[3]);
 
-            $num = hexdec($num);
-
-            foreach (array(3, 2, 1) as $i) {
-                $t = $num % $width;
-                $num /= $width;
+                foreach ([3, 2, 1] as $i) {
+                    $t = $num & 0xf;
+                    $color[$i] = $t << 4 | $t;
+                    $num >>= 4;
+                }
+            } else {
+                $num = hexdec($m[2]);
 
-                $color[$i] = $t * (256/$width) + $t * floor(16/$width);
+                foreach ([3, 2, 1] as $i) {
+                    $color[$i] = $num & 0xff;
+                    $num >>= 8;
+                }
             }
 
             $out = $color;
@@ -1464,7 +1669,7 @@ class Parser
     protected function unit(&$unit)
     {
         if ($this->match('([0-9]*(\.)?[0-9]+)([%a-zA-Z]+)?', $m)) {
-            $unit = array('number', $m[1], empty($m[3]) ? '' : $m[3]);
+            $unit = new Node\Number($m[1], empty($m[3]) ? '' : $m[3]);
 
             return true;
         }
@@ -1485,33 +1690,39 @@ class Parser
 
         if ($this->literal('"', false)) {
             $delim = '"';
-        } elseif ($this->literal('\'', false)) {
-            $delim = '\'';
+        } elseif ($this->literal("'", false)) {
+            $delim = "'";
         } else {
             return false;
         }
 
-        $content = array();
+        $content = [];
         $oldWhite = $this->eatWhiteDefault;
         $this->eatWhiteDefault = false;
+        $hasInterpolation = false;
 
         while ($this->matchString($m, $delim)) {
-            $content[] = $m[1];
+            if ($m[1] !== '') {
+                $content[] = $m[1];
+            }
 
             if ($m[2] === '#{') {
                 $this->count -= strlen($m[2]);
 
                 if ($this->interpolation($inter, false)) {
                     $content[] = $inter;
+                    $hasInterpolation = true;
                 } else {
                     $this->count += strlen($m[2]);
                     $content[] = '#{'; // ignore it
                 }
             } elseif ($m[2] === '\\') {
-                $content[] = $m[2];
-
-                if ($this->literal($delim, false)) {
-                    $content[] = $delim;
+                if ($this->literal('"', false)) {
+                    $content[] = $m[2] . '"';
+                } elseif ($this->literal("'", false)) {
+                    $content[] = $m[2] . "'";
+                } else {
+                    $content[] = $m[2];
                 }
             } else {
                 $this->count -= strlen($delim);
@@ -1522,7 +1733,19 @@ class Parser
         $this->eatWhiteDefault = $oldWhite;
 
         if ($this->literal($delim)) {
-            $out = array('string', $delim, $content);
+            if ($hasInterpolation) {
+                $delim = '"';
+
+                foreach ($content as &$string) {
+                    if ($string === "\\'") {
+                        $string = "'";
+                    } elseif ($string === '\\"') {
+                        $string = '"';
+                    }
+                }
+            }
+
+            $out = [Type::T_STRING, $delim, $content];
 
             return true;
         }
@@ -1541,14 +1764,12 @@ class Parser
      */
     protected function mixedKeyword(&$out)
     {
-        $s = $this->seek();
-
-        $parts = array();
+        $parts = [];
 
         $oldWhite = $this->eatWhiteDefault;
         $this->eatWhiteDefault = false;
 
-        while (true) {
+        for (;;) {
             if ($this->keyword($key)) {
                 $parts[] = $key;
                 continue;
@@ -1591,15 +1812,11 @@ class Parser
         $oldWhite = $this->eatWhiteDefault;
         $this->eatWhiteDefault = false;
 
-        $stop = array('\'', '"', '#{', $end);
-        $stop = array_map(array($this, 'pregQuote'), $stop);
-        $stop[] = self::$commentMulti;
-
-        $patt = '(.*?)(' . implode('|', $stop) . ')';
+        $patt = '(.*?)([\'"]|#\{|' . $this->pregQuote($end) . '|' . self::$commentPattern . ')';
 
         $nestingLevel = 0;
 
-        $content = array();
+        $content = [];
 
         while ($this->match($patt, $m, false)) {
             if (isset($m[1]) && $m[1] !== '') {
@@ -1618,7 +1835,7 @@ class Parser
                 break;
             }
 
-            if (($tok === '\'' || $tok === '"') && $this->string($str)) {
+            if (($tok === "'" || $tok === '"') && $this->string($str)) {
                 $content[] = $str;
                 continue;
             }
@@ -1643,7 +1860,7 @@ class Parser
             $content[count($content) - 1] = rtrim(end($content));
         }
 
-        $out = array('string', '', $content);
+        $out = [Type::T_STRING, '', $content];
 
         return true;
     }
@@ -1664,8 +1881,6 @@ class Parser
         $s = $this->seek();
 
         if ($this->literal('#{') && $this->valueList($value) && $this->literal('}', false)) {
-            // TODO: don't error if out of bounds
-
             if ($lookWhite) {
                 $left = preg_match('/\s/', $this->buffer[$s - 1]) ? ' ' : '';
                 $right = preg_match('/\s/', $this->buffer[$this->count]) ? ' ': '';
@@ -1673,17 +1888,19 @@ class Parser
                 $left = $right = false;
             }
 
-            $out = array('interpolate', $value, $left, $right);
+            $out = [Type::T_INTERPOLATE, $value, $left, $right];
             $this->eatWhiteDefault = $oldWhite;
 
             if ($this->eatWhiteDefault) {
                 $this->whitespace();
             }
+
             return true;
         }
 
         $this->seek($s);
         $this->eatWhiteDefault = $oldWhite;
+
         return false;
     }
 
@@ -1696,23 +1913,29 @@ class Parser
      */
     protected function propertyName(&$out)
     {
-        $s = $this->seek();
-        $parts = array();
+        $parts = [];
 
         $oldWhite = $this->eatWhiteDefault;
         $this->eatWhiteDefault = false;
 
-        while (true) {
+        for (;;) {
             if ($this->interpolation($inter)) {
                 $parts[] = $inter;
-            } elseif ($this->keyword($text)) {
+                continue;
+            }
+
+            if ($this->keyword($text)) {
                 $parts[] = $text;
-            } elseif (count($parts) === 0 && $this->match('[:.#]', $m, false)) {
+                continue;
+            }
+
+            if (count($parts) === 0 && $this->match('[:.#]', $m, false)) {
                 // css hacks
                 $parts[] = $m[0];
-            } else {
-                break;
+                continue;
             }
+
+            break;
         }
 
         $this->eatWhiteDefault = $oldWhite;
@@ -1737,7 +1960,7 @@ class Parser
 
         $this->whitespace(); // get any extra whitespace
 
-        $out = array('string', '', $parts);
+        $out = [Type::T_STRING, '', $parts];
 
         return true;
     }
@@ -1752,7 +1975,7 @@ class Parser
     protected function selectors(&$out)
     {
         $s = $this->seek();
-        $selectors = array();
+        $selectors = [];
 
         while ($this->selector($sel)) {
             $selectors[] = $sel;
@@ -1786,20 +2009,26 @@ class Parser
      */
     protected function selector(&$out)
     {
-        $selector = array();
+        $selector = [];
 
-        while (true) {
+        for (;;) {
             if ($this->match('[>+~]+', $m)) {
-                $selector[] = array($m[0]);
-            } elseif ($this->selectorSingle($part)) {
+                $selector[] = [$m[0]];
+                continue;
+            }
+
+            if ($this->selectorSingle($part)) {
                 $selector[] = $part;
                 $this->match('\s+', $m);
-            } elseif ($this->match('\/[^\/]+\/', $m)) {
-                $selector[] = array($m[0]);
-            } else {
-                break;
+                continue;
             }
 
+            if ($this->match('\/[^\/]+\/', $m)) {
+                $selector[] = [$m[0]];
+                continue;
+            }
+
+            break;
         }
 
         if (count($selector) === 0) {
@@ -1826,13 +2055,13 @@ class Parser
         $oldWhite = $this->eatWhiteDefault;
         $this->eatWhiteDefault = false;
 
-        $parts = array();
+        $parts = [];
 
         if ($this->literal('*', false)) {
             $parts[] = '*';
         }
 
-        while (true) {
+        for (;;) {
             // see if we can stop early
             if ($this->match('\s*[{,]', $m)) {
                 $this->count--;
@@ -1900,7 +2129,7 @@ class Parser
                 $ss = $this->seek();
 
                 if ($this->literal('(') &&
-                    ($this->openString(')', $str, '(') || true ) &&
+                    ($this->openString(')', $str, '(') || true) &&
                     $this->literal(')')
                 ) {
                     $parts[] = '(';
@@ -1920,60 +2149,23 @@ class Parser
             $this->seek($s);
 
             // attribute selector
-            // TODO: replace with open string?
-            if ($this->literal('[', false)) {
-                $attrParts = array('[');
-
-                // keyword, string, operator
-                while (true) {
-                    if ($this->literal(']', false)) {
-                        $this->count--;
-                        break; // get out early
-                    }
-
-                    if ($this->match('\s+', $m)) {
-                        $attrParts[] = ' ';
-                        continue;
-                    }
-
-                    if ($this->string($str)) {
-                        $attrParts[] = $str;
-                        continue;
-                    }
-
-                    if ($this->keyword($word)) {
-                        $attrParts[] = $word;
-                        continue;
-                    }
-
-                    if ($this->interpolation($inter, false)) {
-                        $attrParts[] = $inter;
-                        continue;
-                    }
-
-                    // operator, handles attr namespace too
-                    if ($this->match('[|-~\$\*\^=]+', $m)) {
-                        $attrParts[] = $m[0];
-                        continue;
-                    }
+            if ($this->literal('[') &&
+               ($this->openString(']', $str, '[') || true) &&
+               $this->literal(']')
+            ) {
+                $parts[] = '[';
 
-                    break;
+                if (! empty($str)) {
+                    $parts[] = $str;
                 }
 
-                if ($this->literal(']', false)) {
-                    $attrParts[] = ']';
-
-                    foreach ($attrParts as $part) {
-                        $parts[] = $part;
-                    }
+                $parts[] = ']';
 
-                    continue;
-                }
-
-                $this->seek($s);
-                // TODO: should just break here?
+                continue;
             }
 
+            $this->seek($s);
+
             break;
         }
 
@@ -2000,7 +2192,7 @@ class Parser
         $s = $this->seek();
 
         if ($this->literal('$', false) && $this->keyword($name)) {
-            $out = array('var', $name);
+            $out = [Type::T_VARIABLE, $name];
 
             return true;
         }
@@ -2021,7 +2213,9 @@ class Parser
     protected function keyword(&$word, $eatWhitespace = null)
     {
         if ($this->match(
-            '(([\w_\-\*!"\']|[\\\\].)([\w\-_"\']|[\\\\].)*)',
+            $this->utf8
+                ? '(([\pL\w_\-\*!"\']|[\\\\].)([\pL\w\-_"\']|[\\\\].)*)'
+                : '(([\w_\-\*!"\']|[\\\\].)([\w\-_"\']|[\\\\].)*)',
             $m,
             $eatWhitespace
         )) {
@@ -2042,7 +2236,12 @@ class Parser
      */
     protected function placeholder(&$placeholder)
     {
-        if ($this->match('([\w\-_]+|#[{][$][\w\-_]+[}])', $m)) {
+        if ($this->match(
+            $this->utf8
+                ? '([\pL\w\-_]+|#[{][$][\pL\w\-_]+[}])'
+                : '([\w\-_]+|#[{][$][\w\-_]+[}])',
+            $m
+        )) {
             $placeholder = $m[1];
 
             return true;
@@ -2061,7 +2260,7 @@ class Parser
     protected function url(&$out)
     {
         if ($this->match('(url\(\s*(["\']?)([^)]+)\2\s*\))', $m)) {
-            $out = array('string', '', array('url(' . $m[2] . $m[3] . $m[2] . ')'));
+            $out = [Type::T_STRING, '', ['url(' . $m[2] . $m[3] . $m[2] . ')']];
 
             return true;
         }
@@ -2089,254 +2288,200 @@ class Parser
     }
 
     /**
-     * @deprecated
+     * Strip assignment flag from the list
      *
-     * {@internal
-     *     advance counter to next occurrence of $what
-     *     $until - don't include $what in advance
-     *     $allowNewline, if string, will be used as valid char set
-     * }}
+     * @param array $value
+     *
+     * @return array
      */
-    protected function to($what, &$out, $until = false, $allowNewline = false)
+    protected function stripAssignmentFlags(&$value)
     {
-        if (is_string($allowNewline)) {
-            $validChars = $allowNewline;
-        } else {
-            $validChars = $allowNewline ? '.' : "[^\n]";
-        }
+        $flags = [];
 
-        if (! $this->match('(' . $validChars . '*?)' . $this->pregQuote($what), $m, ! $until)) {
-            return false;
-        }
+        for ($token = &$value; $token[0] === Type::T_LIST && ($s = count($token[2])); $token = &$lastNode) {
+            $lastNode = &$token[2][$s - 1];
 
-        if ($until) {
-            $this->count -= strlen($what); // give back $what
-        }
+            while ($lastNode[0] === Type::T_KEYWORD && in_array($lastNode[1], ['!default', '!global'])) {
+                array_pop($token[2]);
 
-        $out = $m[1];
+                $node = end($token[2]);
 
-        return true;
+                $token = $this->flattenList($token);
+
+                $flags[] = $lastNode[1];
+
+                $lastNode = $node;
+            }
+        }
+
+        return $flags;
     }
 
     /**
-     * Throw parser error
+     * Strip optional flag from selector list
      *
-     * @param string  $msg
-     * @param integer $count
+     * @param array $selectors
      *
-     * @throws \Exception
+     * @return string
      */
-    public function throwParseError($msg = 'parse error', $count = null)
+    protected function stripOptionalFlag(&$selectors)
     {
-        $count = ! isset($count) ? $this->count : $count;
+        $optional = false;
 
-        $line = $this->getLineNo($count);
+        $selector = end($selectors);
+        $part = end($selector);
 
-        if (! empty($this->sourceName)) {
-            $loc = "$this->sourceName on line $line";
-        } else {
-            $loc = "line: $line";
-        }
+        if ($part === ['!optional']) {
+            array_pop($selectors[count($selectors) - 1]);
 
-        if ($this->peek("(.*?)(\n|$)", $m, $count)) {
-            throw new \Exception("$msg: failed at `$m[1]` $loc");
+            $optional = true;
         }
 
-        throw new \Exception("$msg: $loc");
-    }
-
-    /**
-     * Get source file name
-     *
-     * @return string
-     */
-    public function getSourceName()
-    {
-        return $this->sourceName;
+        return $optional;
     }
 
     /**
-     * Get source line number (given character position in the buffer)
+     * Turn list of length 1 into value type
      *
-     * @param integer $pos
+     * @param array $value
      *
-     * @return integer
+     * @return array
      */
-    public function getLineNo($pos)
+    protected function flattenList($value)
     {
-        return 1 + substr_count(substr($this->buffer, 0, $pos), "\n");
+        if ($value[0] === Type::T_LIST && count($value[2]) === 1) {
+            return $this->flattenList($value[2][0]);
+        }
+
+        return $value;
     }
 
     /**
-     * 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
+     * @deprecated
      *
-     * @return boolean True if match; false otherwise
+     * {@internal
+     *     advance counter to next occurrence of $what
+     *     $until - don't include $what in advance
+     *     $allowNewline, if string, will be used as valid char set
+     * }}
      */
-    protected function matchString(&$m, $delim)
+    protected function to($what, &$out, $until = false, $allowNewline = false)
     {
-        $token = null;
-
-        $end = strlen($this->buffer);
-
-        // look for either ending delim, escape, or string interpolation
-        foreach (array('#{', '\\', $delim) as $lookahead) {
-            $pos = strpos($this->buffer, $lookahead, $this->count);
-
-            if ($pos !== false && $pos < $end) {
-                $end = $pos;
-                $token = $lookahead;
-            }
+        if (is_string($allowNewline)) {
+            $validChars = $allowNewline;
+        } else {
+            $validChars = $allowNewline ? '.' : "[^\n]";
         }
 
-        if (! isset($token)) {
+        if (! $this->match('(' . $validChars . '*?)' . $this->pregQuote($what), $m, ! $until)) {
             return false;
         }
 
-        $match = substr($this->buffer, $this->count, $end - $this->count);
-        $m = array(
-            $match . $token,
-            $match,
-            $token
-        );
-        $this->count = $end + strlen($token);
+        if ($until) {
+            $this->count -= strlen($what); // give back $what
+        }
+
+        $out = $m[1];
 
         return true;
     }
 
     /**
-     * Try to match something on head of buffer
-     *
-     * @param string  $regex
-     * @param array   $out
-     * @param boolean $eatWhitespace
-     *
-     * @return boolean
+     * @deprecated
      */
-    protected function match($regex, &$out, $eatWhitespace = null)
+    protected function show()
     {
-        if (! isset($eatWhitespace)) {
-            $eatWhitespace = $this->eatWhiteDefault;
-        }
-
-        $r = '/' . $regex . '/Ais';
-
-        if (preg_match($r, $this->buffer, $out, null, $this->count)) {
-            $this->count += strlen($out[0]);
-
-            if ($eatWhitespace) {
-                $this->whitespace();
-            }
-
-            return true;
+        if ($this->peek("(.*?)(\n|$)", $m, $this->count)) {
+            return $m[1];
         }
 
-        return false;
+        return '';
     }
 
     /**
-     * Match some whitespace
+     * Quote regular expression
      *
-     * @return boolean
+     * @param string $what
+     *
+     * @return string
      */
-    protected function whitespace()
+    private function pregQuote($what)
     {
-        $gotWhite = false;
-
-        while (preg_match(self::$whitePattern, $this->buffer, $m, null, $this->count)) {
-            if (isset($m[1]) && empty($this->commentsSeen[$this->count])) {
-                $this->appendComment(array('comment', $m[1]));
-
-                $this->commentsSeen[$this->count] = true;
-            }
-
-            $this->count += strlen($m[0]);
-            $gotWhite = true;
-        }
-
-        return $gotWhite;
+        return preg_quote($what, '/');
     }
 
     /**
-     * Peek input stream
-     *
-     * @param string  $regex
-     * @param array   $out
-     * @param integer $from
+     * Extract line numbers from buffer
      *
-     * @return integer
+     * @param string $buffer
      */
-    protected function peek($regex, &$out, $from = null)
+    private function extractLineNumbers($buffer)
     {
-        if (! isset($from)) {
-            $from = $this->count;
+        $this->sourcePositions = [0 => 0];
+        $prev = 0;
+
+        while (($pos = strpos($buffer, "\n", $prev)) !== false) {
+            $this->sourcePositions[] = $pos;
+            $prev = $pos + 1;
         }
 
-        $r = '/' . $regex . '/Ais';
-        $result = preg_match($r, $this->buffer, $out, null, $from);
+        $this->sourcePositions[] = strlen($buffer);
 
-        return $result;
+        if (substr($buffer, -1) !== "\n") {
+            $this->sourcePositions[] = strlen($buffer) + 1;
+        }
     }
 
     /**
-     * Seek to position in input stream (or return current position in input stream)
+     * Get source line number and column (given character position in the buffer)
      *
-     * @param integer $where
+     * @param integer $pos
      *
      * @return integer
      */
-    protected function seek($where = null)
+    private function getSourcePosition($pos)
     {
-        if ($where === null) {
-            return $this->count;
-        }
+        $low = 0;
+        $high = count($this->sourcePositions);
 
-        $this->count = $where;
+        while ($low < $high) {
+            $mid = (int) (($high + $low) / 2);
 
-        return true;
-    }
+            if ($pos < $this->sourcePositions[$mid]) {
+                $high = $mid - 1;
+                continue;
+            }
 
-    /**
-     * Quote regular expression
-     *
-     * @param string $what
-     *
-     * @return string
-     */
-    public static function pregQuote($what)
-    {
-        return preg_quote($what, '/');
+            if ($pos >= $this->sourcePositions[$mid + 1]) {
+                $low = $mid + 1;
+                continue;
+            }
+
+            return [$mid + 1, $pos - $this->sourcePositions[$mid]];
+        }
+
+        return [$low + 1, $pos - $this->sourcePositions[$low]];
     }
 
     /**
-     * @deprecated
+     * Save internal encoding
      */
-    protected function show()
+    private function saveEncoding()
     {
-        if ($this->peek("(.*?)(\n|$)", $m, $this->count)) {
-            return $m[1];
-        }
+        if (ini_get('mbstring.func_overload') & 2) {
+            $this->encoding = mb_internal_encoding();
 
-        return '';
+            mb_internal_encoding('iso-8859-1');
+        }
     }
 
     /**
-     * Turn list of length 1 into value type
-     *
-     * @param array $value
-     *
-     * @return array
+     * Restore internal encoding
      */
-    protected function flattenList($value)
+    private function restoreEncoding()
     {
-        if ($value[0] === 'list' && count($value[2]) === 1) {
-            return $this->flattenList($value[2][0]);
+        if ($this->encoding) {
+            mb_internal_encoding($this->encoding);
         }
-
-        return $value;
     }
 }
index 5bd9bef0c8f8a07c09139ce7e30a86ab9b0a1029..221655cad8cad0459b0030d84666fd642f446568 100644 (file)
 namespace Leafo\ScssPhp;
 
 use Leafo\ScssPhp\Compiler;
+use Leafo\ScssPhp\Exception\ServerException;
 use Leafo\ScssPhp\Version;
 
 /**
- * SCSS server
+ * Server
  *
  * @author Leaf Corcoran <leafot@gmail.com>
  */
@@ -115,13 +116,12 @@ class Server
     /**
      * Determine whether .scss file needs to be re-compiled.
      *
-     * @param string $in   Input path
      * @param string $out  Output path
      * @param string $etag ETag
      *
      * @return boolean True if compile required.
      */
-    protected function needsCompile($in, $out, &$etag)
+    protected function needsCompile($out, &$etag)
     {
         if (! is_file($out)) {
             return true;
@@ -129,21 +129,25 @@ class Server
 
         $mtime = filemtime($out);
 
-        if (filemtime($in) > $mtime) {
-            return true;
-        }
-
         $metadataName = $this->metadataName($out);
 
         if (is_readable($metadataName)) {
             $metadata = unserialize(file_get_contents($metadataName));
 
-            foreach ($metadata['imports'] as $import => $importMtime) {
-                if ($importMtime > $mtime) {
+            foreach ($metadata['imports'] as $import => $originalMtime) {
+                $currentMtime = filemtime($import);
+
+                if ($currentMtime !== $originalMtime || $currentMtime > $mtime) {
                     return true;
                 }
             }
 
+            $metaVars = crc32(serialize($this->scss->getVariables()));
+
+            if ($metaVars !== $metadata['vars']) {
+                return true;
+            }
+
             $etag = $metadata['etag'];
 
             return false;
@@ -203,20 +207,21 @@ class Server
         $elapsed = round((microtime(true) - $start), 4);
 
         $v    = Version::VERSION;
-        $t    = @date('r');
+        $t    = date('r');
         $css  = "/* compiled by scssphp $v on $t (${elapsed}s) */\n\n" . $css;
         $etag = md5($css);
 
         file_put_contents($out, $css);
         file_put_contents(
             $this->metadataName($out),
-            serialize(array(
+            serialize([
                 'etag'    => $etag,
                 'imports' => $this->scss->getParsedFiles(),
-            ))
+                'vars'    => crc32(serialize($this->scss->getVariables())),
+            ])
         );
 
-        return array($css, $etag);
+        return [$css, $etag];
     }
 
     /**
@@ -226,11 +231,11 @@ class Server
      *
      * @return string
      */
-    protected function createErrorCSS($error)
+    protected function createErrorCSS(\Exception $error)
     {
         $message = str_replace(
-            array("'", "\n"),
-            array("\\'", "\\A"),
+            ["'", "\n"],
+            ["\\'", "\\A"],
             $error->getfile() . ":\n\n" . $error->getMessage()
         );
 
@@ -264,11 +269,13 @@ class Server
      * @param string $out Output file (.css) optional
      *
      * @return string|bool
+     *
+     * @throws \Leafo\ScssPhp\Exception\ServerException
      */
     public function compileFile($in, $out = null)
     {
         if (! is_readable($in)) {
-            throw new \Exception('load error: failed to find ' . $in);
+            throw new ServerException('load error: failed to find ' . $in);
         }
 
         $pi = pathinfo($in);
@@ -318,7 +325,7 @@ class Server
             $output = $this->cacheName($salt . $input);
             $etag = $noneMatch = trim($this->getIfNoneMatchHeader(), '"');
 
-            if ($this->needsCompile($input, $output, $etag)) {
+            if ($this->needsCompile($output, $etag)) {
                 try {
                     list($css, $etag) = $this->compile($input, $output);
 
@@ -329,7 +336,6 @@ class Server
                     header('ETag: "' . $etag . '"');
 
                     echo $css;
-
                 } catch (\Exception $e) {
                     if ($this->showErrorsAsCSS) {
                         header('Content-type: text/css');
@@ -341,7 +347,6 @@ class Server
 
                         echo 'Parse error: ' . $e->getMessage() . "\n";
                     }
-
                 }
 
                 return;
@@ -360,7 +365,7 @@ class Server
             $modifiedSince = $this->getIfModifiedSinceHeader();
             $mtime = filemtime($output);
 
-            if (@strtotime($modifiedSince) === $mtime) {
+            if (strtotime($modifiedSince) === $mtime) {
                 header($protocol . ' 304 Not Modified');
 
                 return;
@@ -390,19 +395,19 @@ class Server
      *
      * @return string Compiled CSS results
      *
-     * @throws \Exception
+     * @throws \Leafo\ScssPhp\Exception\ServerException
      */
     public function checkedCachedCompile($in, $out, $force = false)
     {
         if (! is_file($in) || ! is_readable($in)) {
-            throw new \Exception('Invalid or unreadable input file specified.');
+            throw new ServerException('Invalid or unreadable input file specified.');
         }
 
         if (is_dir($out) || ! is_writable(file_exists($out) ? $out : dirname($out))) {
-            throw new \Exception('Invalid or unwritable output file specified.');
+            throw new ServerException('Invalid or unwritable output file specified.');
         }
 
-        if ($force || $this->needsCompile($in, $out, $etag)) {
+        if ($force || $this->needsCompile($out, $etag)) {
             list($css, $etag) = $this->compile($in, $out);
         } else {
             $css = file_get_contents($out);
@@ -439,6 +444,10 @@ class Server
 
         $this->scss = $scss;
         $this->showErrorsAsCSS = false;
+
+        if (! ini_get('date.timezone')) {
+            date_default_timezone_set('UTC');
+        }
     }
 
     /**
diff --git a/wcfsetup/install/files/lib/system/style/scssphp/src/Type.php b/wcfsetup/install/files/lib/system/style/scssphp/src/Type.php
new file mode 100644 (file)
index 0000000..8c3886c
--- /dev/null
@@ -0,0 +1,69 @@
+<?php
+/**
+ * SCSSPHP
+ *
+ * @copyright 2012-2015 Leaf Corcoran
+ *
+ * @license http://opensource.org/licenses/MIT MIT
+ *
+ * @link http://leafo.github.io/scssphp
+ */
+
+namespace Leafo\ScssPhp;
+
+/**
+ * Block/node types
+ *
+ * @author Anthon Pang <anthon.pang@gmail.com>
+ */
+class Type
+{
+    const T_ASSIGN = 'assign';
+    const T_AT_ROOT = 'at-root';
+    const T_BLOCK = 'block';
+    const T_BREAK = 'break';
+    const T_CHARSET = 'charset';
+    const T_COLOR = 'color';
+    const T_COMMENT = 'comment';
+    const T_CONTINUE = 'continue';
+    const T_CONTROL = 'control';
+    const T_DEBUG = 'debug';
+    const T_DIRECTIVE = 'directive';
+    const T_EACH = 'each';
+    const T_ELSE = 'else';
+    const T_ELSEIF = 'elseif';
+    const T_ERROR = 'error';
+    const T_EXPRESSION = 'exp';
+    const T_EXTEND = 'extend';
+    const T_FOR = 'for';
+    const T_FUNCTION = 'function';
+    const T_FUNCTION_CALL = 'fncall';
+    const T_HSL = 'hsl';
+    const T_IF = 'if';
+    const T_IMPORT = 'import';
+    const T_INCLUDE = 'include';
+    const T_INTERPOLATE = 'interpolate';
+    const T_INTERPOLATED = 'interpolated';
+    const T_KEYWORD = 'keyword';
+    const T_LIST = 'list';
+    const T_MAP = 'map';
+    const T_MEDIA = 'media';
+    const T_MEDIA_EXPRESSION = 'mediaExp';
+    const T_MEDIA_TYPE = 'mediaType';
+    const T_MEDIA_VALUE = 'mediaValue';
+    const T_MIXIN = 'mixin';
+    const T_MIXIN_CONTENT = 'mixin_content';
+    const T_NESTED_PROPERTY = 'nestedprop';
+    const T_NOT = 'not';
+    const T_NULL = 'null';
+    const T_NUMBER = 'number';
+    const T_RETURN = 'return';
+    const T_ROOT = 'root';
+    const T_SCSSPHP_IMPORT_ONCE = 'scssphp-import-once';
+    const T_SELF = 'self';
+    const T_STRING = 'string';
+    const T_UNARY = 'unary';
+    const T_VARIABLE = 'var';
+    const T_WARN = 'warn';
+    const T_WHILE = 'while';
+}
index 714acdb8951ca37b93c6d6c6a3be58e7ba206149..9f47c1d79762b9b48f7045ba4a3037752c40c1bf 100644 (file)
@@ -14,7 +14,7 @@ namespace Leafo\ScssPhp;
 use Leafo\ScssPhp\Base\Range;
 
 /**
- * SCSS utilties
+ * Utilties
  *
  * @author Anthon Pang <anthon.pang@gmail.com>
  */
index c650a26dbe4e5928b71a0b07d7a3c978f12df05f..80cdeee5adf3dabd4d9554fc18c3af758bd08688 100644 (file)
@@ -18,5 +18,5 @@ namespace Leafo\ScssPhp;
  */
 class Version
 {
-    const VERSION = 'v0.3.0';
+    const VERSION = 'v0.6.6';
 }
index 0e25bf650a08eaf6e59d1dd7733102e787c94f9d..7229f742c0d49e1597f2e12fbba2d9c6ff724daa 100644 (file)
@@ -236,7 +236,7 @@ $wcfImageViewerFontColor: rgba(211, 211, 211, 1);
                        -ms-user-select: none;*/
                        
                        @extend .icon48;
-                       @extend .icon-spinner;
+                       @extend .fa-spinner;
                }
                
                > img {
@@ -404,8 +404,8 @@ $wcfImageViewerFontColor: rgba(211, 211, 211, 1);
                                        
                                        &.loading{
                                                &:before {
-                                                       @extend .icon-48;
-                                                       @extend .icon-spinner;
+                                                       @extend .icon48;
+                                                       @extend .fa-spinner;
                                                }
                                                
                                                > img {
@@ -450,4 +450,4 @@ $wcfImageViewerFontColor: rgba(211, 211, 211, 1);
                        }
                }
        }
-}
\ No newline at end of file
+}