Przeglądaj źródła

Replacing the SCSS->CSS conversion library by a newer one made by Leaf Corcoran: http://leafo.github.io/scssphp, tweaked to work on PHP 5.3

git-svn-id: http://svn.code.sf.net/p/itop/code/trunk@4203 a333f486-631f-4898-b8df-5754b55c2be0
dflaven 9 lat temu
rodzic
commit
be8c0e08da

+ 13 - 15
application/utils.inc.php

@@ -1,5 +1,6 @@
 <?php
 use Html2Text\Html2Text;
+use Leafo\ScssPhp\Compiler;
 // Copyright (C) 2010-2016 Combodo SARL
 //
 //   This file is part of iTop.
@@ -1197,34 +1198,31 @@ class utils
 		$sText = str_replace("\r", "\n", $sText);
 		return str_replace("\n", '<br/>', htmlentities($sText, ENT_QUOTES, 'UTF-8'));
 	}
-
+	
 	/**
 	 * Eventually compiles the SASS (.scss) file into the CSS (.css) file
 	 *
-	 * @param string $sSaasRelPath Relative path to the SCSS file (must have the extension .scss)
+	 * @param string $sSassRelPath Relative path to the SCSS file (must have the extension .scss)
 	 * @return string Relative path to the CSS file (<name>.css)
 	 */
-	static public function GetCSSFromSASS($sSaasRelPath)
+	static public function GetCSSFromSASS($sSassRelPath)
 	{
-		$sSaasPath = APPROOT.$sSaasRelPath;
-		$sCssRelPath = preg_replace('/\.scss$/', '.css', $sSaasRelPath);
+		$sSassPath = APPROOT.$sSassRelPath;
+		$sCssRelPath = preg_replace('/\.scss$/', '.css', $sSassRelPath);
 		$sCssPath = APPROOT.$sCssRelPath;
 		clearstatcache();
-		if (!file_exists($sCssPath) || (is_writable($sCssPath) && (filemtime($sCssPath) < filemtime($sSaasPath))))
+		if (!file_exists($sCssPath) || (is_writable($sCssPath) && (filemtime($sCssPath) < filemtime($sSassPath))))
 		{
-			// Rebuild the CSS file from the Saas file
-			if (file_exists(APPROOT.'lib/sass/sass/SassParser.php'))
-			{
-				require_once(APPROOT.'lib/sass/sass/SassParser.php'); //including Sass libary (Syntactically Awesome Stylesheets)
-				$oParser = new SassParser(array('style'=>'expanded'));
-				$sCss = $oParser->toCss($sSaasPath);
-				file_put_contents($sCssPath, $sCss);
-			}
+			require_once(APPROOT.'lib/scssphp/scss.inc.php');
+			$oScss = new Compiler();
+			$oScss->setImportPaths(array(APPROOT.'/css'));
+			$oScss->setFormatter('Leafo\\ScssPhp\\Formatter\\Expanded');
+			$sCss = $oScss->compile(file_get_contents($sSassPath));
+			file_put_contents($sCssPath, $sCss);
 		}
 		return $sCssRelPath;
 	}
 	
-	
 	static public function GetImageSize($sImageData)
 	{
 		if (function_exists('getimagesizefromstring')) // PHP 5.4.0 or higher

+ 2 - 2
css/css-variables.scss

@@ -4,5 +4,5 @@ $complement-color: #1c94c4;
 $complement-light: #d6e8ef;
 $frame-background-color: #F1F1F1;
 $text-color: #000;
-// Beware the version number MUST beging with a letter otherwise it may be truncated...
-$version: v2.3.0b;
+// Beware the version number MUST be enclosed with quotes otherwise v2.3.0 becomes v2 0.3 .0
+$version: "v2.3.0b";

Plik diff jest za duży
+ 101 - 378
css/light-grey.css


+ 10 - 10
css/light-grey.scss

@@ -1,10 +1,10 @@
 @import 'css-variables.scss';
 
-$hilight-color = $highlight-color;
-$summary-details-background = $grey-color;
-$main-header-background = $frame-background-color;
-$table-even-background = $frame-background-color;
-$popup-menu-highlight-color = $highlight-color;
+$hilight-color: $highlight-color;
+$summary-details-background: $grey-color;
+$main-header-background: $frame-background-color;
+$table-even-background: $frame-background-color;
+$popup-menu-highlight-color: $highlight-color;
 $popup-menu-text-color: #000;
 $popup-menu-background-color: #fff;
 $popup-menu-text-higlight-color: #fff;
@@ -170,7 +170,7 @@ table.listResults td .view-image {
 table.listContainer {
     border: 0;
 	padding: 0;
-	margin:0;
+	margin: 0;
 	width: 100%;
 	clear: both;
 }
@@ -1261,7 +1261,7 @@ select#org_id {
 	background-color: #fff;
 }
 .itop-dashboard a {
-	cursor: unquote("not-allowed");
+	cursor: not-allowed;
 }
 .dragHover {
 	background: url(./ui-lightness/images/ui-bg_diagonals-thick_20_666666_40x40.png?v=#{$version});
@@ -1775,9 +1775,9 @@ span.refresh-button {
 	  width: auto;
 	  margin-right: 5px;
 
-	  -webkit-filter: unquote("grayscale(100%)");
-	  filter: unquote("grayscale(100%)");
-	  filter: unquote("gray");
+	  -webkit-filter: grayscale(100%);
+	  filter: grayscale(100%);
+	  filter: gray;
 	  filter: url("data:image/svg+xml;utf8,<svg version='1.1' xmlns='http://www.w3.org/2000/svg' height='0'><filter id='greyscale'><feColorMatrix type='matrix' values='0.3333 0.3333 0.3333 0 0 0.3333 0.3333 0.3333 0 0 0.3333 0.3333 0.3333 0 0 0 0 0 1 0' /></filter></svg>#greyscale");
 
 	  // IE has no filter option: at least, have some effect when hovering...

+ 20 - 0
lib/scssphp/LICENSE.md

@@ -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.

+ 47 - 0
lib/scssphp/README.md

@@ -0,0 +1,47 @@
+# scssphp
+### <http://leafo.github.io/scssphp>
+
+[![Build](https://travis-ci.org/leafo/scssphp.svg?branch=master)](http://travis-ci.org/leafo/scssphp)
+[![License](https://poser.pugx.org/leafo/scssphp/license.svg)](https://packagist.org/packages/leafo/scssphp)
+
+`scssphp` is a compiler for SCSS written in PHP.
+
+Checkout the homepage, <http://leafo.github.io/scssphp>, for directions on how to use.
+
+## Running Tests
+
+`scssphp` uses [PHPUnit](https://github.com/sebastianbergmann/phpunit) for testing.
+
+Run the following command from the root directory to run every test:
+
+    vendor/bin/phpunit tests
+
+There are several tests in the `tests/` directory:
+
+* `ApiTest.php` contains various unit tests that test the PHP interface.
+* `ExceptionTest.php` contains unit tests that test for exceptions thrown by the parser and compiler.
+* `FailingTest.php` contains tests reported in Github issues that demonstrate compatibility bugs.
+* `InputTest.php` compiles every `.scss` file in the `tests/inputs` directory
+  then compares to the respective `.css` file in the `tests/outputs` directory.
+* `ScssTest.php` extracts (ruby) `scss` tests from the `tests/scss_test.rb` file.
+* `ServerTest.php` contains functional tests for the `Server` class.
+
+When changing any of the tests in `tests/inputs`, the tests will most likely
+fail because the output has changed. Once you verify that the output is correct
+you can run the following command to rebuild all the tests:
+
+    BUILD=1 vendor/bin/phpunit tests
+
+This will compile all the tests, and save results into `tests/outputs`.
+
+To enable the `scss` compatibility tests:
+
+    TEST_SCSS_COMPAT=1 vendor/bin/phpunit tests
+
+## Coding Standard
+
+`scssphp` source conforms to [PSR2](http://www.php-fig.org/psr/psr-2/).
+
+Run the following command from the root directory to check the code for "sniffs".
+
+    vendor/bin/phpcs --standard=PSR2 bin src tests

+ 204 - 0
lib/scssphp/bin/pscss

@@ -0,0 +1,204 @@
+#!/usr/bin/env php
+<?php
+/**
+ * SCSSPHP
+ *
+ * @copyright 2012-2015 Leaf Corcoran
+ *
+ * @license http://opensource.org/licenses/MIT MIT
+ *
+ * @link http://leafo.github.io/scssphp
+ */
+
+error_reporting(E_ALL);
+
+if (version_compare(PHP_VERSION, '5.4') < 0) {
+    die('Requires PHP 5.4 or above');
+}
+
+include __DIR__ . '/../scss.inc.php';
+
+use Leafo\ScssPhp\Compiler;
+use Leafo\ScssPhp\Parser;
+use Leafo\ScssPhp\Version;
+
+$style = null;
+$loadPaths = null;
+$precision = null;
+$dumpTree = false;
+$inputFile = null;
+$changeDir = false;
+$debugInfo = false;
+$lineNumbers = false;
+$ignoreErrors = false;
+$encoding = false;
+
+/**
+ * Parse argument
+ *
+ * @param integer $i
+ * @param array $options
+ *
+ * @return string|null
+ */
+function parseArgument(&$i, $options) {
+    global $argc;
+    global $argv;
+
+    if (! preg_match('/^(?:' . implode('|', (array) $options) . ')=?(.*)/', $argv[$i], $matches)) {
+        return;
+    }
+
+    if (strlen($matches[1])) {
+        return $matches[1];
+    }
+
+    if ($i + 1 < $argc) {
+        $i++;
+
+        return $argv[$i];
+    }
+}
+
+for ($i = 1; $i < $argc; $i++) {
+    if ($argv[$i] === '-h' || $argv[$i] === '--help') {
+        $exe = $argv[0];
+
+        $HELP = <<<EOT
+Usage: $exe [options] [input-file]
+
+Options include:
+
+    -h, --help          Show this message
+    --continue-on-error Continue compilation (as best as possible) when error encountered
+    --debug-info        Annotate selectors with CSS referring to the source file and line number
+    -f=format           Set the output format (compact, compressed, crunched, expanded, or nested)
+    -i=path             Set import path
+    --iso8859-1         Use iso8859-1 encoding instead of utf-8 (default utf-8)
+    --line-numbers      Annotate selectors with comments referring to the source file and line number
+    -p=precision        Set decimal number precision (default 5)
+    -T                  Dump formatted parse tree
+    -v, --version       Print the version
+
+EOT;
+        exit($HELP);
+    }
+
+    if ($argv[$i] === '-v' || $argv[$i] === '--version') {
+        exit(Version::VERSION . "\n");
+    }
+
+    if ($argv[$i] === '--continue-on-error') {
+        $ignoreErrors = true;
+        continue;
+    }
+
+    if ($argv[$i] === '--debug-info') {
+        $debugInfo = true;
+        continue;
+    }
+
+    if ($argv[$i] === '--iso8859-1') {
+        $encoding = 'iso8859-1';
+        continue;
+    }
+
+    if ($argv[$i] === '--line-numbers' || $argv[$i] === '--line-comments') {
+        $lineNumbers = true;
+        continue;
+    }
+
+    if ($argv[$i] === '-T') {
+        $dumpTree = true;
+        continue;
+    }
+
+    $value = parseArgument($i, array('-f', '--style'));
+
+    if (isset($value)) {
+        $style = $value;
+        continue;
+    }
+
+    $value = parseArgument($i, array('-i', '--load_paths'));
+
+    if (isset($value)) {
+        $loadPaths = $value;
+        continue;
+    }
+
+    $value = parseArgument($i, array('-p', '--precision'));
+
+    if (isset($value)) {
+        $precision = $value;
+        continue;
+    }
+
+    if (file_exists($argv[$i])) {
+        $inputFile = $argv[$i];
+        continue;
+    }
+}
+
+
+if ($inputFile) {
+    $data = file_get_contents($inputFile);
+
+    $newWorkingDir = dirname(realpath($inputFile));
+    $oldWorkingDir = getcwd();
+
+    if ($oldWorkingDir !== $newWorkingDir) {
+        $changeDir = chdir($newWorkingDir);
+        $inputFile = basename($inputFile);
+    }
+} else {
+    $data = '';
+
+    while (! feof(STDIN)) {
+        $data .= fread(STDIN, 8192);
+    }
+}
+
+if ($dumpTree) {
+    $parser = new Parser($inputFile);
+
+    print_r(json_decode(json_encode($parser->parse($data)), true));
+
+    exit();
+}
+
+$scss = new Compiler();
+
+if ($debugInfo && $inputFile) {
+    $scss->setLineNumberStyle(Compiler::DEBUG_INFO);
+}
+
+if ($lineNumbers && $inputFile) {
+    $scss->setLineNumberStyle(Compiler::LINE_COMMENTS);
+}
+
+if ($ignoreErrors) {
+    $scss->setIgnoreErrors($ignoreErrors);
+}
+
+if ($loadPaths) {
+    $scss->setImportPaths(explode(PATH_SEPARATOR, $loadPaths));
+}
+
+if ($precision) {
+    $scss->setNumberPrecision($precision);
+}
+
+if ($style) {
+    $scss->setFormatter('Leafo\\ScssPhp\\Formatter\\' . ucfirst($style));
+}
+
+if ($encoding) {
+    $scss->setEncoding($encoding);
+}
+
+echo $scss->compile($data, $inputFile);
+
+if ($changeDir) {
+    chdir($oldWorkingDir);
+}

+ 34 - 0
lib/scssphp/scss.inc.php

@@ -0,0 +1,34 @@
+<?php
+/*
+ * Warning: The library original has been transformed to replace "short array notation" (i.e. []) with its pre-PHP 5.4 version (i.e. array())
+ * in order to make it run on PHP 5.3, using a script based on the excellent php-parser (https://github.com/nikic/PHP-Parser by Nikita Popov)
+ * 
+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';
+}

+ 44 - 0
lib/scssphp/src/Base/Range.php

@@ -0,0 +1,44 @@
+<?php
+/**
+ * SCSSPHP
+ *
+ * @copyright 2015 Leaf Corcoran
+ *
+ * @license http://opensource.org/licenses/MIT MIT
+ *
+ * @link http://leafo.github.io/scssphp
+ */
+namespace Leafo\ScssPhp\Base;
+
+/**
+ * Range
+ *
+ * @author Anthon Pang <anthon.pang@gmail.com>
+ */
+class Range
+{
+    public $first;
+    public $last;
+    /**
+     * Initialize range
+     *
+     * @param integer|float $first
+     * @param integer|float $last
+     */
+    public function __construct($first, $last)
+    {
+        $this->first = $first;
+        $this->last = $last;
+    }
+    /**
+     * Test for inclusion in range
+     *
+     * @param integer|float $value
+     *
+     * @return boolean
+     */
+    public function includes($value)
+    {
+        return $value >= $this->first && $value <= $this->last;
+    }
+}

+ 52 - 0
lib/scssphp/src/Block.php

@@ -0,0 +1,52 @@
+<?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;
+}

Plik diff jest za duży
+ 26 - 0
lib/scssphp/src/Colors.php


+ 3989 - 0
lib/scssphp/src/Compiler.php

@@ -0,0 +1,3989 @@
+<?php
+/**
+ * SCSSPHP
+ *
+ * @copyright 2012-2015 Leaf Corcoran
+ *
+ * @license http://opensource.org/licenses/MIT MIT
+ *
+ * @link http://leafo.github.io/scssphp
+ */
+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;
+/**
+ * The scss compiler and parser.
+ *
+ * Converting SCSS to CSS is a three stage process. The incoming file is parsed
+ * by `Parser` into a syntax tree, then it is compiled into another tree
+ * representing the CSS structure by `Compiler`. The CSS tree is fed into a
+ * formatter, like `Formatter` which then outputs CSS as a string.
+ *
+ * During the first compile, all values are *reduced*, which means that their
+ * types are brought to the lowest form before being dump as strings. This
+ * handles math equations, variable dereferences, and the like.
+ *
+ * The `compile` function of `Compiler` is the entry point.
+ *
+ * In summary:
+ *
+ * The `Compiler` class creates an instance of the parser, feeds it SCSS code,
+ * then transforms the resulting tree to a CSS tree. This class also holds the
+ * evaluation context, such as all available mixins and variables at any given
+ * time.
+ *
+ * The `Parser` class is only concerned with parsing its input.
+ *
+ * The `Formatter` takes a CSS tree, and dumps it to a formatted string,
+ * handling things like indentation.
+ */
+/**
+ * SCSS compiler
+ *
+ * @author Leaf Corcoran <leafot@gmail.com>
+ */
+class Compiler
+{
+    const LINE_COMMENTS = 1;
+    const DEBUG_INFO = 2;
+    const WITH_RULE = 1;
+    const WITH_MEDIA = 2;
+    const WITH_SUPPORTS = 4;
+    const WITH_ALL = 7;
+    /**
+     * @var array
+     */
+    protected static $operatorNames = array('+' => 'add', '-' => 'sub', '*' => 'mul', '/' => 'div', '%' => 'mod', '==' => 'eq', '!=' => 'neq', '<' => 'lt', '>' => 'gt', '<=' => 'lte', '>=' => 'gte', '<=>' => 'cmp');
+    /**
+     * @var array
+     */
+    protected static $namespaces = array('special' => '%', 'mixin' => '@', 'function' => '^');
+    public static $true = array(Type::T_KEYWORD, 'true');
+    public static $false = array(Type::T_KEYWORD, 'false');
+    public static $null = array(Type::T_NULL);
+    public static $nullString = array(Type::T_STRING, '', array());
+    public static $defaultValue = array(Type::T_KEYWORD, '');
+    public static $selfSelector = array(Type::T_SELF);
+    public static $emptyList = array(Type::T_LIST, '', array());
+    public static $emptyMap = array(Type::T_MAP, array(), array());
+    public static $emptyString = array(Type::T_STRING, '"', array());
+    public static $with = array(Type::T_KEYWORD, 'with');
+    public static $without = array(Type::T_KEYWORD, 'without');
+    protected $importPaths = array('');
+    protected $importCache = array();
+    protected $importedFiles = array();
+    protected $userFunctions = array();
+    protected $registeredVars = array();
+    protected $registeredFeatures = array('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 $parser;
+    private $sourceIndex;
+    private $sourceLine;
+    private $sourceColumn;
+    private $stderr;
+    private $shouldEvaluate;
+    private $ignoreErrors;
+    /**
+     * Constructor
+     */
+    public function __construct()
+    {
+        $this->parsedFiles = array();
+        $this->sourceNames = array();
+    }
+    /**
+     * Compile scss
+     *
+     * @api
+     *
+     * @param string $code
+     * @param string $path
+     *
+     * @return string
+     */
+    public function compile($code, $path = null)
+    {
+        $locale = setlocale(LC_NUMERIC, 0);
+        setlocale(LC_NUMERIC, 'C');
+        $this->indentLevel = -1;
+        $this->commentsSeen = array();
+        $this->extends = array();
+        $this->extendsMap = array();
+        $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->injectVariables($this->registeredVars);
+        $this->compileRoot($tree);
+        $this->popEnv();
+        $out = $this->formatter->format($this->scope);
+        setlocale(LC_NUMERIC, $locale);
+        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?
+     *
+     * @param array $target
+     * @param array $origin
+     *
+     * @return boolean
+     */
+    protected function isSelfExtend($target, $origin)
+    {
+        foreach ($origin as $sel) {
+            if (in_array($target, $sel)) {
+                return true;
+            }
+        }
+        return false;
+    }
+    /**
+     * Push extends
+     *
+     * @param array     $target
+     * @param array     $origin
+     * @param \stdClass $block
+     */
+    protected function pushExtends($target, $origin, $block)
+    {
+        if ($this->isSelfExtend($target, $origin)) {
+            return;
+        }
+        $i = count($this->extends);
+        $this->extends[] = array($target, $origin, $block);
+        foreach ($target as $part) {
+            if (isset($this->extendsMap[$part])) {
+                $this->extendsMap[$part][] = $i;
+            } else {
+                $this->extendsMap[$part] = array($i);
+            }
+        }
+    }
+    /**
+     * Make output block
+     *
+     * @param string $type
+     * @param array  $selectors
+     *
+     * @return \Leafo\ScssPhp\Formatter\OutputBlock
+     */
+    protected function makeOutputBlock($type, $selectors = null)
+    {
+        $out = new OutputBlock();
+        $out->type = $type;
+        $out->lines = array();
+        $out->children = array();
+        $out->parent = $this->scope;
+        $out->selectors = $selectors;
+        $out->depth = $this->env->depth;
+        return $out;
+    }
+    /**
+     * Compile root
+     *
+     * @param \Leafo\ScssPhp\Block $rootBlock
+     */
+    protected function compileRoot(Block $rootBlock)
+    {
+        $this->rootBlock = $this->scope = $this->makeOutputBlock(Type::T_ROOT);
+        $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 \Leafo\ScssPhp\Formatter\OutputBlock $block
+     * @param string                               $parentKey
+     */
+    protected function flattenSelectors(OutputBlock $block, $parentKey = null)
+    {
+        if ($block->selectors) {
+            $selectors = array();
+            foreach ($block->selectors as $s) {
+                $selectors[] = $s;
+                if (!is_array($s)) {
+                    continue;
+                }
+                // check extends
+                if (!empty($this->extendsMap)) {
+                    $this->matchExtends($s, $selectors);
+                    // remove duplicates
+                    array_walk($selectors, function (&$value) {
+                        $value = serialize($value);
+                    });
+                    $selectors = array_unique($selectors);
+                    array_walk($selectors, function (&$value) {
+                        $value = unserialize($value);
+                    });
+                }
+            }
+            $block->selectors = array();
+            $placeholderSelector = false;
+            foreach ($selectors as $selector) {
+                if ($this->hasSelectorPlaceholder($selector)) {
+                    $placeholderSelector = true;
+                    continue;
+                }
+                $block->selectors[] = $this->compileSelector($selector);
+            }
+            if ($placeholderSelector && 0 === count($block->selectors) && null !== $parentKey) {
+                unset($block->parent->children[$parentKey]);
+                return;
+            }
+        }
+        foreach ($block->children as $key => $child) {
+            $this->flattenSelectors($child, $key);
+        }
+    }
+    /**
+     * Match extends
+     *
+     * @param array   $selector
+     * @param array   $out
+     * @param integer $from
+     * @param boolean $initial
+     */
+    protected function matchExtends($selector, &$out, $from = 0, $initial = true)
+    {
+        foreach ($selector as $i => $part) {
+            if ($i < $from) {
+                continue;
+            }
+            if ($this->matchExtendsSingle($part, $origin)) {
+                $before = array_slice($selector, 0, $i);
+                $after = array_slice($selector, $i + 1);
+                $s = count($before);
+                foreach ($origin as $new) {
+                    $k = 0;
+                    // remove shared parts
+                    if ($initial) {
+                        while ($k < $s && isset($new[$k]) && $before[$k] === $new[$k]) {
+                            $k++;
+                        }
+                    }
+                    $result = array_merge($before, $k > 0 ? array_slice($new, $k) : $new, $after);
+                    if ($result === $selector) {
+                        continue;
+                    }
+                    $out[] = $result;
+                    // recursively check for more matches
+                    $this->matchExtends($result, $out, $i, false);
+                    // selector sequence merging
+                    if (!empty($before) && count($new) > 1) {
+                        $result2 = array_merge(array_slice($new, 0, -1), $k > 0 ? array_slice($before, $k) : $before, array_slice($new, -1), $after);
+                        $out[] = $result2;
+                    }
+                }
+            }
+        }
+    }
+    /**
+     * Match extends single
+     *
+     * @param array $rawSingle
+     * @param array $outOrigin
+     *
+     * @return boolean
+     */
+    protected function matchExtendsSingle($rawSingle, &$outOrigin)
+    {
+        $counts = array();
+        $single = array();
+        foreach ($rawSingle as $part) {
+            // matches Number
+            if (!is_string($part)) {
+                return false;
+            }
+            if (!preg_match('/^[\\[.:#%]/', $part) && count($single)) {
+                $single[count($single) - 1] .= $part;
+            } else {
+                $single[] = $part;
+            }
+        }
+        foreach ($single as $part) {
+            if (isset($this->extendsMap[$part])) {
+                foreach ($this->extendsMap[$part] as $idx) {
+                    $counts[$idx] = isset($counts[$idx]) ? $counts[$idx] + 1 : 1;
+                }
+            }
+        }
+        $outOrigin = array();
+        $found = false;
+        foreach ($counts as $idx => $count) {
+            list($target, $origin, ) = $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) {
+                // prevent infinite loop when target extends itself
+                if ($this->isSelfExtend($single, $origin)) {
+                    return false;
+                }
+                $combined = $this->combineSelectorSingle(end($new), $rem);
+                if (count(array_diff($combined, $origin[$j][count($origin[$j]) - 1]))) {
+                    $origin[$j][count($origin[$j]) - 1] = $combined;
+                }
+            }
+            $outOrigin = array_merge($outOrigin, $origin);
+            $found = true;
+        }
+        return $found;
+    }
+    /**
+     * Combine selector single
+     *
+     * @param array $base
+     * @param array $other
+     *
+     * @return array
+     */
+    protected function combineSelectorSingle($base, $other)
+    {
+        $tag = array();
+        $out = array();
+        $wasTag = true;
+        foreach (array($base, $other) as $single) {
+            foreach ($single as $part) {
+                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 (count($tag)) {
+            array_unshift($out, $tag[0]);
+        }
+        return $out;
+    }
+    /**
+     * Compile media
+     *
+     * @param \Leafo\ScssPhp\Block $media
+     */
+    protected function compileMedia(Block $media)
+    {
+        $this->pushEnv($media);
+        $mediaQuery = $this->compileMediaQuery($this->multiplyMedia($this->env));
+        if (!empty($mediaQuery)) {
+            $this->scope = $this->makeOutputBlock(Type::T_MEDIA, array($mediaQuery));
+            $parentScope = $this->mediaParent($this->scope);
+            $parentScope->children[] = $this->scope;
+            // top level properties in a media cause it to be wrapped
+            $needsWrap = false;
+            foreach ($media->children as $child) {
+                $type = $child[0];
+                if ($type !== Type::T_BLOCK && $type !== Type::T_MEDIA && $type !== Type::T_DIRECTIVE && $type !== Type::T_IMPORT) {
+                    $needsWrap = true;
+                    break;
+                }
+            }
+            if ($needsWrap) {
+                $wrapped = new Block();
+                $wrapped->sourceIndex = $media->sourceIndex;
+                $wrapped->sourceLine = $media->sourceLine;
+                $wrapped->sourceColumn = $media->sourceColumn;
+                $wrapped->selectors = array();
+                $wrapped->comments = array();
+                $wrapped->parent = $media;
+                $wrapped->children = $media->children;
+                $media->children = array(array(Type::T_BLOCK, $wrapped));
+            }
+            $this->compileChildrenNoReturn($media->children, $this->scope);
+            $this->scope = $this->scope->parent;
+        }
+        $this->popEnv();
+    }
+    /**
+     * Media parent
+     *
+     * @param \Leafo\ScssPhp\Formatter\OutputBlock $scope
+     *
+     * @return \Leafo\ScssPhp\Formatter\OutputBlock
+     */
+    protected function mediaParent(OutputBlock $scope)
+    {
+        while (!empty($scope->parent)) {
+            if (!empty($scope->type) && $scope->type !== Type::T_MEDIA) {
+                break;
+            }
+            $scope = $scope->parent;
+        }
+        return $scope;
+    }
+    /**
+     * Compile directive
+     *
+     * @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, array($s));
+        } else {
+            $this->compileNestedBlock($block, array($s));
+        }
+    }
+    /**
+     * Compile at-root
+     *
+     * @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 = array();
+            $wrapped->parent = $block;
+            $wrapped->children = $block->children;
+            $block->children = array(array(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 = array();
+            $b->comments = $e->block->comments;
+            $b->parent = null;
+            if ($newBlock) {
+                $type = isset($newBlock->type) ? $newBlock->type : Type::T_BLOCK;
+                $b->children = array(array($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 array($type, $newBlock);
+    }
+    /**
+     * Compile @at-root's with: inclusion / without: exclusion into filter flags
+     *
+     * @param array $with
+     *
+     * @return integer
+     */
+    private function compileWith($with)
+    {
+        static $mapping = array('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(array($with, self::$with))) {
+            $without = self::WITH_ALL;
+            $list = $this->coerceList($this->libMapGet(array($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(array($with, self::$without))) {
+            $without = 0;
+            $list = $this->coerceList($this->libMapGet(array($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 = array();
+        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 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->compileChildrenNoReturn($block->children, $this->scope);
+        $this->scope = $this->scope->parent;
+        $this->popEnv();
+    }
+    /**
+     * Recursively compiles a block.
+     *
+     * A block is analogous to a CSS block in most cases. A single SCSS document
+     * is encapsulated in a block when parsed, but it does not have parent tags
+     * so all of its children appear on the root level when compiled.
+     *
+     * Blocks are made up of selectors and children.
+     *
+     * The children of a block are just all the blocks that are defined within.
+     *
+     * Compiling the block involves pushing a fresh environment on the stack,
+     * and iterating through the props, compiling each one.
+     *
+     * @see Compiler::compileChild()
+     *
+     * @param \Leafo\ScssPhp\Block $block
+     */
+    protected function compileBlock(Block $block)
+    {
+        $env = $this->pushEnv($block);
+        $env->selectors = $this->evalSelectors($block->selectors);
+        $out = $this->makeOutputBlock(null);
+        if (isset($this->lineNumberStyle) && count($env->selectors) && count($block->children)) {
+            $annotation = $this->makeOutputBlock(Type::T_COMMENT);
+            $annotation->depth = 0;
+            $file = $this->sourceNames[$block->sourceIndex];
+            $line = $block->sourceLine;
+            switch ($this->lineNumberStyle) {
+                case self::LINE_COMMENTS:
+                    $annotation->lines[] = '/* line ' . $line . ', ' . $file . ' */';
+                    break;
+                case self::DEBUG_INFO:
+                    $annotation->lines[] = '@media -sass-debug-info{filename{font-family:"' . $file . '"}line{font-family:' . $line . '}}';
+                    break;
+            }
+            $this->scope->children[] = $annotation;
+        }
+        $this->scope->children[] = $out;
+        if (count($block->children)) {
+            $out->selectors = $this->multiplySelectors($env);
+            $this->compileChildrenNoReturn($block->children, $out);
+        }
+        $this->formatter->stripSemicolon($out->lines);
+        $this->popEnv();
+    }
+    /**
+     * Compile root level comment
+     *
+     * @param array $block
+     */
+    protected function compileComment($block)
+    {
+        $out = $this->makeOutputBlock(Type::T_COMMENT);
+        $out->lines[] = $block[1];
+        $this->scope->children[] = $out;
+    }
+    /**
+     * Evaluate selectors
+     *
+     * @param array $selectors
+     *
+     * @return array
+     */
+    protected function evalSelectors($selectors)
+    {
+        $this->shouldEvaluate = false;
+        $selectors = array_map(array($this, 'evalSelector'), $selectors);
+        // after evaluating interpolates, we might need a second pass
+        if ($this->shouldEvaluate) {
+            $buffer = $this->collapseSelectors($selectors);
+            $parser = $this->parserFactory(__METHOD__);
+            if ($parser->parseSelector($buffer, $newSelectors)) {
+                $selectors = array_map(array($this, 'evalSelector'), $newSelectors);
+            }
+        }
+        return $selectors;
+    }
+    /**
+     * Evaluate selector
+     *
+     * @param array $selector
+     *
+     * @return array
+     */
+    protected function evalSelector($selector)
+    {
+        return array_map(array($this, 'evalSelectorPart'), $selector);
+    }
+    /**
+     * Evaluate selector part; replaces all the interpolates, stripping quotes
+     *
+     * @param array $part
+     *
+     * @return array
+     */
+    protected function evalSelectorPart($part)
+    {
+        foreach ($part as &$p) {
+            if (is_array($p) && ($p[0] === Type::T_INTERPOLATE || $p[0] === Type::T_STRING)) {
+                $p = $this->compileValue($p);
+                // force re-evaluation
+                if (strpos($p, '&') !== false || strpos($p, ',') !== false) {
+                    $this->shouldEvaluate = true;
+                }
+            } elseif (is_string($p) && strlen($p) >= 2 && ($first = $p[0]) && ($first === '"' || $first === '\'') && substr($p, -1) === $first) {
+                $p = substr($p, 1, -1);
+            }
+        }
+        return $this->flattenSelectorSingle($part);
+    }
+    /**
+     * Collapse selectors
+     *
+     * @param array $selectors
+     *
+     * @return string
+     */
+    protected function collapseSelectors($selectors)
+    {
+        $parts = array();
+        foreach ($selectors as $selector) {
+            $output = '';
+            array_walk_recursive($selector, function ($value, $key) use(&$output) {
+                $output .= $value;
+            });
+            $parts[] = $output;
+        }
+        return implode(', ', $parts);
+    }
+    /**
+     * Flatten selector single; joins together .classes and #ids
+     *
+     * @param array $single
+     *
+     * @return array
+     */
+    protected function flattenSelectorSingle($single)
+    {
+        $joined = array();
+        foreach ($single as $part) {
+            if (empty($joined) || !is_string($part) || preg_match('/[\\[.:#%]/', $part)) {
+                $joined[] = $part;
+                continue;
+            }
+            if (is_array(end($joined))) {
+                $joined[] = $part;
+            } else {
+                $joined[count($joined) - 1] .= $part;
+            }
+        }
+        return $joined;
+    }
+    /**
+     * Compile selector to string; self(&) should have been replaced by now
+     *
+     * @param array $selector
+     *
+     * @return string
+     */
+    protected function compileSelector($selector)
+    {
+        if (!is_array($selector)) {
+            return $selector;
+        }
+        return implode(' ', array_map(array($this, 'compileSelectorPart'), $selector));
+    }
+    /**
+     * Compile selector part
+     *
+     * @param arary $piece
+     *
+     * @return string
+     */
+    protected function compileSelectorPart($piece)
+    {
+        foreach ($piece as &$p) {
+            if (!is_array($p)) {
+                continue;
+            }
+            switch ($p[0]) {
+                case Type::T_SELF:
+                    $p = '&';
+                    break;
+                default:
+                    $p = $this->compileValue($p);
+                    break;
+            }
+        }
+        return implode($piece);
+    }
+    /**
+     * Has selector placeholder?
+     *
+     * @param array $selector
+     *
+     * @return boolean
+     */
+    protected function hasSelectorPlaceholder($selector)
+    {
+        if (!is_array($selector)) {
+            return false;
+        }
+        foreach ($selector as $parts) {
+            foreach ($parts as $part) {
+                if (strlen($part) && '%' === $part[0]) {
+                    return true;
+                }
+            }
+        }
+        return false;
+    }
+    /**
+     * Compile children and return result
+     *
+     * @param array                                $stms
+     * @param \Leafo\ScssPhp\Formatter\OutputBlock $out
+     *
+     * @return array
+     */
+    protected function compileChildren($stms, OutputBlock $out)
+    {
+        foreach ($stms as $stm) {
+            $ret = $this->compileChild($stm, $out);
+            if (isset($ret)) {
+                return $ret;
+            }
+        }
+    }
+    /**
+     * 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
+     *
+     * @param array $queryList
+     *
+     * @return string
+     */
+    protected function compileMediaQuery($queryList)
+    {
+        $out = '@media';
+        $first = true;
+        foreach ($queryList as $query) {
+            $type = null;
+            $parts = array();
+            foreach ($query as $q) {
+                switch ($q[0]) {
+                    case Type::T_MEDIA_TYPE:
+                        if ($type) {
+                            $type = $this->mergeMediaTypes($type, array_map(array($this, 'compileValue'), array_slice($q, 1)));
+                            if (empty($type)) {
+                                // merge failed
+                                return null;
+                            }
+                        } else {
+                            $type = array_map(array($this, 'compileValue'), array_slice($q, 1));
+                        }
+                        break;
+                    case Type::T_MEDIA_EXPRESSION:
+                        if (isset($q[2])) {
+                            $parts[] = '(' . $this->compileValue($q[1]) . $this->formatter->assignSeparator . $this->compileValue($q[2]) . ')';
+                        } else {
+                            $parts[] = '(' . $this->compileValue($q[1]) . ')';
+                        }
+                        break;
+                    case Type::T_MEDIA_VALUE:
+                        $parts[] = $this->compileValue($q[1]);
+                        break;
+                }
+            }
+            if ($type) {
+                array_unshift($parts, implode(' ', array_filter($type)));
+            }
+            if (!empty($parts)) {
+                if ($first) {
+                    $first = false;
+                    $out .= ' ';
+                } else {
+                    $out .= $this->formatter->tagSeparator;
+                }
+                $out .= implode(' and ', $parts);
+            }
+        }
+        return $out;
+    }
+    /**
+     * Merge media types
+     *
+     * @param array $type1
+     * @param array $type2
+     *
+     * @return array|null
+     */
+    protected function mergeMediaTypes($type1, $type2)
+    {
+        if (empty($type1)) {
+            return $type2;
+        }
+        if (empty($type2)) {
+            return $type1;
+        }
+        $m1 = '';
+        $t1 = '';
+        if (count($type1) > 1) {
+            $m1 = strtolower($type1[0]);
+            $t1 = strtolower($type1[1]);
+        } else {
+            $t1 = strtolower($type1[0]);
+        }
+        $m2 = '';
+        $t2 = '';
+        if (count($type2) > 1) {
+            $m2 = strtolower($type2[0]);
+            $t2 = strtolower($type2[1]);
+        } else {
+            $t2 = strtolower($type2[0]);
+        }
+        if ($m1 === Type::T_NOT ^ $m2 === Type::T_NOT) {
+            if ($t1 === $t2) {
+                return null;
+            }
+            return array($m1 === Type::T_NOT ? $m2 : $m1, $m1 === Type::T_NOT ? $t2 : $t1);
+        }
+        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(Type::T_NOT, $t1);
+        }
+        if ($t1 !== $t2) {
+            return null;
+        }
+        // t1 == t2, neither m1 nor m2 are "not"
+        return array(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 boolean $once
+     *
+     * @return boolean
+     */
+    protected function compileImport($rawPath, $out, $once = false)
+    {
+        if ($rawPath[0] === Type::T_STRING) {
+            $path = $this->compileStringContent($rawPath);
+            if ($path = $this->findImport($path)) {
+                if (!$once || !in_array($path, $this->importedFiles)) {
+                    $this->importFile($path, $out);
+                    $this->importedFiles[] = $path;
+                }
+                return true;
+            }
+            return false;
+        }
+        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] !== Type::T_STRING) {
+                    return false;
+                }
+            }
+            foreach ($rawPath[2] as $path) {
+                $this->compileImport($path, $out);
+            }
+            return true;
+        }
+        return false;
+    }
+    /**
+     * Compile child; returns a value to halt execution
+     *
+     * @param array                                $child
+     * @param \Leafo\ScssPhp\Formatter\OutputBlock $out
+     *
+     * @return array
+     */
+    protected function compileChild($child, OutputBlock $out)
+    {
+        $this->sourceIndex = isset($child[Parser::SOURCE_INDEX]) ? $child[Parser::SOURCE_INDEX] : null;
+        $this->sourceLine = isset($child[Parser::SOURCE_LINE]) ? $child[Parser::SOURCE_LINE] : -1;
+        $this->sourceColumn = isset($child[Parser::SOURCE_COLUMN]) ? $child[Parser::SOURCE_COLUMN] : -1;
+        switch ($child[0]) {
+            case Type::T_SCSSPHP_IMPORT_ONCE:
+                list(, $rawPath) = $child;
+                $rawPath = $this->reduce($rawPath);
+                if (!$this->compileImport($rawPath, $out, true)) {
+                    $out->lines[] = '@import ' . $this->compileValue($rawPath) . ';';
+                }
+                break;
+            case Type::T_IMPORT:
+                list(, $rawPath) = $child;
+                $rawPath = $this->reduce($rawPath);
+                if (!$this->compileImport($rawPath, $out)) {
+                    $out->lines[] = '@import ' . $this->compileValue($rawPath) . ';';
+                }
+                break;
+            case Type::T_DIRECTIVE:
+                $this->compileDirective($child[1]);
+                break;
+            case Type::T_AT_ROOT:
+                $this->compileAtRoot($child[1]);
+                break;
+            case Type::T_MEDIA:
+                $this->compileMedia($child[1]);
+                break;
+            case Type::T_BLOCK:
+                $this->compileBlock($child[1]);
+                break;
+            case Type::T_CHARSET:
+                if (!$this->charsetSeen) {
+                    $this->charsetSeen = true;
+                    $out->lines[] = '@charset ' . $this->compileValue($child[1]) . ';';
+                }
+                break;
+            case Type::T_ASSIGN:
+                list(, $name, $value) = $child;
+                if ($name[0] === Type::T_VARIABLE) {
+                    $flags = isset($child[3]) ? $child[3] : array();
+                    $isDefault = in_array('!default', $flags);
+                    $isGlobal = in_array('!global', $flags);
+                    if ($isGlobal) {
+                        $this->set($name[1], $this->reduce($value), false, $this->rootEnv);
+                        break;
+                    }
+                    $shouldSet = $isDefault && (($result = $this->get($name[1], false)) === null || $result === self::$null);
+                    if (!$isDefault || $shouldSet) {
+                        $this->set($name[1], $this->reduce($value));
+                    }
+                    break;
+                }
+                $compiledName = $this->compileValue($name);
+                // handle shorthand syntax: size / line-height
+                if ($compiledName === 'font') {
+                    if ($value[0] === Type::T_EXPRESSION && $value[1] === '/') {
+                        $value = $this->expToString($value);
+                    } elseif ($value[0] === Type::T_LIST) {
+                        foreach ($value[2] as &$item) {
+                            if ($item[0] === Type::T_EXPRESSION && $item[1] === '/') {
+                                $item = $this->expToString($item);
+                            }
+                        }
+                    }
+                }
+                // if the value reduces to null from something else then
+                // the property should be discarded
+                if ($value[0] !== Type::T_NULL) {
+                    $value = $this->reduce($value);
+                    if ($value[0] === Type::T_NULL || $value === self::$nullString) {
+                        break;
+                    }
+                }
+                $compiledValue = $this->compileValue($value);
+                $out->lines[] = $this->formatter->property($compiledName, $compiledValue);
+                break;
+            case Type::T_COMMENT:
+                if ($out->type === Type::T_ROOT) {
+                    $this->compileComment($child);
+                    break;
+                }
+                $out->lines[] = $child[1];
+                break;
+            case Type::T_MIXIN:
+            case Type::T_FUNCTION:
+                list(, $block) = $child;
+                $this->set(self::$namespaces[$block->type] . $block->name, $block);
+                break;
+            case Type::T_EXTEND:
+                list(, $selectors) = $child;
+                foreach ($selectors as $sel) {
+                    $results = $this->evalSelectors(array($sel));
+                    foreach ($results as $result) {
+                        // only use the first one
+                        $result = current($result);
+                        $this->pushExtends($result, $out->selectors, $child);
+                    }
+                }
+                break;
+            case Type::T_IF:
+                list(, $if) = $child;
+                if ($this->isTruthy($this->reduce($if->cond, true))) {
+                    return $this->compileChildren($if->children, $out);
+                }
+                foreach ($if->cases as $case) {
+                    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 Type::T_EACH:
+                list(, $each) = $child;
+                $list = $this->coerceList($this->reduce($each->list));
+                $this->pushEnv();
+                foreach ($list[2] as $item) {
+                    if (count($each->vars) === 1) {
+                        $this->set($each->vars[0], $item, true);
+                    } else {
+                        list(, , $values) = $this->coerceList($item);
+                        foreach ($each->vars as $i => $var) {
+                            $this->set($var, isset($values[$i]) ? $values[$i] : self::$null, true);
+                        }
+                    }
+                    $ret = $this->compileChildren($each->children, $out);
+                    if ($ret) {
+                        if ($ret[0] !== Type::T_CONTROL) {
+                            $this->popEnv();
+                            return $ret;
+                        }
+                        if ($ret[1]) {
+                            break;
+                        }
+                    }
+                }
+                $this->popEnv();
+                break;
+            case Type::T_WHILE:
+                list(, $while) = $child;
+                while ($this->isTruthy($this->reduce($while->cond, true))) {
+                    $ret = $this->compileChildren($while->children, $out);
+                    if ($ret) {
+                        if ($ret[0] !== Type::T_CONTROL) {
+                            return $ret;
+                        }
+                        if ($ret[1]) {
+                            break;
+                        }
+                    }
+                }
+                break;
+            case Type::T_FOR:
+                list(, $for) = $child;
+                $start = $this->reduce($for->start, true);
+                $start = $start[1];
+                $end = $this->reduce($for->end, true);
+                $end = $end[1];
+                $d = $start < $end ? 1 : -1;
+                while (true) {
+                    if (!$for->until && $start - $d == $end || $for->until && $start == $end) {
+                        break;
+                    }
+                    $this->set($for->var, new Node\Number($start, ''));
+                    $start += $d;
+                    $ret = $this->compileChildren($for->children, $out);
+                    if ($ret) {
+                        if ($ret[0] !== Type::T_CONTROL) {
+                            return $ret;
+                        }
+                        if ($ret[1]) {
+                            break;
+                        }
+                    }
+                }
+                break;
+            case Type::T_BREAK:
+                return array(Type::T_CONTROL, true);
+            case Type::T_CONTINUE:
+                return array(Type::T_CONTROL, false);
+            case Type::T_RETURN:
+                return $this->reduce($child[1], true);
+            case Type::T_NESTED_PROPERTY:
+                list(, $prop) = $child;
+                $prefixed = array();
+                $prefix = $this->compileValue($prop->prefix) . '-';
+                foreach ($prop->children as $child) {
+                    switch ($child[0]) {
+                        case Type::T_ASSIGN:
+                            array_unshift($child[1][2], $prefix);
+                            break;
+                        case Type::T_NESTED_PROPERTY:
+                            array_unshift($child[1]->prefix[2], $prefix);
+                            break;
+                    }
+                    $prefixed[] = $child;
+                }
+                $this->compileChildrenNoReturn($prefixed, $out);
+                break;
+            case Type::T_INCLUDE:
+                // including a mixin
+                list(, $name, $argValues, $content) = $child;
+                $mixin = $this->get(self::$namespaces['mixin'] . $name, false);
+                if (!$mixin) {
+                    $this->throwError("Undefined mixin {$name}");
+                    break;
+                }
+                $callingScope = $this->getStoreEnv();
+                // push scope, apply args
+                $this->pushEnv();
+                $this->env->depth--;
+                if (isset($content)) {
+                    $content->scope = $callingScope;
+                    $this->setRaw(self::$namespaces['special'] . 'content', $content, $this->env);
+                }
+                if (isset($mixin->args)) {
+                    $this->applyArguments($mixin->args, $argValues);
+                }
+                $this->env->marker = 'mixin';
+                $this->compileChildrenNoReturn($mixin->children, $out);
+                $this->popEnv();
+                break;
+            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');
+                    break;
+                }
+                $storeEnv = $this->storeEnv;
+                $this->storeEnv = $content->scope;
+                $this->compileChildrenNoReturn($content->children, $out);
+                $this->storeEnv = $storeEnv;
+                break;
+            case Type::T_DEBUG:
+                list(, $value) = $child;
+                $line = $this->sourceLine;
+                $value = $this->compileValue($this->reduce($value, true));
+                fwrite($this->stderr, "Line {$line} DEBUG: {$value}\n");
+                break;
+            case Type::T_WARN:
+                list(, $value) = $child;
+                $line = $this->sourceLine;
+                $value = $this->compileValue($this->reduce($value, true));
+                fwrite($this->stderr, "Line {$line} WARN: {$value}\n");
+                break;
+            case Type::T_ERROR:
+                list(, $value) = $child;
+                $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']}");
+        }
+    }
+    /**
+     * Reduce expression to string
+     *
+     * @param array $exp
+     *
+     * @return array
+     */
+    protected function expToString($exp)
+    {
+        list(, $op, $left, $right, , $whiteLeft, $whiteRight) = $exp;
+        $content = array($this->reduce($left));
+        if ($whiteLeft) {
+            $content[] = ' ';
+        }
+        $content[] = $op;
+        if ($whiteRight) {
+            $content[] = ' ';
+        }
+        $content[] = $this->reduce($right);
+        return array(Type::T_STRING, '', $content);
+    }
+    /**
+     * Is truthy?
+     *
+     * @param array $value
+     *
+     * @return array
+     */
+    protected function isTruthy($value)
+    {
+        return $value !== self::$false && $value !== self::$null;
+    }
+    /**
+     * Should $value cause its operand to eval
+     *
+     * @param array $value
+     *
+     * @return boolean
+     */
+    protected function shouldEval($value)
+    {
+        switch ($value[0]) {
+            case Type::T_EXPRESSION:
+                if ($value[1] === '/') {
+                    return $this->shouldEval($value[2], $value[3]);
+                }
+            // fall-thru
+            case Type::T_VARIABLE:
+            case Type::T_FUNCTION_CALL:
+                return true;
+        }
+        return false;
+    }
+    /**
+     * Reduce value
+     *
+     * @param array   $value
+     * @param boolean $inExp
+     *
+     * @return array
+     */
+    protected function reduce($value, $inExp = false)
+    {
+        list($type) = $value;
+        switch ($type) {
+            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);
+                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);
+                }
+                $left = $this->coerceForExpression($left);
+                $right = $this->coerceForExpression($right);
+                $ltype = $left[0];
+                $rtype = $right[0];
+                $ucOpName = ucfirst($opName);
+                $ucLType = ucfirst($ltype);
+                $ucRType = ucfirst($rtype);
+                // this tries:
+                // 1. op[op name][left type][right type]
+                // 2. op[left type][right type] (passing the op as first arg
+                // 3. op[op name]
+                $fn = "op{$ucOpName}{$ucLType}{$ucRType}";
+                if (is_callable(array($this, $fn)) || ($fn = "op{$ucLType}{$ucRType}") && is_callable(array($this, $fn)) && ($passOp = true) || ($fn = "op{$ucOpName}") && is_callable(array($this, $fn)) && ($genOp = true)) {
+                    $coerceUnit = false;
+                    if (!isset($genOp) && $left[0] === Type::T_NUMBER && $right[0] === Type::T_NUMBER) {
+                        $coerceUnit = true;
+                        switch ($opName) {
+                            case 'mul':
+                                $targetUnit = $left[2];
+                                foreach ($right[2] as $unit => $exp) {
+                                    $targetUnit[$unit] = (isset($targetUnit[$unit]) ? $targetUnit[$unit] : 0) + $exp;
+                                }
+                                break;
+                            case 'div':
+                                $targetUnit = $left[2];
+                                foreach ($right[2] as $unit => $exp) {
+                                    $targetUnit[$unit] = (isset($targetUnit[$unit]) ? $targetUnit[$unit] : 0) - $exp;
+                                }
+                                break;
+                            case 'mod':
+                                $targetUnit = $left[2];
+                                break;
+                            default:
+                                $targetUnit = $left->unitless() ? $right[2] : $left[2];
+                        }
+                        if (!$left->unitless() && !$right->unitless()) {
+                            $left = $left->normalize();
+                            $right = $right->normalize();
+                        }
+                    }
+                    $shouldEval = $inParens || $inExp;
+                    if (isset($passOp)) {
+                        $out = $this->{$fn}($op, $left, $right, $shouldEval);
+                    } else {
+                        $out = $this->{$fn}($left, $right, $shouldEval);
+                    }
+                    if (isset($out)) {
+                        if ($coerceUnit && $out[0] === Type::T_NUMBER) {
+                            $out = $out->coerce($targetUnit);
+                        }
+                        return $out;
+                    }
+                }
+                return $this->expToString($value);
+            case Type::T_UNARY:
+                list(, $op, $exp, $inParens) = $value;
+                $inExp = $inExp || $this->shouldEval($exp);
+                $exp = $this->reduce($exp);
+                if ($exp[0] === Type::T_NUMBER) {
+                    switch ($op) {
+                        case '+':
+                            return new Node\Number($exp[1], $exp[2]);
+                        case '-':
+                            return new Node\Number(-$exp[1], $exp[2]);
+                    }
+                }
+                if ($op === 'not') {
+                    if ($inExp || $inParens) {
+                        if ($exp === self::$false || $exp === self::$null) {
+                            return self::$true;
+                        }
+                        return self::$false;
+                    }
+                    $op = $op . ' ';
+                }
+                return array(Type::T_STRING, '', array($op, $exp));
+            case Type::T_VARIABLE:
+                list(, $name) = $value;
+                return $this->reduce($this->get($name));
+            case Type::T_LIST:
+                foreach ($value[2] as &$item) {
+                    $item = $this->reduce($item);
+                }
+                return $value;
+            case Type::T_MAP:
+                foreach ($value[1] as &$item) {
+                    $item = $this->reduce($item);
+                }
+                foreach ($value[2] as &$item) {
+                    $item = $this->reduce($item);
+                }
+                return $value;
+            case Type::T_STRING:
+                foreach ($value[2] as &$item) {
+                    if (is_array($item) || $item instanceof \ArrayAccess) {
+                        $item = $this->reduce($item);
+                    }
+                }
+                return $value;
+            case Type::T_INTERPOLATE:
+                $value[1] = $this->reduce($value[1]);
+                return $value;
+            case Type::T_FUNCTION_CALL:
+                list(, $name, $argValues) = $value;
+                return $this->fncall($name, $argValues);
+            default:
+                return $value;
+        }
+    }
+    /**
+     * 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;
+        }
+        // native PHP functions
+        if ($this->callNativeFunction($name, $argValues, $returnValue)) {
+            return $returnValue;
+        }
+        // for CSS functions, simply flatten the arguments into a list
+        $listArgs = array();
+        foreach ((array) $argValues as $arg) {
+            if (empty($arg[0])) {
+                $listArgs[] = $this->reduce($arg[1]);
+            }
+        }
+        return array(Type::T_FUNCTION, $name, array(Type::T_LIST, ',', $listArgs));
+    }
+    /**
+     * Normalize name
+     *
+     * @param string $name
+     *
+     * @return string
+     */
+    protected function normalizeName($name)
+    {
+        return str_replace('-', '_', $name);
+    }
+    /**
+     * Normalize value
+     *
+     * @param array $value
+     *
+     * @return array
+     */
+    public function normalizeValue($value)
+    {
+        $value = $this->coerceForExpression($this->reduce($value));
+        list($type) = $value;
+        switch ($type) {
+            case Type::T_LIST:
+                $value = $this->extractInterpolation($value);
+                if ($value[0] !== Type::T_LIST) {
+                    return array(Type::T_KEYWORD, $this->compileValue($value));
+                }
+                foreach ($value[2] as $key => $item) {
+                    $value[2][$key] = $this->normalizeValue($item);
+                }
+                return $value;
+            case Type::T_STRING:
+                return array($type, '"', array($this->compileStringContent($value)));
+            case Type::T_NUMBER:
+                return $value->normalize();
+            case Type::T_INTERPOLATE:
+                return array(Type::T_KEYWORD, $this->compileValue($value));
+            default:
+                return $value;
+        }
+    }
+    /**
+     * Add numbers
+     *
+     * @param array $left
+     * @param array $right
+     *
+     * @return array
+     */
+    protected function opAddNumberNumber($left, $right)
+    {
+        return new Node\Number($left[1] + $right[1], $left[2]);
+    }
+    /**
+     * Multiply numbers
+     *
+     * @param array $left
+     * @param array $right
+     *
+     * @return array
+     */
+    protected function opMulNumberNumber($left, $right)
+    {
+        return new Node\Number($left[1] * $right[1], $left[2]);
+    }
+    /**
+     * Subtract numbers
+     *
+     * @param array $left
+     * @param array $right
+     *
+     * @return array
+     */
+    protected function opSubNumberNumber($left, $right)
+    {
+        return new Node\Number($left[1] - $right[1], $left[2]);
+    }
+    /**
+     * Divide numbers
+     *
+     * @param array $left
+     * @param array $right
+     *
+     * @return array
+     */
+    protected function opDivNumberNumber($left, $right)
+    {
+        if ($right[1] == 0) {
+            return array(Type::T_STRING, '', array($left[1] . $left[2] . '/' . $right[1] . $right[2]));
+        }
+        return new Node\Number($left[1] / $right[1], $left[2]);
+    }
+    /**
+     * Mod numbers
+     *
+     * @param array $left
+     * @param array $right
+     *
+     * @return array
+     */
+    protected function opModNumberNumber($left, $right)
+    {
+        return new Node\Number($left[1] % $right[1], $left[2]);
+    }
+    /**
+     * Add strings
+     *
+     * @param array $left
+     * @param array $right
+     *
+     * @return array
+     */
+    protected function opAdd($left, $right)
+    {
+        if ($strLeft = $this->coerceString($left)) {
+            if ($right[0] === Type::T_STRING) {
+                $right[1] = '';
+            }
+            $strLeft[2][] = $right;
+            return $strLeft;
+        }
+        if ($strRight = $this->coerceString($right)) {
+            if ($left[0] === Type::T_STRING) {
+                $left[1] = '';
+            }
+            array_unshift($strRight[2], $left);
+            return $strRight;
+        }
+    }
+    /**
+     * Boolean and
+     *
+     * @param array   $left
+     * @param array   $right
+     * @param boolean $shouldEval
+     *
+     * @return array
+     */
+    protected function opAnd($left, $right, $shouldEval)
+    {
+        if (!$shouldEval) {
+            return;
+        }
+        if ($left !== self::$false and $left !== self::$null) {
+            return $this->reduce($right, true);
+        }
+        return $left;
+    }
+    /**
+     * Boolean or
+     *
+     * @param array   $left
+     * @param array   $right
+     * @param boolean $shouldEval
+     *
+     * @return array
+     */
+    protected function opOr($left, $right, $shouldEval)
+    {
+        if (!$shouldEval) {
+            return;
+        }
+        if ($left !== self::$false and $left !== self::$null) {
+            return $left;
+        }
+        return $this->reduce($right, true);
+    }
+    /**
+     * Compare colors
+     *
+     * @param string $op
+     * @param array  $left
+     * @param array  $right
+     *
+     * @return array
+     */
+    protected function opColorColor($op, $left, $right)
+    {
+        $out = array(Type::T_COLOR);
+        foreach (array(1, 2, 3) as $i) {
+            $lval = isset($left[$i]) ? $left[$i] : 0;
+            $rval = isset($right[$i]) ? $right[$i] : 0;
+            switch ($op) {
+                case '+':
+                    $out[] = $lval + $rval;
+                    break;
+                case '-':
+                    $out[] = $lval - $rval;
+                    break;
+                case '*':
+                    $out[] = $lval * $rval;
+                    break;
+                case '%':
+                    $out[] = $lval % $rval;
+                    break;
+                case '/':
+                    if ($rval == 0) {
+                        $this->throwError('color: Can\'t divide by zero');
+                        break 2;
+                    }
+                    $out[] = (int) ($lval / $rval);
+                    break;
+                case '==':
+                    return $this->opEq($left, $right);
+                case '!=':
+                    return $this->opNeq($left, $right);
+                default:
+                    $this->throwError("color: unknown op {$op}");
+                    break 2;
+            }
+        }
+        if (isset($left[4])) {
+            $out[4] = $left[4];
+        } elseif (isset($right[4])) {
+            $out[4] = $right[4];
+        }
+        return $this->fixColor($out);
+    }
+    /**
+     * Compare color and number
+     *
+     * @param string $op
+     * @param array  $left
+     * @param array  $right
+     *
+     * @return array
+     */
+    protected function opColorNumber($op, $left, $right)
+    {
+        $value = $right[1];
+        return $this->opColorColor($op, $left, array(Type::T_COLOR, $value, $value, $value));
+    }
+    /**
+     * Compare number and color
+     *
+     * @param string $op
+     * @param array  $left
+     * @param array  $right
+     *
+     * @return array
+     */
+    protected function opNumberColor($op, $left, $right)
+    {
+        $value = $left[1];
+        return $this->opColorColor($op, array(Type::T_COLOR, $value, $value, $value), $right);
+    }
+    /**
+     * Compare number1 == number2
+     *
+     * @param array $left
+     * @param array $right
+     *
+     * @return array
+     */
+    protected function opEq($left, $right)
+    {
+        if (($lStr = $this->coerceString($left)) && ($rStr = $this->coerceString($right))) {
+            $lStr[1] = '';
+            $rStr[1] = '';
+            $left = $this->compileValue($lStr);
+            $right = $this->compileValue($rStr);
+        }
+        return $this->toBool($left === $right);
+    }
+    /**
+     * Compare number1 != number2
+     *
+     * @param array $left
+     * @param array $right
+     *
+     * @return array
+     */
+    protected function opNeq($left, $right)
+    {
+        if (($lStr = $this->coerceString($left)) && ($rStr = $this->coerceString($right))) {
+            $lStr[1] = '';
+            $rStr[1] = '';
+            $left = $this->compileValue($lStr);
+            $right = $this->compileValue($rStr);
+        }
+        return $this->toBool($left !== $right);
+    }
+    /**
+     * Compare number1 >= number2
+     *
+     * @param array $left
+     * @param array $right
+     *
+     * @return array
+     */
+    protected function opGteNumberNumber($left, $right)
+    {
+        return $this->toBool($left[1] >= $right[1]);
+    }
+    /**
+     * Compare number1 > number2
+     *
+     * @param array $left
+     * @param array $right
+     *
+     * @return array
+     */
+    protected function opGtNumberNumber($left, $right)
+    {
+        return $this->toBool($left[1] > $right[1]);
+    }
+    /**
+     * Compare number1 <= number2
+     *
+     * @param array $left
+     * @param array $right
+     *
+     * @return array
+     */
+    protected function opLteNumberNumber($left, $right)
+    {
+        return $this->toBool($left[1] <= $right[1]);
+    }
+    /**
+     * Compare number1 < number2
+     *
+     * @param array $left
+     * @param array $right
+     *
+     * @return array
+     */
+    protected function opLtNumberNumber($left, $right)
+    {
+        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
+     *
+     * @api
+     *
+     * @param mixed $thing
+     *
+     * @return array
+     */
+    public function toBool($thing)
+    {
+        return $thing ? self::$true : self::$false;
+    }
+    /**
+     * Compiles a primitive value into a CSS property value.
+     *
+     * Values in scssphp are typed by being wrapped in arrays, their format is
+     * typically:
+     *
+     *     array(type, contents [, additional_contents]*)
+     *
+     * The input is expected to be reduced. This function will not work on
+     * things like expressions and variables.
+     *
+     * @api
+     *
+     * @param array $value
+     *
+     * @return string
+     */
+    public function compileValue($value)
+    {
+        $value = $this->reduce($value);
+        list($type) = $value;
+        switch ($type) {
+            case Type::T_KEYWORD:
+                return $value[1];
+            case Type::T_COLOR:
+                // [1] - red component (either number for a %)
+                // [2] - green component
+                // [3] - blue component
+                // [4] - optional alpha component
+                list(, $r, $g, $b) = $value;
+                $r = round($r);
+                $g = round($g);
+                $b = round($b);
+                if (count($value) === 5 && $value[4] !== 1) {
+                    // rgba
+                    return 'rgba(' . $r . ', ' . $g . ', ' . $b . ', ' . $value[4] . ')';
+                }
+                $h = sprintf('#%02x%02x%02x', $r, $g, $b);
+                // Converting hex color to short notation (e.g. #003399 to #039)
+                if ($h[1] === $h[2] && $h[3] === $h[4] && $h[5] === $h[6]) {
+                    $h = '#' . $h[1] . $h[3] . $h[5];
+                }
+                return $h;
+            case Type::T_NUMBER:
+                return $value->output($this);
+            case Type::T_STRING:
+                return $value[1] . $this->compileStringContent($value) . $value[1];
+            case Type::T_FUNCTION:
+                $args = !empty($value[2]) ? $this->compileValue($value[2]) : '';
+                return "{$value['1']}({$args})";
+            case Type::T_LIST:
+                $value = $this->extractInterpolation($value);
+                if ($value[0] !== Type::T_LIST) {
+                    return $this->compileValue($value);
+                }
+                list(, $delim, $items) = $value;
+                if ($delim !== ' ') {
+                    $delim .= ' ';
+                }
+                $filtered = array();
+                foreach ($items as $item) {
+                    if ($item[0] === Type::T_NULL) {
+                        continue;
+                    }
+                    $filtered[] = $this->compileValue($item);
+                }
+                return implode("{$delim}", $filtered);
+            case Type::T_MAP:
+                $keys = $value[1];
+                $values = $value[2];
+                $filtered = array();
+                for ($i = 0, $s = count($keys); $i < $s; $i++) {
+                    $filtered[$this->compileValue($keys[$i])] = $this->compileValue($values[$i]);
+                }
+                array_walk($filtered, function (&$value, $key) {
+                    $value = $key . ': ' . $value;
+                });
+                return '(' . implode(', ', $filtered) . ')';
+            case Type::T_INTERPOLATED:
+                // node created by extractInterpolation
+                list(, $interpolate, $left, $right) = $value;
+                list(, , $whiteLeft, $whiteRight) = $interpolate;
+                $left = count($left[2]) > 0 ? $this->compileValue($left) . $whiteLeft : '';
+                $right = count($right[2]) > 0 ? $whiteRight . $this->compileValue($right) : '';
+                return $left . $this->compileValue($interpolate) . $right;
+            case Type::T_INTERPOLATE:
+                // raw parse node
+                list(, $exp) = $value;
+                // strip quotes if it's a string
+                $reduced = $this->reduce($exp);
+                switch ($reduced[0]) {
+                    case Type::T_STRING:
+                        $reduced = array(Type::T_KEYWORD, $this->compileStringContent($reduced));
+                        break;
+                    case Type::T_NULL:
+                        $reduced = array(Type::T_KEYWORD, '');
+                }
+                return $this->compileValue($reduced);
+            case Type::T_NULL:
+                return 'null';
+            default:
+                $this->throwError("unknown value type: {$type}");
+        }
+    }
+    /**
+     * Flatten list
+     *
+     * @param array $list
+     *
+     * @return string
+     */
+    protected function flattenList($list)
+    {
+        return $this->compileValue($list);
+    }
+    /**
+     * Compile string content
+     *
+     * @param array $string
+     *
+     * @return string
+     */
+    protected function compileStringContent($string)
+    {
+        $parts = array();
+        foreach ($string[2] as $part) {
+            if (is_array($part) || $part instanceof \ArrayAccess) {
+                $parts[] = $this->compileValue($part);
+            } else {
+                $parts[] = $part;
+            }
+        }
+        return implode($parts);
+    }
+    /**
+     * Extract interpolation; it doesn't need to be recursive, compileValue will handle that
+     *
+     * @param array $list
+     *
+     * @return array
+     */
+    protected function extractInterpolation($list)
+    {
+        $items = $list[2];
+        foreach ($items as $i => $item) {
+            if ($item[0] === Type::T_INTERPOLATE) {
+                $before = array(Type::T_LIST, $list[1], array_slice($items, 0, $i));
+                $after = array(Type::T_LIST, $list[1], array_slice($items, $i + 1));
+                return array(Type::T_INTERPOLATED, $item, $before, $after);
+            }
+        }
+        return $list;
+    }
+    /**
+     * Find the final set of selectors
+     *
+     * @param \Leafo\ScssPhp\Compiler\Environment $env
+     *
+     * @return array
+     */
+    protected function multiplySelectors(Environment $env)
+    {
+        $envs = $this->compactEnv($env);
+        $selectors = array();
+        $parentSelectors = array(array());
+        while ($env = array_pop($envs)) {
+            if (empty($env->selectors)) {
+                continue;
+            }
+            $selectors = array();
+            foreach ($env->selectors as $selector) {
+                foreach ($parentSelectors as $parent) {
+                    $selectors[] = $this->joinSelectors($parent, $selector);
+                }
+            }
+            $parentSelectors = $selectors;
+        }
+        return $selectors;
+    }
+    /**
+     * Join selectors; looks for & to replace, or append parent before child
+     *
+     * @param array $parent
+     * @param array $child
+     *
+     * @return array
+     */
+    protected function joinSelectors($parent, $child)
+    {
+        $setSelf = false;
+        $out = array();
+        foreach ($child as $part) {
+            $newPart = array();
+            foreach ($part as $p) {
+                if ($p === self::$selfSelector) {
+                    $setSelf = true;
+                    foreach ($parent as $i => $parentPart) {
+                        if ($i > 0) {
+                            $out[] = $newPart;
+                            $newPart = array();
+                        }
+                        foreach ($parentPart as $pp) {
+                            $newPart[] = $pp;
+                        }
+                    }
+                } else {
+                    $newPart[] = $p;
+                }
+            }
+            $out[] = $newPart;
+        }
+        return $setSelf ? $out : array_merge($parent, $child);
+    }
+    /**
+     * Multiply media
+     *
+     * @param \Leafo\ScssPhp\Compiler\Environment $env
+     * @param array                               $childQueries
+     *
+     * @return array
+     */
+    protected function multiplyMedia(Environment $env = null, $childQueries = null)
+    {
+        if (!isset($env) || !empty($env->block->type) && $env->block->type !== Type::T_MEDIA) {
+            return $childQueries;
+        }
+        // plain old block, skip
+        if (empty($env->block->type)) {
+            return $this->multiplyMedia($env->parent, $childQueries);
+        }
+        $parentQueries = isset($env->block->queryList) ? $env->block->queryList : array(array(array(Type::T_MEDIA_VALUE, $env->block->value)));
+        if ($childQueries === null) {
+            $childQueries = $parentQueries;
+        } else {
+            $originalQueries = $childQueries;
+            $childQueries = array();
+            foreach ($parentQueries as $parentQuery) {
+                foreach ($originalQueries as $childQuery) {
+                    $childQueries[] = array_merge($parentQuery, $childQuery);
+                }
+            }
+        }
+        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 = array(); $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 \Leafo\ScssPhp\Block $block
+     *
+     * @return \Leafo\ScssPhp\Compiler\Environment
+     */
+    protected function pushEnv(Block $block = null)
+    {
+        $env = new Environment();
+        $env->parent = $this->env;
+        $env->store = array();
+        $env->block = $block;
+        $env->depth = isset($this->env->depth) ? $this->env->depth + 1 : 0;
+        $this->env = $env;
+        return $env;
+    }
+    /**
+     * Pop environment
+     */
+    protected function popEnv()
+    {
+        $this->env = $this->env->parent;
+    }
+    /**
+     * Get store environment
+     *
+     * @return \Leafo\ScssPhp\Compiler\Environment
+     */
+    protected function getStoreEnv()
+    {
+        return isset($this->storeEnv) ? $this->storeEnv : $this->env;
+    }
+    /**
+     * Set variable
+     *
+     * @param string                              $name
+     * @param mixed                               $value
+     * @param boolean                             $shadow
+     * @param \Leafo\ScssPhp\Compiler\Environment $env
+     */
+    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 {
+            $this->setExisting($name, $value, $env);
+        }
+    }
+    /**
+     * Set existing variable
+     *
+     * @param string                              $name
+     * @param mixed                               $value
+     * @param \Leafo\ScssPhp\Compiler\Environment $env
+     */
+    protected function setExisting($name, $value, Environment $env)
+    {
+        $storeEnv = $env;
+        $hasNamespace = $name[0] === '^' || $name[0] === '@' || $name[0] === '%';
+        for (;;) {
+            if (array_key_exists($name, $env->store)) {
+                break;
+            }
+            if (!$hasNamespace && isset($env->marker)) {
+                $env = $storeEnv;
+                break;
+            }
+            if (!isset($env->parent)) {
+                $env = $storeEnv;
+                break;
+            }
+            $env = $env->parent;
+        }
+        $env->store[$name] = $value;
+    }
+    /**
+     * Set raw variable
+     *
+     * @param string                              $name
+     * @param mixed                               $value
+     * @param \Leafo\ScssPhp\Compiler\Environment $env
+     */
+    protected function setRaw($name, $value, Environment $env)
+    {
+        $env->store[$name] = $value;
+    }
+    /**
+     * Get variable
+     *
+     * @api
+     *
+     * @param string                              $name
+     * @param boolean                             $shouldThrow
+     * @param \Leafo\ScssPhp\Compiler\Environment $env
+     *
+     * @return mixed
+     */
+    public function get($name, $shouldThrow = true, Environment $env = null)
+    {
+        $name = $this->normalizeName($name);
+        if (!isset($env)) {
+            $env = $this->getStoreEnv();
+        }
+        $hasNamespace = $name[0] === '^' || $name[0] === '@' || $name[0] === '%';
+        for (;;) {
+            if (array_key_exists($name, $env->store)) {
+                return $env->store[$name];
+            }
+            if (!$hasNamespace && isset($env->marker)) {
+                $env = $this->rootEnv;
+                continue;
+            }
+            if (!isset($env->parent)) {
+                break;
+            }
+            $env = $env->parent;
+        }
+        if ($shouldThrow) {
+            $this->throwError("Undefined variable \${$name}");
+        }
+    }
+    /**
+     * Has variable?
+     *
+     * @param string                              $name
+     * @param \Leafo\ScssPhp\Compiler\Environment $env
+     *
+     * @return boolean
+     */
+    protected function has($name, Environment $env = null)
+    {
+        return $this->get($name, false, $env) !== null;
+    }
+    /**
+     * Inject variables
+     *
+     * @param array $args
+     */
+    protected function injectVariables(array $args)
+    {
+        if (empty($args)) {
+            return;
+        }
+        $parser = $this->parserFactory(__METHOD__);
+        foreach ($args as $name => $strValue) {
+            if ($name[0] === '$') {
+                $name = substr($name, 1);
+            }
+            if (!$parser->parseValue($strValue, $value)) {
+                $value = $this->coerceValue($strValue);
+            }
+            $this->set($name, $value);
+        }
+    }
+    /**
+     * Set variables
+     *
+     * @api
+     *
+     * @param array $variables
+     */
+    public function setVariables(array $variables)
+    {
+        $this->registeredVars = array_merge($this->registeredVars, $variables);
+    }
+    /**
+     * Unset variable
+     *
+     * @api
+     *
+     * @param string $name
+     */
+    public function unsetVariable($name)
+    {
+        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
+     *
+     * @api
+     *
+     * @return array
+     */
+    public function getParsedFiles()
+    {
+        return $this->parsedFiles;
+    }
+    /**
+     * Add import path
+     *
+     * @api
+     *
+     * @param string $path
+     */
+    public function addImportPath($path)
+    {
+        if (!in_array($path, $this->importPaths)) {
+            $this->importPaths[] = $path;
+        }
+    }
+    /**
+     * Set import paths
+     *
+     * @api
+     *
+     * @param string|array $path
+     */
+    public function setImportPaths($path)
+    {
+        $this->importPaths = (array) $path;
+    }
+    /**
+     * Set number precision
+     *
+     * @api
+     *
+     * @param integer $numberPrecision
+     */
+    public function setNumberPrecision($numberPrecision)
+    {
+        Node\Number::$precision = $numberPrecision;
+    }
+    /**
+     * Set formatter
+     *
+     * @api
+     *
+     * @param string $formatterName
+     */
+    public function setFormatter($formatterName)
+    {
+        $this->formatter = $formatterName;
+    }
+    /**
+     * Set line number style
+     *
+     * @api
+     *
+     * @param string $lineNumberStyle
+     */
+    public function setLineNumberStyle($lineNumberStyle)
+    {
+        $this->lineNumberStyle = $lineNumberStyle;
+    }
+    /**
+     * Register function
+     *
+     * @api
+     *
+     * @param string   $name
+     * @param callable $func
+     * @param array    $prototype
+     */
+    public function registerFunction($name, $func, $prototype = null)
+    {
+        $this->userFunctions[$this->normalizeName($name)] = array($func, $prototype);
+    }
+    /**
+     * Unregister function
+     *
+     * @api
+     *
+     * @param string $name
+     */
+    public function unregisterFunction($name)
+    {
+        unset($this->userFunctions[$this->normalizeName($name)]);
+    }
+    /**
+     * Add feature
+     *
+     * @api
+     *
+     * @param string $name
+     */
+    public function addFeature($name)
+    {
+        $this->registeredFeatures[$name] = true;
+    }
+    /**
+     * Import file
+     *
+     * @param string $path
+     * @param array  $out
+     */
+    protected function importFile($path, $out)
+    {
+        // see if tree is cached
+        $realPath = realpath($path);
+        if (isset($this->importCache[$realPath])) {
+            $this->handleImportLoop($realPath);
+            $tree = $this->importCache[$realPath];
+        } else {
+            $code = file_get_contents($path);
+            $parser = $this->parserFactory($path);
+            $tree = $parser->parse($code);
+            $this->importCache[$realPath] = $tree;
+        }
+        $pi = pathinfo($path);
+        array_unshift($this->importPaths, $pi['dirname']);
+        $this->compileChildrenNoReturn($tree->children, $out);
+        array_shift($this->importPaths);
+    }
+    /**
+     * Return the file path for an import url if it exists
+     *
+     * @api
+     *
+     * @param string $url
+     *
+     * @return string|null
+     */
+    public function findImport($url)
+    {
+        $urls = array();
+        // 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));
+        }
+        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;
+                    if ($this->fileExists($file = $full . '.scss') || $this->fileExists($file = $full)) {
+                        return $file;
+                    }
+                }
+            } elseif (is_callable($dir)) {
+                // check custom callback for import path
+                $file = call_user_func($dir, $url);
+                if ($file !== null) {
+                    return $file;
+                }
+            }
+        }
+        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)
+     *
+     * @api
+     *
+     * @param string $msg Message with optional sprintf()-style vararg parameters
+     *
+     * @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());
+        }
+        $line = $this->sourceLine;
+        $msg = "{$msg}: line: {$line}";
+        throw new CompilerException($msg);
+    }
+    /**
+     * Handle import loop
+     *
+     * @param string $name
+     *
+     * @throws \Exception
+     */
+    protected function handleImportLoop($name)
+    {
+        for ($env = $this->env; $env; $env = $env->parent) {
+            $file = $this->sourceNames[$env->block->sourceIndex];
+            if (realpath($file) === $name) {
+                $this->throwError('An @import loop has been found: %s imports %s', $file, basename($file));
+                break;
+            }
+        }
+    }
+    /**
+     * Does file exist?
+     *
+     * @param string $name
+     *
+     * @return boolean
+     */
+    protected function fileExists($name)
+    {
+        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();
+        // set the args
+        if (isset($func->args)) {
+            $this->applyArguments($func->args, $argValues);
+        }
+        // throw away lines and children
+        $tmp = new OutputBlock();
+        $tmp->lines = array();
+        $tmp->children = array();
+        $this->env->marker = 'function';
+        $ret = $this->compileChildren($func->children, $tmp);
+        $this->popEnv();
+        $returnValue = !isset($ret) ? self::$defaultValue : $ret;
+        return true;
+    }
+    /**
+     * Call built-in and registered (PHP) functions
+     *
+     * @param string $name
+     * @param array  $args
+     * @param array  $returnValue
+     *
+     * @return boolean Returns true if returnValue is set; otherwise, false
+     */
+    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
+            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 {
+            return false;
+        }
+        list($sorted, $kwargs) = $this->sortArgs($prototype, $args);
+        if ($name !== 'if' && $name !== 'call') {
+            foreach ($sorted as &$val) {
+                $val = $this->reduce($val, true);
+            }
+        }
+        $returnValue = call_user_func($f, $sorted, $kwargs);
+        if (!isset($returnValue)) {
+            return false;
+        }
+        $returnValue = $this->coerceValue($returnValue);
+        return true;
+    }
+    /**
+     * Get built-in function
+     *
+     * @param string $name Normalized name
+     *
+     * @return array
+     */
+    protected function getBuiltinFunction($name)
+    {
+        $libName = 'lib' . preg_replace_callback('/_(.)/', function ($m) {
+            return ucfirst($m[1]);
+        }, ucfirst($name));
+        return array($this, $libName);
+    }
+    /**
+     * Sorts keyword arguments
+     *
+     * @param array $prototype
+     * @param array $args
+     *
+     * @return array
+     */
+    protected function sortArgs($prototype, $args)
+    {
+        $keyArgs = array();
+        $posArgs = array();
+        // separate positional and keyword arguments
+        foreach ($args as $arg) {
+            list($key, $value) = $arg;
+            $key = $key[1];
+            if (empty($key)) {
+                $posArgs[] = $value;
+            } else {
+                $keyArgs[$key] = $value;
+            }
+        }
+        if (!isset($prototype)) {
+            return array($posArgs, $keyArgs);
+        }
+        // copy positional args
+        $finalArgs = array_pad($posArgs, count($prototype), null);
+        // overwrite positional args with keyword args
+        foreach ($prototype as $i => $names) {
+            foreach ((array) $names as $name) {
+                if (isset($keyArgs[$name])) {
+                    $finalArgs[$i] = $keyArgs[$name];
+                }
+            }
+        }
+        return array($finalArgs, $keyArgs);
+    }
+    /**
+     * Apply argument values per definition
+     *
+     * @param array $argDef
+     * @param array $argValues
+     *
+     * @throws \Exception
+     */
+    protected function applyArguments($argDef, $argValues)
+    {
+        $storeEnv = $this->getStoreEnv();
+        $env = new Environment();
+        $env->store = $storeEnv->store;
+        $hasVariable = false;
+        $args = array();
+        foreach ($argDef as $i => $arg) {
+            list($name, $default, $isVariable) = $argDef[$i];
+            $args[$name] = array($i, $name, $default, $isVariable);
+            $hasVariable |= $isVariable;
+        }
+        $keywordArgs = array();
+        $deferredKeywordArgs = array();
+        $remaining = array();
+        // assign the keyword args
+        foreach ((array) $argValues as $arg) {
+            if (!empty($arg[0])) {
+                if (!isset($args[$arg[0][1]])) {
+                    if ($hasVariable) {
+                        $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] === 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 {
+                            $remaining[] = $item;
+                        }
+                    }
+                } else {
+                    $remaining[] = $val;
+                }
+            } else {
+                $remaining[] = $arg[1];
+            }
+        }
+        foreach ($args as $arg) {
+            list($i, $name, $default, $isVariable) = $arg;
+            if ($isVariable) {
+                $val = array(Type::T_LIST, ',', array(), $isVariable);
+                for ($count = count($remaining); $i < $count; $i++) {
+                    $val[2][] = $remaining[$i];
+                }
+                foreach ($deferredKeywordArgs as $itemName => $item) {
+                    $val[2][$itemName] = $item;
+                }
+            } elseif (isset($remaining[$i])) {
+                $val = $remaining[$i];
+            } elseif (isset($keywordArgs[$name])) {
+                $val = $keywordArgs[$name];
+            } elseif (!empty($default)) {
+                continue;
+            } else {
+                $this->throwError("Missing argument {$name}");
+                break;
+            }
+            $this->set($name, $this->reduce($val, true), true, $env);
+        }
+        $storeEnv->store = $env->store;
+        foreach ($args as $arg) {
+            list($i, $name, $default, $isVariable) = $arg;
+            if ($isVariable || isset($remaining[$i]) || isset($keywordArgs[$name]) || empty($default)) {
+                continue;
+            }
+            $this->set($name, $this->reduce($default, true), true);
+        }
+    }
+    /**
+     * Coerce a php value into a scss one
+     *
+     * @param mixed $value
+     *
+     * @return array
+     */
+    private function coerceValue($value)
+    {
+        if (is_array($value) || $value instanceof \ArrayAccess) {
+            return $value;
+        }
+        if (is_bool($value)) {
+            return $this->toBool($value);
+        }
+        if ($value === null) {
+            $value = self::$null;
+        }
+        if (is_numeric($value)) {
+            return new Node\Number($value, '');
+        }
+        if ($value === '') {
+            return self::$emptyString;
+        }
+        return array(Type::T_KEYWORD, $value);
+    }
+    /**
+     * Coerce something to map
+     *
+     * @param array $item
+     *
+     * @return array
+     */
+    protected function coerceMap($item)
+    {
+        if ($item[0] === Type::T_MAP) {
+            return $item;
+        }
+        if ($item === self::$emptyList) {
+            return self::$emptyMap;
+        }
+        return array(Type::T_MAP, array($item), array(self::$null));
+    }
+    /**
+     * Coerce something to list
+     *
+     * @param array $item
+     *
+     * @return array
+     */
+    protected function coerceList($item, $delim = ',')
+    {
+        if (isset($item) && $item[0] === Type::T_LIST) {
+            return $item;
+        }
+        if (isset($item) && $item[0] === Type::T_MAP) {
+            $keys = $item[1];
+            $values = $item[2];
+            $list = array();
+            for ($i = 0, $s = count($keys); $i < $s; $i++) {
+                $key = $keys[$i];
+                $value = $values[$i];
+                $list[] = array(Type::T_LIST, '', array(array(Type::T_KEYWORD, $this->compileStringContent($this->coerceString($key))), $value));
+            }
+            return array(Type::T_LIST, ',', $list);
+        }
+        return array(Type::T_LIST, $delim, !isset($item) ? array() : array($item));
+    }
+    /**
+     * Coerce color for expression
+     *
+     * @param array $value
+     *
+     * @return array|null
+     */
+    protected function coerceForExpression($value)
+    {
+        if ($color = $this->coerceColor($value)) {
+            return $color;
+        }
+        return $value;
+    }
+    /**
+     * Coerce value to color
+     *
+     * @param array $value
+     *
+     * @return array|null
+     */
+    protected function coerceColor($value)
+    {
+        switch ($value[0]) {
+            case Type::T_COLOR:
+                return $value;
+            case Type::T_KEYWORD:
+                $name = strtolower($value[1]);
+                if (isset(Colors::$cssColors[$name])) {
+                    $rgba = explode(',', Colors::$cssColors[$name]);
+                    return isset($rgba[3]) ? array(Type::T_COLOR, (int) $rgba[0], (int) $rgba[1], (int) $rgba[2], (int) $rgba[3]) : array(Type::T_COLOR, (int) $rgba[0], (int) $rgba[1], (int) $rgba[2]);
+                }
+                return null;
+        }
+        return null;
+    }
+    /**
+     * Coerce value to string
+     *
+     * @param array $value
+     *
+     * @return array|null
+     */
+    protected function coerceString($value)
+    {
+        if ($value[0] === Type::T_STRING) {
+            return $value;
+        }
+        return array(Type::T_STRING, '', array($this->compileValue($value)));
+    }
+    /**
+     * Coerce value to a percentage
+     *
+     * @param array $value
+     *
+     * @return integer|float
+     */
+    protected function coercePercent($value)
+    {
+        if ($value[0] === Type::T_NUMBER) {
+            if (!empty($value[2]['%'])) {
+                return $value[1] / 100;
+            }
+            return $value[1];
+        }
+        return 0;
+    }
+    /**
+     * Assert value is a map
+     *
+     * @api
+     *
+     * @param array $value
+     *
+     * @return array
+     *
+     * @throws \Exception
+     */
+    public function assertMap($value)
+    {
+        $value = $this->coerceMap($value);
+        if ($value[0] !== Type::T_MAP) {
+            $this->throwError('expecting map');
+        }
+        return $value;
+    }
+    /**
+     * Assert value is a list
+     *
+     * @api
+     *
+     * @param array $value
+     *
+     * @return array
+     *
+     * @throws \Exception
+     */
+    public function assertList($value)
+    {
+        if ($value[0] !== Type::T_LIST) {
+            $this->throwError('expecting list');
+        }
+        return $value;
+    }
+    /**
+     * Assert value is a color
+     *
+     * @api
+     *
+     * @param array $value
+     *
+     * @return array
+     *
+     * @throws \Exception
+     */
+    public function assertColor($value)
+    {
+        if ($color = $this->coerceColor($value)) {
+            return $color;
+        }
+        $this->throwError('expecting color');
+    }
+    /**
+     * Assert value is a number
+     *
+     * @api
+     *
+     * @param array $value
+     *
+     * @return integer|float
+     *
+     * @throws \Exception
+     */
+    public function assertNumber($value)
+    {
+        if ($value[0] !== Type::T_NUMBER) {
+            $this->throwError('expecting number');
+        }
+        return $value[1];
+    }
+    /**
+     * Make sure a color's components don't go out of bounds
+     *
+     * @param array $c
+     *
+     * @return array
+     */
+    protected function fixColor($c)
+    {
+        foreach (array(1, 2, 3) as $i) {
+            if ($c[$i] < 0) {
+                $c[$i] = 0;
+            }
+            if ($c[$i] > 255) {
+                $c[$i] = 255;
+            }
+        }
+        return $c;
+    }
+    /**
+     * Convert RGB to HSL
+     *
+     * @api
+     *
+     * @param integer $red
+     * @param integer $green
+     * @param integer $blue
+     *
+     * @return array
+     */
+    public function toHSL($red, $green, $blue)
+    {
+        $min = min($red, $green, $blue);
+        $max = max($red, $green, $blue);
+        $l = $min + $max;
+        $d = $max - $min;
+        if ((int) $d === 0) {
+            $h = $s = 0;
+        } else {
+            if ($l < 255) {
+                $s = $d / $l;
+            } else {
+                $s = $d / (510 - $l);
+            }
+            if ($red == $max) {
+                $h = 60 * ($green - $blue) / $d;
+            } elseif ($green == $max) {
+                $h = 60 * ($blue - $red) / $d + 120;
+            } elseif ($blue == $max) {
+                $h = 60 * ($red - $green) / $d + 240;
+            }
+        }
+        return array(Type::T_HSL, fmod($h, 360), $s * 100, $l / 5.1);
+    }
+    /**
+     * Hue to RGB helper
+     *
+     * @param float $m1
+     * @param float $m2
+     * @param float $h
+     *
+     * @return float
+     */
+    private function hueToRGB($m1, $m2, $h)
+    {
+        if ($h < 0) {
+            $h += 1;
+        } elseif ($h > 1) {
+            $h -= 1;
+        }
+        if ($h * 6 < 1) {
+            return $m1 + ($m2 - $m1) * $h * 6;
+        }
+        if ($h * 2 < 1) {
+            return $m2;
+        }
+        if ($h * 3 < 2) {
+            return $m1 + ($m2 - $m1) * (2 / 3 - $h) * 6;
+        }
+        return $m1;
+    }
+    /**
+     * Convert HSL to RGB
+     *
+     * @api
+     *
+     * @param integer $hue        H from 0 to 360
+     * @param integer $saturation S from 0 to 100
+     * @param integer $lightness  L from 0 to 100
+     *
+     * @return array
+     */
+    public function toRGB($hue, $saturation, $lightness)
+    {
+        if ($hue < 0) {
+            $hue += 360;
+        }
+        $h = $hue / 360;
+        $s = min(100, max(0, $saturation)) / 100;
+        $l = min(100, max(0, $lightness)) / 100;
+        $m2 = $l <= 0.5 ? $l * ($s + 1) : $l + $s - $l * $s;
+        $m1 = $l * 2 - $m2;
+        $r = $this->hueToRGB($m1, $m2, $h + 1 / 3) * 255;
+        $g = $this->hueToRGB($m1, $m2, $h) * 255;
+        $b = $this->hueToRGB($m1, $m2, $h - 1 / 3) * 255;
+        $out = array(Type::T_COLOR, $r, $g, $b);
+        return $out;
+    }
+    // Built in functions
+    //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 array(null, $a, false);
+        }, $args);
+        if (count($kwargs)) {
+            foreach ($kwargs as $key => $value) {
+                $args[] = array(array(Type::T_VARIABLE, $key), $value, false);
+            }
+        }
+        return $this->reduce(array(Type::T_FUNCTION_CALL, $name, $args));
+    }
+    protected static $libIf = array('condition', 'if-true', 'if-false');
+    protected function libIf($args)
+    {
+        list($cond, $t, $f) = $args;
+        if (!$this->isTruthy($this->reduce($cond, true))) {
+            return $this->reduce($f, true);
+        }
+        return $this->reduce($t, true);
+    }
+    protected static $libIndex = array('list', 'value');
+    protected function libIndex($args)
+    {
+        list($list, $value) = $args;
+        if ($value[0] === Type::T_MAP) {
+            return self::$null;
+        }
+        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] !== Type::T_LIST) {
+            return self::$null;
+        }
+        $values = array();
+        foreach ($list[2] as $item) {
+            $values[] = $this->normalizeValue($item);
+        }
+        $key = array_search($this->normalizeValue($value), $values);
+        return false === $key ? self::$null : $key + 1;
+    }
+    protected static $libRgb = array('red', 'green', 'blue');
+    protected function libRgb($args)
+    {
+        list($r, $g, $b) = $args;
+        return array(Type::T_COLOR, $r[1], $g[1], $b[1]);
+    }
+    protected static $libRgba = array(array('red', 'color'), 'green', 'blue', 'alpha');
+    protected function libRgba($args)
+    {
+        if ($color = $this->coerceColor($args[0])) {
+            $num = !isset($args[1]) ? $args[3] : $args[1];
+            $alpha = $this->assertNumber($num);
+            $color[4] = $alpha;
+            return $color;
+        }
+        list($r, $g, $b, $a) = $args;
+        return array(Type::T_COLOR, $r[1], $g[1], $b[1], $a[1]);
+    }
+    // helper function for adjust_color, change_color, and scale_color
+    protected function alterColor($args, $fn)
+    {
+        $color = $this->assertColor($args[0]);
+        foreach (array(1, 2, 3, 7) as $i) {
+            if (isset($args[$i])) {
+                $val = $this->assertNumber($args[$i]);
+                $ii = $i === 7 ? 4 : $i;
+                // alpha
+                $color[$ii] = call_user_func($fn, isset($color[$ii]) ? $color[$ii] : 0, $val, $i);
+            }
+        }
+        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) {
+                if (isset($args[$i])) {
+                    $val = $this->assertNumber($args[$i]);
+                    $hsl[$i - 3] = call_user_func($fn, $hsl[$i - 3], $val, $i);
+                }
+            }
+            $rgb = $this->toRGB($hsl[1], $hsl[2], $hsl[3]);
+            if (isset($color[4])) {
+                $rgb[4] = $color[4];
+            }
+            $color = $rgb;
+        }
+        return $color;
+    }
+    protected static $libAdjustColor = array('color', 'red', 'green', 'blue', 'hue', 'saturation', 'lightness', 'alpha');
+    protected function libAdjustColor($args)
+    {
+        return $this->alterColor($args, function ($base, $alter, $i) {
+            return $base + $alter;
+        });
+    }
+    protected static $libChangeColor = array('color', 'red', 'green', 'blue', 'hue', 'saturation', 'lightness', 'alpha');
+    protected function libChangeColor($args)
+    {
+        return $this->alterColor($args, function ($base, $alter, $i) {
+            return $alter;
+        });
+    }
+    protected static $libScaleColor = array('color', 'red', 'green', 'blue', 'hue', 'saturation', 'lightness', 'alpha');
+    protected function libScaleColor($args)
+    {
+        return $this->alterColor($args, function ($base, $scale, $i) {
+            // 1, 2, 3 - rgb
+            // 4, 5, 6 - hsl
+            // 7 - a
+            switch ($i) {
+                case 1:
+                case 2:
+                case 3:
+                    $max = 255;
+                    break;
+                case 4:
+                    $max = 360;
+                    break;
+                case 7:
+                    $max = 1;
+                    break;
+                default:
+                    $max = 100;
+            }
+            $scale = $scale / 100;
+            if ($scale < 0) {
+                return $base * $scale + $base;
+            }
+            return ($max - $base) * $scale + $base;
+        });
+    }
+    protected static $libIeHexStr = array('color');
+    protected function libIeHexStr($args)
+    {
+        $color = $this->coerceColor($args[0]);
+        $color[4] = isset($color[4]) ? round(255 * $color[4]) : 255;
+        return sprintf('#%02X%02X%02X%02X', $color[4], $color[1], $color[2], $color[3]);
+    }
+    protected static $libRed = array('color');
+    protected function libRed($args)
+    {
+        $color = $this->coerceColor($args[0]);
+        return $color[1];
+    }
+    protected static $libGreen = array('color');
+    protected function libGreen($args)
+    {
+        $color = $this->coerceColor($args[0]);
+        return $color[2];
+    }
+    protected static $libBlue = array('color');
+    protected function libBlue($args)
+    {
+        $color = $this->coerceColor($args[0]);
+        return $color[3];
+    }
+    protected static $libAlpha = array('color');
+    protected function libAlpha($args)
+    {
+        if ($color = $this->coerceColor($args[0])) {
+            return isset($color[4]) ? $color[4] : 1;
+        }
+        // this might be the IE function, so return value unchanged
+        return null;
+    }
+    protected static $libOpacity = array('color');
+    protected function libOpacity($args)
+    {
+        $value = $args[0];
+        if ($value[0] === Type::T_NUMBER) {
+            return null;
+        }
+        return $this->libAlpha($args);
+    }
+    // mix two colors
+    protected static $libMix = array('color-1', 'color-2', 'weight');
+    protected function libMix($args)
+    {
+        list($first, $second, $weight) = $args;
+        $first = $this->assertColor($first);
+        $second = $this->assertColor($second);
+        if (!isset($weight)) {
+            $weight = 0.5;
+        } else {
+            $weight = $this->coercePercent($weight);
+        }
+        $firstAlpha = isset($first[4]) ? $first[4] : 1;
+        $secondAlpha = isset($second[4]) ? $second[4] : 1;
+        $w = $weight * 2 - 1;
+        $a = $firstAlpha - $secondAlpha;
+        $w1 = (($w * $a === -1 ? $w : ($w + $a) / (1 + $w * $a)) + 1) / 2.0;
+        $w2 = 1.0 - $w1;
+        $new = array(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);
+        }
+        return $this->fixColor($new);
+    }
+    protected static $libHsl = array('hue', 'saturation', 'lightness');
+    protected function libHsl($args)
+    {
+        list($h, $s, $l) = $args;
+        return $this->toRGB($h[1], $s[1], $l[1]);
+    }
+    protected static $libHsla = array('hue', 'saturation', 'lightness', 'alpha');
+    protected function libHsla($args)
+    {
+        list($h, $s, $l, $a) = $args;
+        $color = $this->toRGB($h[1], $s[1], $l[1]);
+        $color[4] = $a[1];
+        return $color;
+    }
+    protected static $libHue = array('color');
+    protected function libHue($args)
+    {
+        $color = $this->assertColor($args[0]);
+        $hsl = $this->toHSL($color[1], $color[2], $color[3]);
+        return new Node\Number($hsl[1], 'deg');
+    }
+    protected static $libSaturation = array('color');
+    protected function libSaturation($args)
+    {
+        $color = $this->assertColor($args[0]);
+        $hsl = $this->toHSL($color[1], $color[2], $color[3]);
+        return new Node\Number($hsl[2], '%');
+    }
+    protected static $libLightness = array('color');
+    protected function libLightness($args)
+    {
+        $color = $this->assertColor($args[0]);
+        $hsl = $this->toHSL($color[1], $color[2], $color[3]);
+        return new Node\Number($hsl[3], '%');
+    }
+    protected function adjustHsl($color, $idx, $amount)
+    {
+        $hsl = $this->toHSL($color[1], $color[2], $color[3]);
+        $hsl[$idx] += $amount;
+        $out = $this->toRGB($hsl[1], $hsl[2], $hsl[3]);
+        if (isset($color[4])) {
+            $out[4] = $color[4];
+        }
+        return $out;
+    }
+    protected static $libAdjustHue = array('color', 'degrees');
+    protected function libAdjustHue($args)
+    {
+        $color = $this->assertColor($args[0]);
+        $degrees = $this->assertNumber($args[1]);
+        return $this->adjustHsl($color, 1, $degrees);
+    }
+    protected static $libLighten = array('color', 'amount');
+    protected function libLighten($args)
+    {
+        $color = $this->assertColor($args[0]);
+        $amount = Util::checkRange('amount', new Range(0, 100), $args[1], '%');
+        return $this->adjustHsl($color, 3, $amount);
+    }
+    protected static $libDarken = array('color', 'amount');
+    protected function libDarken($args)
+    {
+        $color = $this->assertColor($args[0]);
+        $amount = Util::checkRange('amount', new Range(0, 100), $args[1], '%');
+        return $this->adjustHsl($color, 3, -$amount);
+    }
+    protected static $libSaturate = array('color', 'amount');
+    protected function libSaturate($args)
+    {
+        $value = $args[0];
+        if ($value[0] === Type::T_NUMBER) {
+            return null;
+        }
+        $color = $this->assertColor($value);
+        $amount = 100 * $this->coercePercent($args[1]);
+        return $this->adjustHsl($color, 2, $amount);
+    }
+    protected static $libDesaturate = array('color', 'amount');
+    protected function libDesaturate($args)
+    {
+        $color = $this->assertColor($args[0]);
+        $amount = 100 * $this->coercePercent($args[1]);
+        return $this->adjustHsl($color, 2, -$amount);
+    }
+    protected static $libGrayscale = array('color');
+    protected function libGrayscale($args)
+    {
+        $value = $args[0];
+        if ($value[0] === Type::T_NUMBER) {
+            return null;
+        }
+        return $this->adjustHsl($this->assertColor($value), 2, -100);
+    }
+    protected static $libComplement = array('color');
+    protected function libComplement($args)
+    {
+        return $this->adjustHsl($this->assertColor($args[0]), 1, 180);
+    }
+    protected static $libInvert = array('color');
+    protected function libInvert($args)
+    {
+        $value = $args[0];
+        if ($value[0] === Type::T_NUMBER) {
+            return null;
+        }
+        $color = $this->assertColor($value);
+        $color[1] = 255 - $color[1];
+        $color[2] = 255 - $color[2];
+        $color[3] = 255 - $color[3];
+        return $color;
+    }
+    // increases opacity by amount
+    protected static $libOpacify = array('color', 'amount');
+    protected function libOpacify($args)
+    {
+        $color = $this->assertColor($args[0]);
+        $amount = $this->coercePercent($args[1]);
+        $color[4] = (isset($color[4]) ? $color[4] : 1) + $amount;
+        $color[4] = min(1, max(0, $color[4]));
+        return $color;
+    }
+    protected static $libFadeIn = array('color', 'amount');
+    protected function libFadeIn($args)
+    {
+        return $this->libOpacify($args);
+    }
+    // decreases opacity by amount
+    protected static $libTransparentize = array('color', 'amount');
+    protected function libTransparentize($args)
+    {
+        $color = $this->assertColor($args[0]);
+        $amount = $this->coercePercent($args[1]);
+        $color[4] = (isset($color[4]) ? $color[4] : 1) - $amount;
+        $color[4] = min(1, max(0, $color[4]));
+        return $color;
+    }
+    protected static $libFadeOut = array('color', 'amount');
+    protected function libFadeOut($args)
+    {
+        return $this->libTransparentize($args);
+    }
+    protected static $libUnquote = array('string');
+    protected function libUnquote($args)
+    {
+        $str = $args[0];
+        if ($str[0] === Type::T_STRING) {
+            $str[1] = '';
+        }
+        return $str;
+    }
+    protected static $libQuote = array('string');
+    protected function libQuote($args)
+    {
+        $value = $args[0];
+        if ($value[0] === Type::T_STRING && !empty($value[1])) {
+            return $value;
+        }
+        return array(Type::T_STRING, '"', array($value));
+    }
+    protected static $libPercentage = array('value');
+    protected function libPercentage($args)
+    {
+        return new Node\Number($this->coercePercent($args[0]) * 100, '%');
+    }
+    protected static $libRound = array('value');
+    protected function libRound($args)
+    {
+        $num = $args[0];
+        $num[1] = round($num[1]);
+        return $num;
+    }
+    protected static $libFloor = array('value');
+    protected function libFloor($args)
+    {
+        $num = $args[0];
+        $num[1] = floor($num[1]);
+        return $num;
+    }
+    protected static $libCeil = array('value');
+    protected function libCeil($args)
+    {
+        $num = $args[0];
+        $num[1] = ceil($num[1]);
+        return $num;
+    }
+    protected static $libAbs = array('value');
+    protected function libAbs($args)
+    {
+        $num = $args[0];
+        $num[1] = abs($num[1]);
+        return $num;
+    }
+    protected function libMin($args)
+    {
+        $numbers = $this->getNormalizedNumbers($args);
+        $min = null;
+        foreach ($numbers as $key => $number) {
+            if (null === $min || $number[1] <= $min[1]) {
+                $min = array($key, $number[1]);
+            }
+        }
+        return $args[$min[0]];
+    }
+    protected function libMax($args)
+    {
+        $numbers = $this->getNormalizedNumbers($args);
+        $max = null;
+        foreach ($numbers as $key => $number) {
+            if (null === $max || $number[1] >= $max[1]) {
+                $max = array($key, $number[1]);
+            }
+        }
+        return $args[$max[0]];
+    }
+    /**
+     * Helper to normalize args containing numbers
+     *
+     * @param array $args
+     *
+     * @return array
+     */
+    protected function getNormalizedNumbers($args)
+    {
+        $unit = null;
+        $originalUnit = null;
+        $numbers = array();
+        foreach ($args as $key => $item) {
+            if ($item[0] !== Type::T_NUMBER) {
+                $this->throwError('%s is not a number', $item[0]);
+                break;
+            }
+            $number = $item->normalize();
+            if (null === $unit) {
+                $unit = $number[2];
+                $originalUnit = $item->unitStr();
+            } elseif ($unit !== $number[2]) {
+                $this->throwError('Incompatible units: "%s" and "%s".', $originalUnit, $item->unitStr());
+                break;
+            }
+            $numbers[$key] = $number;
+        }
+        return $numbers;
+    }
+    protected static $libLength = array('list');
+    protected function libLength($args)
+    {
+        $list = $this->coerceList($args[0]);
+        return count($list[2]);
+    }
+    //protected static $libListSeparator = ['list...'];
+    protected function libListSeparator($args)
+    {
+        if (count($args) > 1) {
+            return 'comma';
+        }
+        $list = $this->coerceList($args[0]);
+        if (count($list[2]) <= 1) {
+            return 'space';
+        }
+        if ($list[1] === ',') {
+            return 'comma';
+        }
+        return 'space';
+    }
+    protected static $libNth = array('list', 'n');
+    protected function libNth($args)
+    {
+        $list = $this->coerceList($args[0]);
+        $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 function libSetNth($args)
+    {
+        $list = $this->coerceList($args[0]);
+        $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];
+        return $list;
+    }
+    protected static $libMapGet = array('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->compileStringContent($this->coerceString($map[1][$i]))) {
+                return $map[2][$i];
+            }
+        }
+        return self::$null;
+    }
+    protected static $libMapKeys = array('map');
+    protected function libMapKeys($args)
+    {
+        $map = $this->assertMap($args[0]);
+        $keys = $map[1];
+        return array(Type::T_LIST, ',', $keys);
+    }
+    protected static $libMapValues = array('map');
+    protected function libMapValues($args)
+    {
+        $map = $this->assertMap($args[0]);
+        $values = $map[2];
+        return array(Type::T_LIST, ',', $values);
+    }
+    protected static $libMapRemove = array('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->compileStringContent($this->coerceString($map[1][$i]))) {
+                array_splice($map[1], $i, 1);
+                array_splice($map[2], $i, 1);
+            }
+        }
+        return $map;
+    }
+    protected static $libMapHasKey = array('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->compileStringContent($this->coerceString($map[1][$i]))) {
+                return true;
+            }
+        }
+        return false;
+    }
+    protected static $libMapMerge = array('map-1', 'map-2');
+    protected function libMapMerge($args)
+    {
+        $map1 = $this->assertMap($args[0]);
+        $map2 = $this->assertMap($args[1]);
+        return array(Type::T_MAP, array_merge($map1[1], $map2[1]), array_merge($map1[2], $map2[2]));
+    }
+    protected static $libKeywords = array('args');
+    protected function libKeywords($args)
+    {
+        $this->assertList($args[0]);
+        $keys = array();
+        $values = array();
+        foreach ($args[0][2] as $name => $arg) {
+            $keys[] = array(Type::T_KEYWORD, $name);
+            $values[] = $arg;
+        }
+        return array(Type::T_MAP, $keys, $values);
+    }
+    protected function listSeparatorForJoin($list1, $sep)
+    {
+        if (!isset($sep)) {
+            return $list1[1];
+        }
+        switch ($this->compileValue($sep)) {
+            case 'comma':
+                return ',';
+            case 'space':
+                return '';
+            default:
+                return $list1[1];
+        }
+    }
+    protected static $libJoin = array('list1', 'list2', 'separator');
+    protected function libJoin($args)
+    {
+        list($list1, $list2, $sep) = $args;
+        $list1 = $this->coerceList($list1, ' ');
+        $list2 = $this->coerceList($list2, ' ');
+        $sep = $this->listSeparatorForJoin($list1, $sep);
+        return array(Type::T_LIST, $sep, array_merge($list1[2], $list2[2]));
+    }
+    protected static $libAppend = array('list', 'val', 'separator');
+    protected function libAppend($args)
+    {
+        list($list1, $value, $sep) = $args;
+        $list1 = $this->coerceList($list1, ' ');
+        $sep = $this->listSeparatorForJoin($list1, $sep);
+        return array(Type::T_LIST, $sep, array_merge($list1[2], array($value)));
+    }
+    protected function libZip($args)
+    {
+        foreach ($args as $arg) {
+            $this->assertList($arg);
+        }
+        $lists = array();
+        $firstList = array_shift($args);
+        foreach ($firstList[2] as $key => $item) {
+            $list = array(Type::T_LIST, '', array($item));
+            foreach ($args as $arg) {
+                if (isset($arg[2][$key])) {
+                    $list[2][] = $arg[2][$key];
+                } else {
+                    break 2;
+                }
+            }
+            $lists[] = $list;
+        }
+        return array(Type::T_LIST, ',', $lists);
+    }
+    protected static $libTypeOf = array('value');
+    protected function libTypeOf($args)
+    {
+        $value = $args[0];
+        switch ($value[0]) {
+            case Type::T_KEYWORD:
+                if ($value === self::$true || $value === self::$false) {
+                    return 'bool';
+                }
+                if ($this->coerceColor($value)) {
+                    return 'color';
+                }
+            // fall-thru
+            case Type::T_FUNCTION:
+                return 'string';
+            case Type::T_LIST:
+                if (isset($value[3]) && $value[3]) {
+                    return 'arglist';
+                }
+            // fall-thru
+            default:
+                return $value[0];
+        }
+    }
+    protected static $libUnit = array('number');
+    protected function libUnit($args)
+    {
+        $num = $args[0];
+        if ($num[0] === Type::T_NUMBER) {
+            return array(Type::T_STRING, '"', array($num->unitStr()));
+        }
+        return '';
+    }
+    protected static $libUnitless = array('number');
+    protected function libUnitless($args)
+    {
+        $value = $args[0];
+        return $value[0] === Type::T_NUMBER && $value->unitless();
+    }
+    protected static $libComparable = array('number-1', 'number-2');
+    protected function libComparable($args)
+    {
+        list($number1, $number2) = $args;
+        if (!isset($number1[0]) || $number1[0] !== Type::T_NUMBER || !isset($number2[0]) || $number2[0] !== Type::T_NUMBER) {
+            $this->throwError('Invalid argument(s) for "comparable"');
+            return;
+        }
+        $number1 = $number1->normalize();
+        $number2 = $number2->normalize();
+        return $number1[2] === $number2[2] || $number1->unitless() || $number2->unitless();
+    }
+    protected static $libStrIndex = array('string', 'substring');
+    protected function libStrIndex($args)
+    {
+        $string = $this->coerceString($args[0]);
+        $stringContent = $this->compileStringContent($string);
+        $substring = $this->coerceString($args[1]);
+        $substringContent = $this->compileStringContent($substring);
+        $result = strpos($stringContent, $substringContent);
+        return $result === false ? self::$null : new Node\Number($result + 1, '');
+    }
+    protected static $libStrInsert = array('string', 'insert', 'index');
+    protected function libStrInsert($args)
+    {
+        $string = $this->coerceString($args[0]);
+        $stringContent = $this->compileStringContent($string);
+        $insert = $this->coerceString($args[1]);
+        $insertContent = $this->compileStringContent($insert);
+        list(, $index) = $args[2];
+        $string[2] = array(substr_replace($stringContent, $insertContent, $index - 1, 0));
+        return $string;
+    }
+    protected static $libStrLength = array('string');
+    protected function libStrLength($args)
+    {
+        $string = $this->coerceString($args[0]);
+        $stringContent = $this->compileStringContent($string);
+        return new Node\Number(strlen($stringContent), '');
+    }
+    protected static $libStrSlice = array('string', 'start-at', 'end-at');
+    protected function libStrSlice($args)
+    {
+        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];
+        if ($start > 0) {
+            $start--;
+        }
+        $end = (int) $args[2][1];
+        $length = $end < 0 ? $end + 1 : ($end > 0 ? $end - $start : $end);
+        $string[2] = $length ? array(substr($stringContent, $start, $length)) : array(substr($stringContent, $start));
+        return $string;
+    }
+    protected static $libToLowerCase = array('string');
+    protected function libToLowerCase($args)
+    {
+        $string = $this->coerceString($args[0]);
+        $stringContent = $this->compileStringContent($string);
+        $string[2] = array(mb_strtolower($stringContent));
+        return $string;
+    }
+    protected static $libToUpperCase = array('string');
+    protected function libToUpperCase($args)
+    {
+        $string = $this->coerceString($args[0]);
+        $stringContent = $this->compileStringContent($string);
+        $string[2] = array(mb_strtoupper($stringContent));
+        return $string;
+    }
+    protected static $libFeatureExists = array('feature');
+    protected function libFeatureExists($args)
+    {
+        $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 function libFunctionExists($args)
+    {
+        $string = $this->coerceString($args[0]);
+        $name = $this->compileStringContent($string);
+        // user defined functions
+        if ($this->has(self::$namespaces['function'] . $name)) {
+            return true;
+        }
+        $name = $this->normalizeName($name);
+        if (isset($this->userFunctions[$name])) {
+            return true;
+        }
+        // built-in functions
+        $f = $this->getBuiltinFunction($name);
+        return $this->toBool(is_callable($f));
+    }
+    protected static $libGlobalVariableExists = array('name');
+    protected function libGlobalVariableExists($args)
+    {
+        $string = $this->coerceString($args[0]);
+        $name = $this->compileStringContent($string);
+        return $this->has($name, $this->rootEnv);
+    }
+    protected static $libMixinExists = array('name');
+    protected function libMixinExists($args)
+    {
+        $string = $this->coerceString($args[0]);
+        $name = $this->compileStringContent($string);
+        return $this->has(self::$namespaces['mixin'] . $name);
+    }
+    protected static $libVariableExists = array('name');
+    protected function libVariableExists($args)
+    {
+        $string = $this->coerceString($args[0]);
+        $name = $this->compileStringContent($string);
+        return $this->has($name);
+    }
+    /**
+     * Workaround IE7's content counter bug.
+     *
+     * @param array $args
+     */
+    protected function libCounter($args)
+    {
+        $list = array_map(array($this, 'compileValue'), $args);
+        return array(Type::T_STRING, '', array('counter(' . implode(',', $list) . ')'));
+    }
+    protected static $libRandom = array('limit');
+    protected function libRandom($args)
+    {
+        if (isset($args[0])) {
+            $n = $this->assertNumber($args[0]);
+            if ($n < 1) {
+                $this->throwError('limit must be greater than or equal to 1');
+                return;
+            }
+            return new Node\Number(mt_rand(1, $n), '');
+        }
+        return new Node\Number(mt_rand(1, mt_getrandmax()), '');
+    }
+    protected function libUniqueId()
+    {
+        static $id;
+        if (!isset($id)) {
+            $id = mt_rand(0, pow(36, 8));
+        }
+        $id += mt_rand(0, 10) + 1;
+        return array(Type::T_STRING, '', array('u' . str_pad(base_convert($id, 10, 36), 8, '0', STR_PAD_LEFT)));
+    }
+    protected static $libInspect = array('value');
+    protected function libInspect($args)
+    {
+        if ($args[0] === self::$null) {
+            return array(Type::T_KEYWORD, 'null');
+        }
+        return $args[0];
+    }
+}

+ 36 - 0
lib/scssphp/src/Compiler/Environment.php

@@ -0,0 +1,36 @@
+<?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;
+}

+ 20 - 0
lib/scssphp/src/Exception/CompilerException.php

@@ -0,0 +1,20 @@
+<?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
+{
+}

+ 20 - 0
lib/scssphp/src/Exception/ParserException.php

@@ -0,0 +1,20 @@
+<?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
+{
+}

+ 20 - 0
lib/scssphp/src/Exception/ServerException.php

@@ -0,0 +1,20 @@
+<?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
+{
+}

+ 178 - 0
lib/scssphp/src/Formatter.php

@@ -0,0 +1,178 @@
+<?php
+/**
+ * SCSSPHP
+ *
+ * @copyright 2012-2015 Leaf Corcoran
+ *
+ * @license http://opensource.org/licenses/MIT MIT
+ *
+ * @link http://leafo.github.io/scssphp
+ */
+namespace Leafo\ScssPhp;
+
+use Leafo\ScssPhp\Formatter\OutputBlock;
+/**
+ * Base formatter
+ *
+ * @author Leaf Corcoran <leafot@gmail.com>
+ */
+abstract class Formatter
+{
+    /**
+     * @var integer
+     */
+    public $indentLevel;
+    /**
+     * @var string
+     */
+    public $indentChar;
+    /**
+     * @var string
+     */
+    public $break;
+    /**
+     * @var string
+     */
+    public $open;
+    /**
+     * @var string
+     */
+    public $close;
+    /**
+     * @var string
+     */
+    public $tagSeparator;
+    /**
+     * @var string
+     */
+    public $assignSeparator;
+    /**
+     * @var boolea
+     */
+    public $keepSemicolons;
+    /**
+     * Initialize formatter
+     *
+     * @api
+     */
+    public abstract function __construct();
+    /**
+     * Return indentation (whitespace)
+     *
+     * @return string
+     */
+    protected function indentStr()
+    {
+        return '';
+    }
+    /**
+     * Return property assignment
+     *
+     * @api
+     *
+     * @param string $name
+     * @param mixed  $value
+     *
+     * @return string
+     */
+    public function property($name, $value)
+    {
+        return rtrim($name) . $this->assignSeparator . $value . ';';
+    }
+    /**
+     * Strip semi-colon appended by property(); it's a separator, not a terminator
+     *
+     * @api
+     *
+     * @param array $lines
+     */
+    public function stripSemicolon(&$lines)
+    {
+        if ($this->keepSemicolons) {
+            return;
+        }
+        if (($count = count($lines)) && substr($lines[$count - 1], -1) === ';') {
+            $lines[$count - 1] = substr($lines[$count - 1], 0, -1);
+        }
+    }
+    /**
+     * Output lines inside a block
+     *
+     * @param \Leafo\ScssPhp\Formatter\OutputBlock $block
+     */
+    protected function blockLines(OutputBlock $block)
+    {
+        $inner = $this->indentStr();
+        $glue = $this->break . $inner;
+        echo $inner . implode($glue, $block->lines);
+        if (!empty($block->children)) {
+            echo $this->break;
+        }
+    }
+    /**
+     * 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 \Leafo\ScssPhp\Formatter\OutputBlock $block
+     */
+    protected function block(OutputBlock $block)
+    {
+        if (empty($block->lines) && empty($block->children)) {
+            return;
+        }
+        $pre = $this->indentStr();
+        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--;
+            if (empty($block->children)) {
+                echo $this->break;
+            }
+            echo $pre . $this->close . $this->break;
+        }
+    }
+    /**
+     * Entry point to formatting a block
+     *
+     * @api
+     *
+     * @param \Leafo\ScssPhp\Formatter\OutputBlock $block An abstract syntax tree
+     *
+     * @return string
+     */
+    public function format(OutputBlock $block)
+    {
+        ob_start();
+        $this->block($block);
+        $out = ob_get_clean();
+        return $out;
+    }
+}

+ 44 - 0
lib/scssphp/src/Formatter/Compact.php

@@ -0,0 +1,44 @@
+<?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;
+/**
+ * Compact formatter
+ *
+ * @author Leaf Corcoran <leafot@gmail.com>
+ */
+class Compact extends Formatter
+{
+    /**
+     * {@inheritdoc}
+     */
+    public function __construct()
+    {
+        $this->indentLevel = 0;
+        $this->indentChar = '';
+        $this->break = '';
+        $this->open = ' {';
+        $this->close = '}
+
+';
+        $this->tagSeparator = ',';
+        $this->assignSeparator = ':';
+        $this->keepSemicolons = true;
+    }
+    /**
+     * {@inheritdoc}
+     */
+    public function indentStr()
+    {
+        return ' ';
+    }
+}

+ 55 - 0
lib/scssphp/src/Formatter/Compressed.php

@@ -0,0 +1,55 @@
+<?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;
+/**
+ * Compressed formatter
+ *
+ * @author Leaf Corcoran <leafot@gmail.com>
+ */
+class Compressed extends Formatter
+{
+    /**
+     * {@inheritdoc}
+     */
+    public function __construct()
+    {
+        $this->indentLevel = 0;
+        $this->indentChar = '  ';
+        $this->break = '';
+        $this->open = '{';
+        $this->close = '}';
+        $this->tagSeparator = ',';
+        $this->assignSeparator = ':';
+        $this->keepSemicolons = false;
+    }
+    /**
+     * {@inheritdoc}
+     */
+    public function blockLines(OutputBlock $block)
+    {
+        $inner = $this->indentStr();
+        $glue = $this->break . $inner;
+        foreach ($block->lines as $index => $line) {
+            if (substr($line, 0, 2) === '/*' && substr($line, 2, 1) !== '!') {
+                unset($block->lines[$index]);
+            } elseif (substr($line, 0, 3) === '/*!') {
+                $block->lines[$index] = '/*' . substr($line, 3);
+            }
+        }
+        echo $inner . implode($glue, $block->lines);
+        if (!empty($block->children)) {
+            echo $this->break;
+        }
+    }
+}

+ 53 - 0
lib/scssphp/src/Formatter/Crunched.php

@@ -0,0 +1,53 @@
+<?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;
+/**
+ * Crunched formatter
+ *
+ * @author Anthon Pang <anthon.pang@gmail.com>
+ */
+class Crunched extends Formatter
+{
+    /**
+     * {@inheritdoc}
+     */
+    public function __construct()
+    {
+        $this->indentLevel = 0;
+        $this->indentChar = '  ';
+        $this->break = '';
+        $this->open = '{';
+        $this->close = '}';
+        $this->tagSeparator = ',';
+        $this->assignSeparator = ':';
+        $this->keepSemicolons = false;
+    }
+    /**
+     * {@inheritdoc}
+     */
+    public function blockLines(OutputBlock $block)
+    {
+        $inner = $this->indentStr();
+        $glue = $this->break . $inner;
+        foreach ($block->lines as $index => $line) {
+            if (substr($line, 0, 2) === '/*') {
+                unset($block->lines[$index]);
+            }
+        }
+        echo $inner . implode($glue, $block->lines);
+        if (!empty($block->children)) {
+            echo $this->break;
+        }
+    }
+}

+ 99 - 0
lib/scssphp/src/Formatter/Debug.php

@@ -0,0 +1,99 @@
+<?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 = '
+';
+        $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);
+    }
+}

+ 61 - 0
lib/scssphp/src/Formatter/Expanded.php

@@ -0,0 +1,61 @@
+<?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;
+/**
+ * Expanded formatter
+ *
+ * @author Leaf Corcoran <leafot@gmail.com>
+ */
+class Expanded extends Formatter
+{
+    /**
+     * {@inheritdoc}
+     */
+    public function __construct()
+    {
+        $this->indentLevel = 0;
+        $this->indentChar = '  ';
+        $this->break = '
+';
+        $this->open = ' {';
+        $this->close = '}';
+        $this->tagSeparator = ', ';
+        $this->assignSeparator = ': ';
+        $this->keepSemicolons = true;
+    }
+    /**
+     * {@inheritdoc}
+     */
+    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) {
+            if (substr($line, 0, 2) === '/*') {
+                $block->lines[$index] = preg_replace('/(\\r|\\n)+/', $glue, $line);
+            }
+        }
+        echo $inner . implode($glue, $block->lines);
+        if (empty($block->selectors) || !empty($block->children)) {
+            echo $this->break;
+        }
+    }
+}

+ 160 - 0
lib/scssphp/src/Formatter/Nested.php

@@ -0,0 +1,160 @@
+<?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;
+/**
+ * Nested formatter
+ *
+ * @author Leaf Corcoran <leafot@gmail.com>
+ */
+class Nested extends Formatter
+{
+    /**
+     * @var integer
+     */
+    private $depth;
+    /**
+     * {@inheritdoc}
+     */
+    public function __construct()
+    {
+        $this->indentLevel = 0;
+        $this->indentChar = '  ';
+        $this->break = '
+';
+        $this->open = ' {';
+        $this->close = ' }';
+        $this->tagSeparator = ', ';
+        $this->assignSeparator = ': ';
+        $this->keepSemicolons = true;
+    }
+    /**
+     * {@inheritdoc}
+     */
+    protected function indentStr()
+    {
+        $n = $this->depth - 1;
+        return str_repeat($this->indentChar, max($this->indentLevel + $n, 0));
+    }
+    /**
+     * {@inheritdoc}
+     */
+    protected function blockLines(OutputBlock $block)
+    {
+        $inner = $this->indentStr();
+        $glue = $this->break . $inner;
+        foreach ($block->lines as $index => $line) {
+            if (substr($line, 0, 2) === '/*') {
+                $block->lines[$index] = preg_replace('/(\\r|\\n)+/', $glue, $line);
+            }
+        }
+        echo $inner . implode($glue, $block->lines);
+        if (!empty($block->children)) {
+            echo $this->break;
+        }
+    }
+    /**
+     * {@inheritdoc}
+     */
+    protected function blockSelectors(OutputBlock $block)
+    {
+        $inner = $this->indentStr();
+        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);
+            if ($i < count($block->children) - 1) {
+                echo $this->break;
+                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;
+        }
+        if ($block->type === 'root') {
+            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 = 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;
+        // make relative to parent
+        foreach ($block->children as $child) {
+            $this->adjustAllChildren($child);
+            $child->depth = $child->depth - $block->depth;
+        }
+    }
+}

+ 44 - 0
lib/scssphp/src/Formatter/OutputBlock.php

@@ -0,0 +1,44 @@
+<?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;
+}

+ 36 - 0
lib/scssphp/src/Node.php

@@ -0,0 +1,36 @@
+<?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;
+}

+ 249 - 0
lib/scssphp/src/Node/Number.php

@@ -0,0 +1,249 @@
+<?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
+     */
+    public static $precision = 5;
+    /**
+     * @see http://www.w3.org/TR/2012/WD-css3-values-20120308/
+     *
+     * @var array
+     */
+    protected static $unitTable = array('in' => array('in' => 1, 'pc' => 6, 'pt' => 72, 'px' => 96, 'cm' => 2.54, 'mm' => 25.4, 'q' => 101.6), 'turn' => array('deg' => 360, 'grad' => 400, 'rad' => 6.283185307179586, 'turn' => 1), 's' => array('s' => 1, 'ms' => 1000), 'Hz' => array('Hz' => 1, 'kHz' => 0.001), 'dpi' => array('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 ? array($initialUnit => 1) : array());
+    }
+    /**
+     * 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 = array();
+        $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 = array();
+        $denominators = array();
+        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 = array();
+            $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 = array();
+        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);
+        }
+    }
+}

BIN
lib/scssphp/src/Parser.php


+ 364 - 0
lib/scssphp/src/Server.php

@@ -0,0 +1,364 @@
+<?php
+/**
+ * SCSSPHP
+ *
+ * @copyright 2012-2015 Leaf Corcoran
+ *
+ * @license http://opensource.org/licenses/MIT MIT
+ *
+ * @link http://leafo.github.io/scssphp
+ */
+namespace Leafo\ScssPhp;
+
+use Leafo\ScssPhp\Compiler;
+use Leafo\ScssPhp\Exception\ServerException;
+use Leafo\ScssPhp\Version;
+/**
+ * Server
+ *
+ * @author Leaf Corcoran <leafot@gmail.com>
+ */
+class Server
+{
+    /**
+     * @var boolean
+     */
+    private $showErrorsAsCSS;
+    /**
+     * @var string
+     */
+    private $dir;
+    /**
+     * @var string
+     */
+    private $cacheDir;
+    /**
+     * @var \Leafo\ScssPhp\Compiler
+     */
+    private $scss;
+    /**
+     * Join path components
+     *
+     * @param string $left  Path component, left of the directory separator
+     * @param string $right Path component, right of the directory separator
+     *
+     * @return string
+     */
+    protected function join($left, $right)
+    {
+        return rtrim($left, '/\\') . DIRECTORY_SEPARATOR . ltrim($right, '/\\');
+    }
+    /**
+     * Get name of requested .scss file
+     *
+     * @return string|null
+     */
+    protected function inputName()
+    {
+        switch (true) {
+            case isset($_GET['p']):
+                return $_GET['p'];
+            case isset($_SERVER['PATH_INFO']):
+                return $_SERVER['PATH_INFO'];
+            case isset($_SERVER['DOCUMENT_URI']):
+                return substr($_SERVER['DOCUMENT_URI'], strlen($_SERVER['SCRIPT_NAME']));
+        }
+    }
+    /**
+     * Get path to requested .scss file
+     *
+     * @return string
+     */
+    protected function findInput()
+    {
+        if (($input = $this->inputName()) && strpos($input, '..') === false && substr($input, -5) === '.scss') {
+            $name = $this->join($this->dir, $input);
+            if (is_file($name) && is_readable($name)) {
+                return $name;
+            }
+        }
+        return false;
+    }
+    /**
+     * Get path to cached .css file
+     *
+     * @return string
+     */
+    protected function cacheName($fname)
+    {
+        return $this->join($this->cacheDir, md5($fname) . '.css');
+    }
+    /**
+     * Get path to meta data
+     *
+     * @return string
+     */
+    protected function metadataName($out)
+    {
+        return $out . '.meta';
+    }
+    /**
+     * Determine whether .scss file needs to be re-compiled.
+     *
+     * @param string $out  Output path
+     * @param string $etag ETag
+     *
+     * @return boolean True if compile required.
+     */
+    protected function needsCompile($out, &$etag)
+    {
+        if (!is_file($out)) {
+            return true;
+        }
+        $mtime = filemtime($out);
+        $metadataName = $this->metadataName($out);
+        if (is_readable($metadataName)) {
+            $metadata = unserialize(file_get_contents($metadataName));
+            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;
+        }
+        return true;
+    }
+    /**
+     * Get If-Modified-Since header from client request
+     *
+     * @return string|null
+     */
+    protected function getIfModifiedSinceHeader()
+    {
+        $modifiedSince = null;
+        if (isset($_SERVER['HTTP_IF_MODIFIED_SINCE'])) {
+            $modifiedSince = $_SERVER['HTTP_IF_MODIFIED_SINCE'];
+            if (false !== ($semicolonPos = strpos($modifiedSince, ';'))) {
+                $modifiedSince = substr($modifiedSince, 0, $semicolonPos);
+            }
+        }
+        return $modifiedSince;
+    }
+    /**
+     * Get If-None-Match header from client request
+     *
+     * @return string|null
+     */
+    protected function getIfNoneMatchHeader()
+    {
+        $noneMatch = null;
+        if (isset($_SERVER['HTTP_IF_NONE_MATCH'])) {
+            $noneMatch = $_SERVER['HTTP_IF_NONE_MATCH'];
+        }
+        return $noneMatch;
+    }
+    /**
+     * Compile .scss file
+     *
+     * @param string $in  Input path (.scss)
+     * @param string $out Output path (.css)
+     *
+     * @return array
+     */
+    protected function compile($in, $out)
+    {
+        $start = microtime(true);
+        $css = $this->scss->compile(file_get_contents($in), $in);
+        $elapsed = round(microtime(true) - $start, 4);
+        $v = Version::VERSION;
+        $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('etag' => $etag, 'imports' => $this->scss->getParsedFiles(), 'vars' => crc32(serialize($this->scss->getVariables())))));
+        return array($css, $etag);
+    }
+    /**
+     * Format error as a pseudo-element in CSS
+     *
+     * @param \Exception $error
+     *
+     * @return string
+     */
+    protected function createErrorCSS(\Exception $error)
+    {
+        $message = str_replace(array('\'', '
+'), array('\\\'', '\\A'), $error->getfile() . ':
+
+' . $error->getMessage());
+        return "body { display: none !important; }\n                html:after {\n                    background: white;\n                    color: black;\n                    content: '{$message}';\n                    display: block !important;\n                    font-family: mono;\n                    padding: 1em;\n                    white-space: pre;\n                }";
+    }
+    /**
+     * Render errors as a pseudo-element within valid CSS, displaying the errors on any
+     * page that includes this CSS.
+     *
+     * @param boolean $show
+     */
+    public function showErrorsAsCSS($show = true)
+    {
+        $this->showErrorsAsCSS = $show;
+    }
+    /**
+     * Compile .scss file
+     *
+     * @param string $in  Input file (.scss)
+     * @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 ServerException('load error: failed to find ' . $in);
+        }
+        $pi = pathinfo($in);
+        $this->scss->addImportPath($pi['dirname'] . '/');
+        $compiled = $this->scss->compile(file_get_contents($in), $in);
+        if ($out !== null) {
+            return file_put_contents($out, $compiled);
+        }
+        return $compiled;
+    }
+    /**
+     * Check if file need compiling
+     *
+     * @param string $in  Input file (.scss)
+     * @param string $out Output file (.css)
+     *
+     * @return bool
+     */
+    public function checkedCompile($in, $out)
+    {
+        if (!is_file($out) || filemtime($in) > filemtime($out)) {
+            $this->compileFile($in, $out);
+            return true;
+        }
+        return false;
+    }
+    /**
+     * Compile requested scss and serve css.  Outputs HTTP response.
+     *
+     * @param string $salt Prefix a string to the filename for creating the cache name hash
+     */
+    public function serve($salt = '')
+    {
+        $protocol = isset($_SERVER['SERVER_PROTOCOL']) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0';
+        if ($input = $this->findInput()) {
+            $output = $this->cacheName($salt . $input);
+            $etag = $noneMatch = trim($this->getIfNoneMatchHeader(), '"');
+            if ($this->needsCompile($output, $etag)) {
+                try {
+                    list($css, $etag) = $this->compile($input, $output);
+                    $lastModified = gmdate('D, d M Y H:i:s', filemtime($output)) . ' GMT';
+                    header('Last-Modified: ' . $lastModified);
+                    header('Content-type: text/css');
+                    header('ETag: "' . $etag . '"');
+                    echo $css;
+                } catch (\Exception $e) {
+                    if ($this->showErrorsAsCSS) {
+                        header('Content-type: text/css');
+                        echo $this->createErrorCSS($e);
+                    } else {
+                        header($protocol . ' 500 Internal Server Error');
+                        header('Content-type: text/plain');
+                        echo 'Parse error: ' . $e->getMessage() . '
+';
+                    }
+                }
+                return;
+            }
+            header('X-SCSS-Cache: true');
+            header('Content-type: text/css');
+            header('ETag: "' . $etag . '"');
+            if ($etag === $noneMatch) {
+                header($protocol . ' 304 Not Modified');
+                return;
+            }
+            $modifiedSince = $this->getIfModifiedSinceHeader();
+            $mtime = filemtime($output);
+            if (strtotime($modifiedSince) === $mtime) {
+                header($protocol . ' 304 Not Modified');
+                return;
+            }
+            $lastModified = gmdate('D, d M Y H:i:s', $mtime) . ' GMT';
+            header('Last-Modified: ' . $lastModified);
+            echo file_get_contents($output);
+            return;
+        }
+        header($protocol . ' 404 Not Found');
+        header('Content-type: text/plain');
+        $v = Version::VERSION;
+        echo "/* INPUT NOT FOUND scss {$v} */\n";
+    }
+    /**
+     * Based on explicit input/output files does a full change check on cache before compiling.
+     *
+     * @param string  $in
+     * @param string  $out
+     * @param boolean $force
+     *
+     * @return string Compiled CSS results
+     *
+     * @throws \Leafo\ScssPhp\Exception\ServerException
+     */
+    public function checkedCachedCompile($in, $out, $force = false)
+    {
+        if (!is_file($in) || !is_readable($in)) {
+            throw new ServerException('Invalid or unreadable input file specified.');
+        }
+        if (is_dir($out) || !is_writable(file_exists($out) ? $out : dirname($out))) {
+            throw new ServerException('Invalid or unwritable output file specified.');
+        }
+        if ($force || $this->needsCompile($out, $etag)) {
+            list($css, $etag) = $this->compile($in, $out);
+        } else {
+            $css = file_get_contents($out);
+        }
+        return $css;
+    }
+    /**
+     * Constructor
+     *
+     * @param string                       $dir      Root directory to .scss files
+     * @param string                       $cacheDir Cache directory
+     * @param \Leafo\ScssPhp\Compiler|null $scss     SCSS compiler instance
+     */
+    public function __construct($dir, $cacheDir = null, $scss = null)
+    {
+        $this->dir = $dir;
+        if (!isset($cacheDir)) {
+            $cacheDir = $this->join($dir, 'scss_cache');
+        }
+        $this->cacheDir = $cacheDir;
+        if (!is_dir($this->cacheDir)) {
+            mkdir($this->cacheDir, 493, true);
+        }
+        if (!isset($scss)) {
+            $scss = new Compiler();
+            $scss->setImportPaths($this->dir);
+        }
+        $this->scss = $scss;
+        $this->showErrorsAsCSS = false;
+        if (!ini_get('date.timezone')) {
+            date_default_timezone_set('UTC');
+        }
+    }
+    /**
+     * Helper method to serve compiled scss
+     *
+     * @param string $path Root path
+     */
+    public static function serveFrom($path)
+    {
+        $server = new self($path);
+        $server->serve();
+    }
+}

+ 68 - 0
lib/scssphp/src/Type.php

@@ -0,0 +1,68 @@
+<?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';
+}

+ 49 - 0
lib/scssphp/src/Util.php

@@ -0,0 +1,49 @@
+<?php
+/**
+ * SCSSPHP
+ *
+ * @copyright 2012-2015 Leaf Corcoran
+ *
+ * @license http://opensource.org/licenses/MIT MIT
+ *
+ * @link http://leafo.github.io/scssphp
+ */
+namespace Leafo\ScssPhp;
+
+use Leafo\ScssPhp\Base\Range;
+/**
+ * Utilties
+ *
+ * @author Anthon Pang <anthon.pang@gmail.com>
+ */
+class Util
+{
+    /**
+     * Asserts that `value` falls within `range` (inclusive), leaving
+     * room for slight floating-point errors.
+     *
+     * @param string $name  The name of the value. Used in the error message.
+     * @param Range  $range Range of values.
+     * @param array  $value The value to check.
+     * @param string $unit  The unit of the value. Used in error reporting.
+     *
+     * @return mixed `value` adjusted to fall within range, if it was outside by a floating-point margin.
+     *
+     * @throws \Exception
+     */
+    public static function checkRange($name, Range $range, $value, $unit = '')
+    {
+        $val = $value[1];
+        $grace = new Range(-1.0E-5, 1.0E-5);
+        if ($range->includes($val)) {
+            return $val;
+        }
+        if ($grace->includes($val - $range->first)) {
+            return $range->first;
+        }
+        if ($grace->includes($val - $range->last)) {
+            return $range->last;
+        }
+        throw new \Exception("{$name} {$val} must be between {$range->first} and {$range->last}{$unit}");
+    }
+}

+ 21 - 0
lib/scssphp/src/Version.php

@@ -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;
+
+/**
+ * SCSSPHP version
+ *
+ * @author Leaf Corcoran <leafot@gmail.com>
+ */
+class Version
+{
+    const VERSION = 'v0.6.3';
+}

+ 24 - 0
setup/licenses/community-licences.xml

@@ -2354,4 +2354,28 @@ END OF TERMS AND CONDITIONS
 
 </pre>]]></text>
   </license>
+  <license>
+    <product>scssphp</product>
+    <author>Leaf Corcoran</author>
+    <license_type>MIT</license_type>
+    <text><![CDATA[<pre>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.
+</pre>]]></text>
+  </license>
 </licenses>

Niektóre pliki nie zostały wyświetlone z powodu dużej ilości zmienionych plików