diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 00000000..31dd6aa4 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,14 @@ +root = true + +[*] +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true +charset = utf-8 + +[Makefile] +indent_style = tab + +[*.php] +indent_style = space +indent_size = 4 diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 00000000..73d1a258 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,8 @@ +Makefile export-ignore +.editorconfig export-ignore +.gitattributes export-ignore +.gitignore export-ignore +.travis.yml export-ignore +box.json.dist export-ignore +phpunit.xml.dist export-ignore +tests/ export-ignore diff --git a/.gitignore b/.gitignore index c8e6336e..999984a4 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,11 @@ -/*.scss -/*.css -/*.php +.idea .sass-cache +scss_cache +composer.lock +/*.css +/*.scss +/_site/ /sass/ -/compass/ \ No newline at end of file +/compass/ +/vendor/ +/frameworks/ diff --git a/.travis.yml b/.travis.yml index d7b25e03..ccbbf00b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,5 +1,22 @@ language: php -script: phpunit tests +sudo: false + php: - - 5.3 - - 5.4 + - 5.6 + - 7.0 + - 7.1 + - 7.2 + - nightly + +install: composer install + +script: + - vendor/bin/phpunit tests + +branches: + only: + - master + +matrix: + allow_failures: + - php: nightly diff --git a/LICENSE.md b/LICENSE.md index f19ca081..2f5412f9 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -1,21 +1,20 @@ - -License (MIT) -Copyright (C) 2012 by Leaf Corcoran - -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. +Copyright (c) 2015 Leaf Corcoran, http://leafo.github.io/scssphp + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/Makefile b/Makefile index 7b72c70f..fb50ffdb 100644 --- a/Makefile +++ b/Makefile @@ -1,3 +1,8 @@ - test: - phpunit --colors tests \ No newline at end of file + vendor/bin/phpunit --colors tests + +compat: + TEST_SCSS_COMPAT=1 vendor/bin/phpunit --colors tests | tail -2 + +standard: + vendor/bin/phpcs --standard=PSR2 bin src example tests *.php diff --git a/README.md b/README.md index 82d06130..d3fbf154 100644 --- a/README.md +++ b/README.md @@ -1,34 +1,50 @@ -# scssphp v0.0.4 -### +# This repo has been archived -[![Build Status](https://secure.travis-ci.org/leafo/scssphp.png)](http://travis-ci.org/leafo/scssphp) -`scssphp` is a compiler for SCSS written in PHP. +#### Please go to https://github.com/scssphp/scssphp + +---- -It implements SCSS 3.1.20. It does not implement the SASS syntax, only the SCSS -syntax. +## scssphp -Checkout the homepage, , for directions on how to use. +![License](https://poser.pugx.org/leafo/scssphp/license.svg) +`scssphp` is a compiler for SCSS written in PHP. -## Running Tests +### Running Tests `scssphp` uses [PHPUnit](https://github.com/sebastianbergmann/phpunit) for testing. Run the following command from the root directory to run every test: - phpunit tests + vendor/bin/phpunit tests -There are two kinds of tests in the `tests/` directory: +There are several tests in the `tests/` directory: -* `ApiTests.php` contains various unit tests that test the PHP interface. -* `InputTests.php` compiles every `.scss` file in the `tests/inputs` 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=true phpunit 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 diff --git a/bin/pscss b/bin/pscss new file mode 100755 index 00000000..ce80e6a9 --- /dev/null +++ b/bin/pscss @@ -0,0 +1,215 @@ +#!/usr/bin/env php +parse($data)), true)); + + exit(); +} + +$scss = new Compiler(); + +if ($debugInfo) { + $scss->setLineNumberStyle(Compiler::DEBUG_INFO); +} + +if ($lineNumbers) { + $scss->setLineNumberStyle(Compiler::LINE_COMMENTS); +} + +if ($ignoreErrors) { + $scss->setIgnoreErrors($ignoreErrors); +} + +if ($loadPaths) { + $scss->setImportPaths(explode(PATH_SEPARATOR, $loadPaths)); +} + +if ($precision) { + $scss->setNumberPrecision($precision); +} + +if ($style) { + $scss->setFormatter('Leafo\\ScssPhp\\Formatter\\' . ucfirst($style)); +} + +if ($sourceMap) { + $scss->setSourceMap(Compiler::SOURCE_MAP_INLINE); +} + +if ($encoding) { + $scss->setEncoding($encoding); +} + +echo $scss->compile($data, $inputFile); + +if ($changeDir) { + chdir($oldWorkingDir); +} diff --git a/composer.json b/composer.json index c3a6e3f5..eaa8e87b 100644 --- a/composer.json +++ b/composer.json @@ -2,10 +2,10 @@ "name": "leafo/scssphp", "type": "library", "description": "scssphp is a compiler for SCSS written in PHP.", - "homepage": "http://leafo.net/scssphp/", + "keywords": ["css", "stylesheet", "scss", "sass", "less"], + "homepage": "http://leafo.github.io/scssphp/", "license": [ - "MIT", - "GPL-3.0" + "MIT" ], "authors": [ { @@ -15,13 +15,30 @@ } ], "autoload": { - "classmap": ["scss.inc.php"] + "psr-4": { "Leafo\\ScssPhp\\": "src/" } + }, + "autoload-dev": { + "psr-4": { "Leafo\\ScssPhp\\Test\\": "tests/" } }, "require": { - "php": ">=5.2.0" + "php": "^5.4.0 || ^7" }, "require-dev": { - "php": ">=5.3.0", - "phpunit/phpunit": "3.7.*" - } + "squizlabs/php_codesniffer": "~2.5", + "phpunit/phpunit": "~4.6", + "twbs/bootstrap": "~4.3", + "zurb/foundation": "~6.5" + }, + "bin": ["bin/pscss"], + "archive": { + "exclude": [ + "/Makefile", + "/.gitattributes", + "/.gitignore", + "/.travis.yml", + "/phpunit.xml.dist", + "/tests" + ] + }, + "abandoned": "scssphp/scssphp" } diff --git a/example/Server.php b/example/Server.php new file mode 100644 index 00000000..54060b1a --- /dev/null +++ b/example/Server.php @@ -0,0 +1,515 @@ + + */ +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 = gmdate('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([ + 'etag' => $etag, + 'imports' => $this->scss->getParsedFiles(), + 'vars' => crc32(serialize($this->scss->getVariables())), + ]) + ); + + return [$css, $etag]; + } + + /** + * Format error as a pseudo-element in CSS + * + * @param \Exception $error + * + * @return string + */ + protected function createErrorCSS(\Exception $error) + { + $message = str_replace( + ["'", "\n"], + ["\\'", "\\A"], + $error->getfile() . ":\n\n" . $error->getMessage() + ); + + return "body { display: none !important; } + html:after { + background: white; + color: black; + content: '$message'; + display: block !important; + font-family: mono; + padding: 1em; + white-space: pre; + }"; + } + + /** + * 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('r', filemtime($output)); + + 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() . "\n"; + } + } + + 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('r', $mtime); + 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; + } + + /** + * Execute scssphp on a .scss file or a scssphp cache structure + * + * The scssphp cache structure contains information about a specific + * scss file having been parsed. It can be used as a hint for future + * calls to determine whether or not a rebuild is required. + * + * The cache structure contains two important keys that may be used + * externally: + * + * compiled: The final compiled CSS + * updated: The time (in seconds) the CSS was last compiled + * + * The cache structure is a plain-ol' PHP associative array and can + * be serialized and unserialized without a hitch. + * + * @param mixed $in Input + * @param boolean $force Force rebuild? + * + * @return array scssphp cache structure + */ + public function cachedCompile($in, $force = false) + { + // assume no root + $root = null; + + if (is_string($in)) { + $root = $in; + } elseif (is_array($in) and isset($in['root'])) { + if ($force or ! isset($in['files'])) { + // If we are forcing a recompile or if for some reason the + // structure does not contain any file information we should + // specify the root to trigger a rebuild. + $root = $in['root']; + } elseif (isset($in['files']) and is_array($in['files'])) { + foreach ($in['files'] as $fname => $ftime) { + if (! file_exists($fname) or filemtime($fname) > $ftime) { + // One of the files we knew about previously has changed + // so we should look at our incoming root again. + $root = $in['root']; + break; + } + } + } + } else { + // TODO: Throw an exception? We got neither a string nor something + // that looks like a compatible lessphp cache structure. + return null; + } + + if ($root !== null) { + // If we have a root value which means we should rebuild. + $out = []; + $out['root'] = $root; + $out['compiled'] = $this->compileFile($root); + $out['files'] = $this->scss->getParsedFiles(); + $out['updated'] = time(); + return $out; + } else { + // No changes, pass back the structure + // we were given initially. + return $in; + } + } + + /** + * 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)) { + throw new ServerException('Cache directory doesn\'t exist: ' . $cacheDir); + } + + if (! isset($scss)) { + $scss = new Compiler(); + $scss->setImportPaths($this->dir); + } + + $this->scss = $scss; + $this->showErrorsAsCSS = false; + + date_default_timezone_set('UTC'); + } +} diff --git a/package.sh b/package.sh deleted file mode 100755 index 02a528da..00000000 --- a/package.sh +++ /dev/null @@ -1,24 +0,0 @@ -#!/bin/sh - -# creates tar.gz for current version - -TARGET_DIR="site/www/src" - -VERSION=`./pscss -v | sed -n 's/^v\(.*\)$/\1/p'` -OUT_DIR="tmp/scssphp" -TMP=`dirname $OUT_DIR` - -mkdir -p $OUT_DIR -tar -c `git ls-files` | tar -C $OUT_DIR -x - -rm $OUT_DIR/.gitignore -rm $OUT_DIR/package.sh -rm $OUT_DIR/todo -rm -r $OUT_DIR/site - -OUT_PATH="$TARGET_DIR/scssphp-$VERSION.tar.gz" -tar -czf "$OUT_PATH" -C $TMP scssphp/ -echo "Wrote $OUT_PATH" - -rm -r $TMP - diff --git a/phpunit.xml.dist b/phpunit.xml.dist new file mode 100644 index 00000000..c007930c --- /dev/null +++ b/phpunit.xml.dist @@ -0,0 +1,28 @@ + + + + + + + + tests + + + + + + src + + + + diff --git a/pscss b/pscss deleted file mode 100755 index 7d8cba80..00000000 --- a/pscss +++ /dev/null @@ -1,53 +0,0 @@ -#!/usr/bin/env php -parse($data)); - exit(); -} - -$scss = new scssc(); -if (has("f")) { - $scss->setFormatter($opts["f"]); -} -echo $scss->compile($data); diff --git a/scss.inc.php b/scss.inc.php index 92002e9d..2a5f0774 100644 --- a/scss.inc.php +++ b/scss.inc.php @@ -1,3943 +1,34 @@ "add", - '-' => "sub", - '*' => "mul", - '/' => "div", - '%' => "mod", - - '==' => "eq", - '!=' => "neq", - '<' => "lt", - '>' => "gt", - - '<=' => "lte", - '>=' => "gte", - ); - - static protected $namespaces = array( - "special" => "%", - "mixin" => "@", - "function" => "^", - ); - - static protected $numberPrecision = 3; - static protected $unitTable = array( - "in" => array( - "in" => 1, - "pt" => 72, - "pc" => 6, - "cm" => 2.54, - "mm" => 25.4, - "px" => 96, - ) - ); - - static public $true = array("keyword", "true"); - static public $false = array("keyword", "false"); - - static public $defaultValue = array("keyword", ""); - static public $selfSelector = array("self"); - - protected $importPaths = array(""); - protected $importCache = array(); - - protected $userFunctions = array(); - - protected $formatter = "scss_formatter_nested"; - - function compile($code, $name=null) { - $this->indentLevel = -1; - $this->commentsSeen = array(); - $this->extends = array(); - $this->extendsMap = array(); - - $locale = setlocale(LC_NUMERIC, 0); - setlocale(LC_NUMERIC, "C"); - - $this->parsedFiles = array(); - $this->parser = new scss_parser($name); - $tree = $this->parser->parse($code); - - $this->formatter = new $this->formatter(); - - $this->env = null; - $this->scope = null; - - $this->compileRoot($tree); - $this->flattenSelectors($this->scope); - - ob_start(); - $this->formatter->block($this->scope); - $out = ob_get_clean(); - - setlocale(LC_NUMERIC, $locale); - return $out; - } - - protected function pushExtends($target, $origin) { - $i = count($this->extends); - $this->extends[] = array($target, $origin); - - foreach ($target as $part) { - if (isset($this->extendsMap[$part])) { - $this->extendsMap[$part][] = $i; - } else { - $this->extendsMap[$part] = array($i); - } - } - } - - protected function makeOutputBlock($type, $selectors = null) { - $out = new stdClass; - $out->type = $type; - $out->lines = array(); - $out->children = array(); - $out->parent = $this->scope; - $out->selectors = $selectors; - $out->depth = $this->env->depth; - - return $out; - } - - protected function matchExtendsSingle($single, &$out_origin, &$out_rem) { - $counts = array(); - foreach ($single as $part) { - if (!is_string($part)) return false; // hmm - - if (isset($this->extendsMap[$part])) { - foreach ($this->extendsMap[$part] as $idx) { - $counts[$idx] = - isset($counts[$idx]) ? $counts[$idx] + 1 : 1; - } - } - } - - foreach ($counts as $idx => $count) { - list($target, $origin) = $this->extends[$idx]; - // check count - if ($count != count($target)) continue; - // check if target is subset of single - if (array_diff(array_intersect($single, $target), $target)) continue; - - $out_origin = $origin; - $out_rem = array_diff($single, $target); - - return true; - } - - return false; - } - - protected function combineSelectorSingle($base, $other) { - $tag = null; - $out = array(); - - foreach (array($base, $other) as $single) { - foreach ($single as $part) { - if (preg_match('/^[^.#:]/', $part)) { - $tag = $part; - } else { - $out[] = $part; - } - } - } - - if ($tag) { - array_unshift($out, $tag); - } - - return $out; - } - - protected function matchExtends($selector, &$out, $from = 0, $initial=true) { - foreach ($selector as $i => $part) { - if ($i < $from) continue; - - if ($this->matchExtendsSingle($part, $origin, $rem)) { - $before = array_slice($selector, 0, $i); - $after = array_slice($selector, $i + 1); - - foreach ($origin as $new) { - $new[count($new) - 1] = - $this->combineSelectorSingle(end($new), $rem); - - $k = 0; - // remove shared parts - if ($initial) { - foreach ($before as $k => $val) { - if (!isset($new[$k]) || $val != $new[$k]) { - break; - } - } - } - - $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; - } - } - } - } - } - - protected function flattenSelectors($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); - } - } - - $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); - } - } - - protected function compileRoot($rootBlock) { - $this->pushEnv($rootBlock); - $this->scope = $this->makeOutputBlock("root"); - $this->compileChildren($rootBlock->children, $this->scope); - $this->popEnv(); - } - - protected function compileMedia($media) { - $this->pushEnv($media); - $parentScope = $this->mediaParent($this->scope); - - $this->scope = $this->makeOutputBlock("media", array( - $this->compileMediaQuery($this->multiplyMedia($this->env))) - ); - - $parentScope->children[] = $this->scope; - - $this->compileChildren($media->children, $this->scope); - - $this->scope = $this->scope->parent; - $this->popEnv(); - } - - protected function mediaParent($scope) { - while (!empty($scope->parent)) { - if (!empty($scope->type) && $scope->type != "media") { - break; - } - $scope = $scope->parent; - } - - return $scope; - } - - // TODO refactor compileNestedBlock and compileMedia into same thing - protected function compileNestedBlock($block, $selectors) { - $this->pushEnv($block); - - $this->scope = $this->makeOutputBlock($block->type, $selectors); - $this->scope->parent->children[] = $this->scope; - $this->compileChildren($block->children, $this->scope); - - $this->scope = $this->scope->parent; - $this->popEnv(); - } - - protected function compileBlock($block) { - $env = $this->pushEnv($block); - - $env->selectors = - array_map(array($this, "evalSelector"), $block->selectors); - - $out = $this->makeOutputBlock(null, $this->multiplySelectors($env)); - $this->scope->children[] = $out; - $this->compileChildren($block->children, $out); - - $this->popEnv(); - } - - // joins together .classes and #ids - 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; - } - - // replaces all the interpolates - protected function evalSelector($selector) { - return array_map(array($this, "evalSelectorPart"), $selector); - } - - protected function evalSelectorPart($piece) { - foreach ($piece as &$p) { - if (!is_array($p)) continue; - - switch ($p[0]) { - case "interpolate": - $p = $this->compileValue($p); - break; - case "string": - $p = $this->compileValue($p); - break; - } - } - - return $this->flattenSelectorSingle($piece); - } - - // compiles to string - // self(&) should have been replaced by now - protected function compileSelector($selector) { - if (!is_array($selector)) return $selector; // media and the like - - return implode(" ", array_map( - array($this, "compileSelectorPart"), $selector)); - } - - protected function compileSelectorPart($piece) { - foreach ($piece as &$p) { - if (!is_array($p)) continue; - - switch ($p[0]) { - case "self": - $p = "&"; - break; - default: - $p = $this->compileValue($p); - break; - } - } - - return implode($piece); - } - - protected function hasSelectorPlaceholder($selector) - { - if (!is_array($selector)) return false; - - foreach ($selector as $parts) { - foreach ($parts as $part) { - if ('%' == $part[0]) { - return true; - } - } - } - - return false; - } - - protected function compileChildren($stms, $out) { - foreach ($stms as $stm) { - $ret = $this->compileChild($stm, $out); - if (!is_null($ret)) return $ret; - } - } - - protected function compileMediaQuery($queryList) { - $out = "@media"; - $first = true; - foreach ($queryList as $query){ - $parts = array(); - foreach ($query as $q) { - switch ($q[0]) { - case "mediaType": - $parts[] = implode(" ", array_map(array($this, "compileValue"), array_slice($q, 1))); - break; - case "mediaExp": - if (isset($q[2])) { - $parts[] = "(". $this->compileValue($q[1]) . $this->formatter->assignSeparator . $this->compileValue($q[2]) . ")"; - } else { - $parts[] = "(" . $this->compileValue($q[1]) . ")"; - } - break; - } - } - if (!empty($parts)) { - if ($first) { - $first = false; - $out .= " "; - } else { - $out .= $this->formatter->tagSeparator; - } - $out .= implode(" and ", $parts); - } - } - return $out; - } - - // returns true if the value was something that could be imported - protected function compileImport($rawPath, $out) { - if ($rawPath[0] == "string") { - $path = $this->compileStringContent($rawPath); - if ($path = $this->findImport($path)) { - $this->importFile($path, $out); - return true; - } - return false; - } if ($rawPath[0] == "list") { - // handle a list of strings - if (count($rawPath[2]) == 0) return false; - foreach ($rawPath[2] as $path) { - if ($path[0] != "string") return false; - } - - foreach ($rawPath[2] as $path) { - $this->compileImport($path, $out); - } - - return true; - } - - return false; - } - - // return a value to halt execution - protected function compileChild($child, $out) { - switch ($child[0]) { - case "import": - list(,$rawPath) = $child; - $rawPath = $this->reduce($rawPath); - if (!$this->compileImport($rawPath, $out)) { - $out->lines[] = "@import " . $this->compileValue($rawPath) . ";"; - } - break; - case "directive": - list(, $directive) = $child; - $s = "@" . $directive->name; - if (!empty($directive->value)) { - $s .= " " . $this->compileValue($directive->value); - } - $this->compileNestedBlock($directive, array($s)); - break; - case "media": - $this->compileMedia($child[1]); - break; - case "block": - $this->compileBlock($child[1]); - break; - case "charset": - $out->lines[] = "@charset ".$this->compileValue($child[1]).";"; - break; - case "assign": - list(,$name, $value) = $child; - if ($name[0] == "var") { - $isDefault = !empty($child[3]); - if (!$isDefault || $this->get($name[1], true) === true) { - $this->set($name[1], $this->reduce($value)); - } - break; - } - - $out->lines[] = $this->formatter->property( - $this->compileValue($child[1]), - $this->compileValue($child[2])); - break; - case "comment": - $out->lines[] = $child[1]; - break; - case "mixin": - case "function": - list(,$block) = $child; - $this->set(self::$namespaces[$block->type] . $block->name, $block); - break; - case "extend": - list(, $selectors) = $child; - foreach ($selectors as $sel) { - // only use the first one - $sel = current($this->evalSelector($sel)); - $this->pushExtends($sel, $out->selectors); - } - break; - case "if": - list(, $if) = $child; - if ($this->reduce($if->cond, true) != self::$false) { - return $this->compileChildren($if->children, $out); - } else { - foreach ($if->cases as $case) { - if ($case->type == "else" || - $case->type == "elseif" && ($this->reduce($case->cond) != self::$false)) - { - return $this->compileChildren($case->children, $out); - } - } - } - break; - case "return": - return $this->reduce($child[1], true); - case "each": - list(,$each) = $child; - $list = $this->reduce($this->coerceList($each->list)); - foreach ($list[2] as $item) { - $this->pushEnv(); - $this->set($each->var, $item); - // TODO: allow return from here - $this->compileChildren($each->children, $out); - $this->popEnv(); - } - break; - case "while": - list(,$while) = $child; - while ($this->reduce($while->cond, true) != self::$false) { - $ret = $this->compileChildren($while->children, $out); - if ($ret) return $ret; - } - break; - case "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, array("number", $start, "")); - $start += $d; - - $ret = $this->compileChildren($for->children, $out); - if ($ret) return $ret; - } - - break; - case "nestedprop": - list(,$prop) = $child; - $prefixed = array(); - $prefix = $this->compileValue($prop->prefix) . "-"; - foreach ($prop->children as $child) { - if ($child[0] == "assign") { - array_unshift($child[1][2], $prefix); - } - if ($child[0] == "nestedprop") { - array_unshift($child[1]->prefix[2], $prefix); - } - $prefixed[] = $child; - } - $this->compileChildren($prefixed, $out); - break; - case "include": // including a mixin - list(,$name, $argValues, $content) = $child; - $mixin = $this->get(self::$namespaces["mixin"] . $name, false); - if (!$mixin) { - throw new Exception(sprintf('Undefined mixin "%s"', $name)); - } - - $callingScope = $this->env; - - // push scope, apply args - $this->pushEnv(); - if ($this->env->depth > 0) { - $this->env->depth--; - } - - if (!is_null($content)) { - $content->scope = $callingScope; - $this->setRaw(self::$namespaces["special"] . "content", $content); - } - - if (!is_null($mixin->args)) { - $this->applyArguments($mixin->args, $argValues); - } - - foreach ($mixin->children as $child) { - $this->compileChild($child, $out); - } - - $this->popEnv(); - - break; - case "mixin_content": - $content = $this->get(self::$namespaces["special"] . "content"); - if (is_null($content)) { - throw new Exception("Unexpected @content inside of mixin"); - } - - $this->storeEnv = $content->scope; - - foreach ($content->children as $child) { - $this->compileChild($child, $out); - } - - unset($this->storeEnv); - break; - case "debug": - list(,$value, $pos) = $child; - $line = $this->parser->getLineNo($pos); - $value = $this->compileValue($this->reduce($value, true)); - fwrite(STDERR, "Line $line DEBUG: $value\n"); - break; - default: - throw new Exception("unknown child type: $child[0]"); - } - } - - protected function expToString($exp) { - list(, $op, $left, $right, $inParens, $whiteLeft, $whiteRight) = $exp; - $content = array($left); - if ($whiteLeft) $content[] = " "; - $content[] = $op; - if ($whiteRight) $content[] = " "; - $content[] = $right; - return array("string", "", $content); - } - - // should $value cause its operand to eval - protected function shouldEval($value) { - switch ($value[0]) { - case "exp": - if ($value[1] == "/") { - return $this->shouldEval($value[2], $value[3]); - } - case "var": - case "fncall": - return true; - } - return false; - } - - protected function reduce($value, $inExp = false) { - list($type) = $value; - switch ($type) { - case "exp": - list(, $op, $left, $right, $inParens) = $value; - $opName = isset(self::$operatorNames[$op]) ? self::$operatorNames[$op] : $op; - - $inExp = $inExp || $this->shouldEval($left) || $this->shouldEval($right); - - $left = $this->reduce($left, true); - $right = $this->reduce($right, true); - - // only do division in special cases - if ($opName == "div" && !$inParens && !$inExp) { - if ($left[0] != "color" && $right[0] != "color") { - return $this->expToString($value); - } - } - - $left = $this->coerceForExpression($left); - $right = $this->coerceForExpression($right); - - $ltype = $left[0]; - $rtype = $right[0]; - - // 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_${opName}_${ltype}_${rtype}"; - if (is_callable(array($this, $fn)) || - (($fn = "op_${ltype}_${rtype}") && - is_callable(array($this, $fn)) && - $passOp = true) || - (($fn = "op_${opName}") && - is_callable(array($this, $fn)) && - $genOp = true)) - { - $unitChange = false; - if (!isset($genOp) && - $left[0] == "number" && $right[0] == "number") - { - if ($opName == "mod" && $right[2] != "") { - throw new Exception(sprintf('Cannot modulo by a number with units: %s%s.', $right[1], $right[2])); - } - - $unitChange = true; - $emptyUnit = $left[2] == "" || $right[2] == ""; - $targetUnit = "" != $left[2] ? $left[2] : $right[2]; - - if ($opName != "mul") { - $left[2] = "" != $left[2] ? $left[2] : $targetUnit; - $right[2] = "" != $right[2] ? $right[2] : $targetUnit; - } - - if ($opName != "mod") { - $left = $this->normalizeNumber($left); - $right = $this->normalizeNumber($right); - } - - if ($opName == "div" && !$emptyUnit && $left[2] == $right[2]) { - $targetUnit = ""; - } - - if ($opName == "mul") { - $left[2] = "" != $left[2] ? $left[2] : $right[2]; - $right[2] = "" != $right[2] ? $right[2] : $left[2]; - } elseif ($opName == "div" && $left[2] == $right[2]) { - $left[2] = ""; - $right[2] = ""; - } - } - - $shouldEval = $inParens || $inExp; - if (isset($passOp)) { - $out = $this->$fn($op, $left, $right, $shouldEval); - } else { - $out = $this->$fn($left, $right, $shouldEval); - } - - if (!is_null($out)) { - if ($unitChange && $out[0] == "number") { - $out = $this->coerceUnit($out, $targetUnit); - } - return $out; - } - } - - return $this->expToString($value); - case "unary": - list(, $op, $exp, $inParens) = $value; - $inExp = $inExp || $this->shouldEval($exp); - - $exp = $this->reduce($exp); - if ($exp[0] == "number") { - switch ($op) { - case "+": - return $exp; - case "-": - $exp[1] *= -1; - return $exp; - } - } - - if ($op == "not") { - if ($inExp || $inParens) { - if ($exp == self::$false) { - return self::$true; - } else { - return self::$false; - } - } else { - $op = $op . " "; - } - } - - return array("string", "", array($op, $exp)); - case "var": - list(, $name) = $value; - return $this->reduce($this->get($name)); - case "list": - foreach ($value[2] as &$item) { - $item = $this->reduce($item); - } - return $value; - case "string": - foreach ($value[2] as &$item) { - if (is_array($item)) { - $item = $this->reduce($item); - } - } - return $value; - case "interpolate": - $value[1] = $this->reduce($value[1]); - return $value; - case "fncall": - list(,$name, $argValues) = $value; - - // user defined function? - $func = $this->get(self::$namespaces["function"] . $name, false); - if ($func) { - $this->pushEnv(); - - // set the args - if (isset($func->args)) { - $this->applyArguments($func->args, $argValues); - } - - // throw away lines and children - $tmp = (object)array( - "lines" => array(), - "children" => array() - ); - $ret = $this->compileChildren($func->children, $tmp); - $this->popEnv(); - - return is_null($ret) ? self::$defaultValue : $ret; - } - - // built in function - if ($this->callBuiltin($name, $argValues, $returnValue)) { - return $returnValue; - } - - // need to flatten the arguments into a list - $listArgs = array(); - foreach ((array)$argValues as $arg) { - if (empty($arg[0])) { - $listArgs[] = $this->reduce($arg[1]); - } - } - return array("function", $name, array("list", ",", $listArgs)); - default: - return $value; - } - } - - public function normalizeValue($value) { - $value = $this->coerceForExpression($this->reduce($value)); - list($type) = $value; - - switch ($type) { - case "list": - $value = $this->extractInterpolation($value); - if ($value[0] != "list") { - return array("keyword", $this->compileValue($value)); - } - foreach ($value[2] as $key => $item) { - $value[2][$key] = $this->normalizeValue($item); - } - return $value; - case "number": - return $this->normalizeNumber($value); - default: - return $value; - } - } - - // just does physical lengths for now - protected function normalizeNumber($number) { - list(, $value, $unit) = $number; - if (isset(self::$unitTable["in"][$unit])) { - $conv = self::$unitTable["in"][$unit]; - return array("number", $value / $conv, "in"); - } - return $number; - } - - // $number should be normalized - protected function coerceUnit($number, $unit) { - list(, $value, $baseUnit) = $number; - if (isset(self::$unitTable[$baseUnit][$unit])) { - $value = $value * self::$unitTable[$baseUnit][$unit]; - } - - return array("number", $value, $unit); - } - - protected function op_add_number_number($left, $right) { - return array("number", $left[1] + $right[1], $left[2]); - } - - protected function op_mul_number_number($left, $right) { - return array("number", $left[1] * $right[1], $left[2]); - } - - protected function op_sub_number_number($left, $right) { - return array("number", $left[1] - $right[1], $left[2]); - } - - protected function op_div_number_number($left, $right) { - return array("number", $left[1] / $right[1], $left[2]); - } - - protected function op_mod_number_number($left, $right) { - return array("number", $left[1] % $right[1], $left[2]); - } - - // adding strings - protected function op_add($left, $right) { - if ($strLeft = $this->coerceString($left)) { - if ($right[0] == "string") { - $right[1] = ""; - } - $strLeft[2][] = $right; - return $strLeft; - } - - if ($strRight = $this->coerceString($right)) { - if ($left[0] == "string") { - $left[1] = ""; - } - array_unshift($strRight[2], $left); - return $strRight; - } - } - - protected function op_and($left, $right, $shouldEval) { - if (!$shouldEval) return; - if ($left != self::$false) return $right; - return $left; - } - - protected function op_or($left, $right, $shouldEval) { - if (!$shouldEval) return; - if ($left != self::$false) return $left; - return $right; - } - - protected function op_color_color($op, $left, $right) { - $out = array('color'); - foreach (range(1, 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) { - throw new Exception("color: Can't divide by zero"); - } - $out[] = $lval / $rval; - break; - default: - throw new Exception("color: unknow op $op"); - } - } - - if (isset($left[4])) $out[4] = $left[4]; - elseif (isset($right[4])) $out[4] = $right[4]; - - return $this->fixColor($out); - } - - protected function op_color_number($op, $left, $right) { - $value = $right[1]; - return $this->op_color_color($op, $left, - array("color", $value, $value, $value)); - } - - protected function op_number_color($op, $left, $right) { - $value = $left[1]; - return $this->op_color_color($op, - array("color", $value, $value, $value), $right); - } - - protected function op_eq($left, $right) { - if (($lStr = $this->coerceString($left)) && ($rStr = $this->coerceString($right))) { - $lStr[1] = ""; - $rStr[1] = ""; - return $this->toBool($this->compileValue($lStr) == $this->compileValue($rStr)); - } - - return $this->toBool($left == $right); - } - - protected function op_neq($left, $right) { - return $this->toBool($left != $right); - } - - protected function op_gte_number_number($left, $right) { - return $this->toBool($left[1] >= $right[1]); - } - - protected function op_gt_number_number($left, $right) { - return $this->toBool($left[1] > $right[1]); - } - - protected function op_lte_number_number($left, $right) { - return $this->toBool($left[1] <= $right[1]); - } - - protected function op_lt_number_number($left, $right) { - return $this->toBool($left[1] < $right[1]); - } - - protected function toBool($thing) { - return $thing ? self::$true : self::$false; - } - - protected function compileValue($value) { - $value = $this->reduce($value); - - list($type) = $value; - switch ($type) { - case "keyword": - return $value[1]; - case "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 "number": - return round($value[1], self::$numberPrecision) . $value[2]; - case "string": - return $value[1] . $this->compileStringContent($value) . $value[1]; - case "function": - $args = !empty($value[2]) ? $this->compileValue($value[2]) : ""; - return "$value[1]($args)"; - case "list": - $value = $this->extractInterpolation($value); - if ($value[0] != "list") return $this->compileValue($value); - - list(, $delim, $items) = $value; - foreach ($items as &$item) { - $item = $this->compileValue($item); - } - return implode("$delim ", $items); - case "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 "interpolate": # raw parse node - list(, $exp) = $value; - - // strip quotes if it's a string - $reduced = $this->reduce($exp); - if ($reduced[0] == "string") { - $reduced = array("keyword", - $this->compileStringContent($reduced)); - } - - return $this->compileValue($reduced); - default: - throw new Exception("unknown value type: $type"); - } - } - - protected function compileStringContent($string) { - $parts = array(); - foreach ($string[2] as $part) { - if (is_array($part)) { - $parts[] = $this->compileValue($part); - } else { - $parts[] = $part; - } - } - - return implode($parts); - } - - // doesn't need to be recursive, compileValue will handle that - protected function extractInterpolation($list) { - $items = $list[2]; - foreach ($items as $i => $item) { - if ($item[0] == "interpolate") { - $before = array("list", $list[1], array_slice($items, 0, $i)); - $after = array("list", $list[1], array_slice($items, $i + 1)); - return array("interpolated", $item, $before, $after); - } - } - return $list; - } - - // find the final set of selectors - protected function multiplySelectors($env) { - $envs = array(); - while (null !== $env) { - if (!empty($env->selectors)) { - $envs[] = $env; - } - $env = $env->parent; - }; - - $selectors = array(); - $parentSelectors = array(array()); - while ($env = array_pop($envs)) { - $selectors = array(); - foreach ($env->selectors as $selector) { - foreach ($parentSelectors as $parent) { - $selectors[] = $this->joinSelectors($parent, $selector); - } - } - $parentSelectors = $selectors; - } - - return $selectors; - } - - // looks for & to replace, or append parent before child - 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); - } - - protected function multiplyMedia($env, $childQueries = null) { - if (is_null($env) || - !empty($env->block->type) && $env->block->type != "media") - { - return $childQueries; - } - - // plain old block, skip - if (empty($env->block->type)) { - return $this->multiplyMedia($env->parent, $childQueries); - } - - $parentQueries = $env->block->queryList; - 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 something to list - protected function coerceList($item, $delim = ",") { - if (!is_null($item) && $item[0] == "list") { - return $item; - } - - return array("list", $delim, is_null($item) ? array(): array($item)); - } - - protected function applyArguments($argDef, $argValues) { - $args = array(); - foreach ($argDef as $i => $arg) { - list($name, $default, $isVariable) = $argDef[$i]; - $args[$name] = array($i, $name, $default, $isVariable); - } - - $keywordArgs = array(); - $remaining = array(); - // assign the keyword args - foreach ((array) $argValues as $arg) { - if (!empty($arg[0])) { - if (!isset($args[$arg[0][1]])) { - throw new Exception(sprintf('Mixin or function doesn\'t have an argument named $%s.', $arg[0][1])); - } elseif ($args[$arg[0][1]][0] < count($remaining)) { - throw new Exception(sprintf('The argument $%s was passed both by position and by name.', $arg[0][1])); - } - $keywordArgs[$arg[0][1]] = $arg[1]; - } elseif (count($keywordArgs)) { - throw new Exception('Positional arguments must come before keyword arguments.'); - } elseif ($arg[2] == true) { - $val = $this->reduce($arg[1], true); - if ($val[0] == "list") { - foreach ($val[2] as $item) { - $remaining[] = $item; - } - } else { - $remaining[] = $val; - } - } else { - $remaining[] = $arg[1]; - } - } - - foreach ($args as $arg) { - list($i, $name, $default, $isVariable) = $arg; - if ($isVariable) { - $val = array("list", ",", array()); - for ($count = count($remaining); $i < $count; $i++) { - $val[2][] = $remaining[$i]; - } - } elseif (isset($remaining[$i])) { - $val = $remaining[$i]; - } elseif (isset($keywordArgs[$name])) { - $val = $keywordArgs[$name]; - } elseif (!empty($default)) { - $val = $default; - } else { - throw new Exception(sprintf('There is missing argument $%s', $name)); - } - - $this->set($name, $this->reduce($val, true), true); - } - } - - protected function pushEnv($block=null) { - $env = new stdClass; - $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; - } - - protected function normalizeName($name) { - return str_replace("-", "_", $name); - } - - protected function getStoreEnv() { - return isset($this->storeEnv) ? $this->storeEnv : $this->env; - } - - protected function set($name, $value, $shadow=false) { - $name = $this->normalizeName($name); - if ($shadow) { - $this->setRaw($name, $value); - } else { - $this->setExisting($name, $value); - } - } - - // todo: this is bugged? - protected function setExisting($name, $value, $env = null) { - if (is_null($env)) $env = $this->getStoreEnv(); - - if (isset($env->store[$name])) { - $env->store[$name] = $value; - } elseif (!is_null($env->parent)) { - $this->setExisting($name, $value, $env->parent); - } else { - $this->env->store[$name] = $value; - } - } - - protected function setRaw($name, $value) { - $this->env->store[$name] = $value; - } - - protected function get($name, $defaultValue = null, $env = null) { - $name = $this->normalizeName($name); - - if (is_null($env)) $env = $this->getStoreEnv(); - if (is_null($defaultValue)) $defaultValue = self::$defaultValue; - - if (isset($env->store[$name])) { - return $env->store[$name]; - } elseif (!is_null($env->parent)) { - return $this->get($name, $defaultValue, $env->parent); - } - - return $defaultValue; // found nothing - } - - protected function popEnv() { - $env = $this->env; - $this->env = $this->env->parent; - return $env; - } - - public function getParsedFiles() { - return $this->parsedFiles; - } - - public function addImportPath($path) { - $this->importPaths[] = $path; - } - - public function setImportPaths($path) { - $this->importPaths = (array)$path; - } - - public function setFormatter($formatterName) { - $this->formatter = $formatterName; - } - - public function registerFunction($name, $func) { - $this->userFunctions[$this->normalizeName($name)] = $func; - } - - public function unregisterFunction($name) { - unset($this->userFunctions[$this->normalizeName($name)]); - } - - protected function importFile($path, $out) { - // see if tree is cached - $realPath = realpath($path); - if (isset($this->importCache[$realPath])) { - $tree = $this->importCache[$realPath]; - } else { - $code = file_get_contents($path); - $parser = new scss_parser($path); - $tree = $parser->parse($code); - $this->parsedFiles[] = $path; - - $this->importCache[$realPath] = $tree; - } - - $pi = pathinfo($path); - array_unshift($this->importPaths, $pi['dirname']); - $this->compileChildren($tree->children, $out); - array_shift($this->importPaths); - } - - // results the file path for an import url if it exists - protected function findImport($url) { - $urls = array(); - - // for "normal" scss imports (ignore vanilla css and external requests) - if (!preg_match('/\.css|^http:\/\/$/', $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; - } - } - } else { - // check custom callback for import path - $file = call_user_func($dir,$url,$this); - if ($file !== null) { - return $file; - } - } - } - - return null; - } - - protected function fileExists($name) { - return is_file($name); - } - - protected function callBuiltin($name, $args, &$returnValue) { - // try a lib function - $name = $this->normalizeName($name); - $libName = "lib_".$name; - $f = array($this, $libName); - $prototype = isset(self::$$libName) ? self::$$libName : null; - - if (is_callable($f)) { - $sorted = $this->sortArgs($prototype, $args); - foreach ($sorted as &$val) { - $val = $this->reduce($val, true); - } - $returnValue = call_user_func($f, $sorted, $this); - } else if (isset($this->userFunctions[$name])) { - // see if we can find a user function - $fn = $this->userFunctions[$name]; - - foreach ($args as &$val) { - $val = $this->reduce($val[1], true); - } - - $returnValue = call_user_func($fn, $args, $this); - } - - if (isset($returnValue)) { - // coerce a php value into a scss one - if (is_numeric($returnValue)) { - $returnValue = array('number', $returnValue, ""); - } elseif (is_bool($returnValue)) { - $returnValue = $returnValue ? self::$true : self::$false; - } elseif (!is_array($returnValue)) { - $returnValue = array('keyword', $returnValue); - } - - return true; - } - - return false; - } - - // sorts any keyword arguments - // TODO: merge with apply arguments - protected function sortArgs($prototype, $args) { - $keyArgs = array(); - $posArgs = array(); - - foreach ($args as $arg) { - list($key, $value) = $arg; - $key = $key[1]; - if (empty($key)) { - $posArgs[] = $value; - } else { - $keyArgs[$key] = $value; - } - } - - if (is_null($prototype)) return $posArgs; - - $finalArgs = array(); - foreach ($prototype as $i => $names) { - if (isset($posArgs[$i])) { - $finalArgs[] = $posArgs[$i]; - continue; - } - - $set = false; - foreach ((array)$names as $name) { - if (isset($keyArgs[$name])) { - $finalArgs[] = $keyArgs[$name]; - $set = true; - break; - } - } - - if (!$set) { - $finalArgs[] = null; - } - } - - return $finalArgs; - } - - protected function coerceForExpression($value) { - if ($color = $this->coerceColor($value)) { - return $color; - } - - return $value; - } - - protected function coerceColor($value) { - switch ($value[0]) { - case "color": return $value; - case "keyword": - $name = $value[1]; - if (isset(self::$cssColors[$name])) { - list($r, $g, $b) = explode(',', self::$cssColors[$name]); - return array('color', (int) $r, (int) $g, (int) $b); - } - return null; - } - - return null; - } - - protected function coerceString($value) { - switch ($value[0]) { - case "string": - return $value; - case "keyword": - return array("string", "", array($value[1])); - } - return null; - } - - protected function assertList($value) { - if ($value[0] != "list") - throw new exception("expecting list"); - return $value; - } - - protected function assertColor($value) { - if ($color = $this->coerceColor($value)) return $color; - throw new Exception("expecting color"); - } - - protected function assertNumber($value) { - if ($value[0] != "number") - throw new Exception("expecting number"); - return $value[1]; - } - - protected function coercePercent($value) { - if ($value[0] == "number") { - if ($value[2] == "%") { - return $value[1] / 100; - } - return $value[1]; - } - return 0; - } - - // make sure a color's components don't go out of bounds - protected function fixColor($c) { - foreach (range(1, 3) as $i) { - if ($c[$i] < 0) $c[$i] = 0; - if ($c[$i] > 255) $c[$i] = 255; - } - - return $c; - } - - function toHSL($r, $g, $b) { - $r = $r / 255; - $g = $g / 255; - $b = $b / 255; - - $min = min($r, $g, $b); - $max = max($r, $g, $b); - - $L = ($min + $max) / 2; - if ($min == $max) { - $S = $H = 0; - } else { - if ($L < 0.5) - $S = ($max - $min)/($max + $min); - else - $S = ($max - $min)/(2.0 - $max - $min); - - if ($r == $max) $H = ($g - $b)/($max - $min); - elseif ($g == $max) $H = 2.0 + ($b - $r)/($max - $min); - elseif ($b == $max) $H = 4.0 + ($r - $g)/($max - $min); - - } - - return array('hsl', - ($H < 0 ? $H + 6 : $H)*60, - $S*100, - $L*100, - ); - } - - function toRGB_helper($comp, $temp1, $temp2) { - if ($comp < 0) $comp += 1.0; - elseif ($comp > 1) $comp -= 1.0; - - if (6 * $comp < 1) return $temp1 + ($temp2 - $temp1) * 6 * $comp; - if (2 * $comp < 1) return $temp2; - if (3 * $comp < 2) return $temp1 + ($temp2 - $temp1)*((2/3) - $comp) * 6; - - return $temp1; - } - - // H from 0 to 360, S and L from 0 to 100 - function toRGB($H, $S, $L) { - $H = $H % 360; - if ($H < 0) $H += 360; - - $S = min(100, max(0, $S)); - $L = min(100, max(0, $L)); - - $H = $H / 360; - $S = $S / 100; - $L = $L / 100; - - if ($S == 0) { - $r = $g = $b = $L; - } else { - $temp2 = $L < 0.5 ? - $L*(1.0 + $S) : - $L + $S - $L * $S; - - $temp1 = 2.0 * $L - $temp2; - - $r = $this->toRGB_helper($H + 1/3, $temp1, $temp2); - $g = $this->toRGB_helper($H, $temp1, $temp2); - $b = $this->toRGB_helper($H - 1/3, $temp1, $temp2); - } - - $out = array('color', $r*255, $g*255, $b*255); - return $out; - } - - // Built in functions - - protected static $lib_if = array("condition", "if-true", "if-false"); - protected function lib_if($args) { - list($cond,$t, $f) = $args; - if ($cond == self::$false) return $f; - return $t; - } - - protected static $lib_index = array("list", "value"); - protected function lib_index($args) { - list($list, $value) = $args; - $list = $this->assertList($list); - - $values = array(); - foreach ($list[2] as $item) { - $values[] = $this->normalizeValue($item); - } - $key = array_search($this->normalizeValue($value), $values); - - return false === $key ? false : $key + 1; - } - - protected static $lib_rgb = array("red", "green", "blue"); - protected function lib_rgb($args) { - list($r,$g,$b) = $args; - return array("color", $r[1], $g[1], $b[1]); - } - - protected static $lib_rgba = array( - array("red", "color"), - "green", "blue", "alpha"); - protected function lib_rgba($args) { - if ($color = $this->coerceColor($args[0])) { - $num = is_null($args[1]) ? $args[3] : $args[1]; - $alpha = $this->assertNumber($num); - $color[4] = $alpha; - return $color; - } - - list($r,$g,$b, $a) = $args; - return array("color", $r[1], $g[1], $b[1], $a[1]); - } - - // helper function for adjust_color, change_color, and scale_color - protected function alter_color($args, $fn) { - $color = $this->assertColor($args[0]); - - foreach (array(1,2,3,7) as $i) { - if (!is_null($args[$i])) { - $val = $this->assertNumber($args[$i]); - $ii = $i == 7 ? 4 : $i; // alpha - $color[$ii] = - $this->$fn(isset($color[$ii]) ? $color[$ii] : 0, $val, $i); - } - } - - if (!is_null($args[4]) || !is_null($args[5]) || !is_null($args[6])) { - $hsl = $this->toHSL($color[1], $color[2], $color[3]); - foreach (array(4,5,6) as $i) { - if (!is_null($args[$i])) { - $val = $this->assertNumber($args[$i]); - $hsl[$i - 3] = $this->$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 $lib_adjust_color = array( - "color", "red", "green", "blue", - "hue", "saturation", "lightness", "alpha" - ); - protected function adjust_color_helper($base, $alter, $i) { - return $base += $alter; - } - protected function lib_adjust_color($args) { - return $this->alter_color($args, "adjust_color_helper"); - } - - protected static $lib_change_color = array( - "color", "red", "green", "blue", - "hue", "saturation", "lightness", "alpha" - ); - protected function change_color_helper($base, $alter, $i) { - return $alter; - } - protected function lib_change_color($args) { - return $this->alter_color($args, "change_color_helper"); - } - - protected static $lib_scale_color = array( - "color", "red", "green", "blue", - "hue", "saturation", "lightness", "alpha" - ); - protected function scale_color_helper($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; - } else { - return ($max - $base) * $scale + $base; - } - } - protected function lib_scale_color($args) { - return $this->alter_color($args, "scale_color_helper"); - } - - protected static $lib_ie_hex_str = array("color"); - protected function lib_ie_hex_str($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 $lib_red = array("color"); - protected function lib_red($args) { - list($color) = $args; - return $color[1]; - } - - protected static $lib_green = array("color"); - protected function lib_green($args) { - list($color) = $args; - return $color[2]; - } - - protected static $lib_blue = array("color"); - protected function lib_blue($args) { - list($color) = $args; - return $color[3]; - } - - protected static $lib_alpha = array("color"); - protected function lib_alpha($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 array("function", "alpha", array("list", ",", $args)); - } - - protected static $lib_opacity = array("color"); - protected function lib_opacity($args) { - return $this->lib_alpha($args); - } - - // mix two colors - protected static $lib_mix = array("color-1", "color-2", "weight"); - protected function lib_mix($args) { - list($first, $second, $weight) = $args; - $first = $this->assertColor($first); - $second = $this->assertColor($second); - - if (is_null($weight)) { - $weight = 0.5; - } else { - $weight = $this->coercePercent($weight); - } - - $first_a = isset($first[4]) ? $first[4] : 1; - $second_a = isset($second[4]) ? $second[4] : 1; - - $w = $weight * 2 - 1; - $a = $first_a - $second_a; - - $w1 = (($w * $a == -1 ? $w : ($w + $a)/(1 + $w * $a)) + 1) / 2.0; - $w2 = 1.0 - $w1; - - $new = array('color', - $w1 * $first[1] + $w2 * $second[1], - $w1 * $first[2] + $w2 * $second[2], - $w1 * $first[3] + $w2 * $second[3], - ); - - if ($first_a != 1.0 || $second_a != 1.0) { - $new[] = $first_a * $weight + $second_a * ($weight - 1); - } - - return $this->fixColor($new); - } - - protected static $lib_hsl = array("hue", "saturation", "lightness"); - protected function lib_hsl($args) { - list($h, $s, $l) = $args; - return $this->toRGB($h[1], $s[1], $l[1]); - } - - protected static $lib_hsla = array("hue", "saturation", - "lightness", "alpha"); - protected function lib_hsla($args) { - list($h, $s, $l, $a) = $args; - $color = $this->toRGB($h[1], $s[1], $l[1]); - $color[4] = $a[1]; - return $color; - } - - protected static $lib_hue = array("color"); - protected function lib_hue($args) { - $color = $this->assertColor($args[0]); - $hsl = $this->toHSL($color[1], $color[2], $color[3]); - return array("number", $hsl[1], "deg"); - } - - protected static $lib_saturation = array("color"); - protected function lib_saturation($args) { - $color = $this->assertColor($args[0]); - $hsl = $this->toHSL($color[1], $color[2], $color[3]); - return array("number", $hsl[2], "%"); - } - - protected static $lib_lightness = array("color"); - protected function lib_lightness($args) { - $color = $this->assertColor($args[0]); - $hsl = $this->toHSL($color[1], $color[2], $color[3]); - return array("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 $lib_adjust_hue = array("color", "degrees"); - protected function lib_adjust_hue($args) { - $color = $this->assertColor($args[0]); - $degrees = $this->assertNumber($args[1]); - return $this->adjustHsl($color, 1, $degrees); - } - - protected static $lib_lighten = array("color", "amount"); - protected function lib_lighten($args) { - $color = $this->assertColor($args[0]); - $amount = 100*$this->coercePercent($args[1]); - return $this->adjustHsl($color, 3, $amount); - } - - protected static $lib_darken = array("color", "amount"); - protected function lib_darken($args) { - $color = $this->assertColor($args[0]); - $amount = 100*$this->coercePercent($args[1]); - return $this->adjustHsl($color, 3, -$amount); - } - - protected static $lib_saturate = array("color", "amount"); - protected function lib_saturate($args) { - $color = $this->assertColor($args[0]); - $amount = 100*$this->coercePercent($args[1]); - return $this->adjustHsl($color, 2, $amount); - } - - protected static $lib_desaturate = array("color", "amount"); - protected function lib_desaturate($args) { - $color = $this->assertColor($args[0]); - $amount = 100*$this->coercePercent($args[1]); - return $this->adjustHsl($color, 2, -$amount); - } - - protected static $lib_grayscale = array("color"); - protected function lib_grayscale($args) { - return $this->adjustHsl($this->assertColor($args[0]), 2, -100); - } - - protected static $lib_complement = array("color"); - protected function lib_complement($args) { - return $this->adjustHsl($this->assertColor($args[0]), 1, 180); - } - - protected static $lib_invert = array("color"); - protected function lib_invert($args) { - $color = $this->assertColor($args[0]); - $color[1] = 255 - $color[1]; - $color[2] = 255 - $color[2]; - $color[3] = 255 - $color[3]; - return $color; - } - - - // increases opacity by amount - protected static $lib_opacify = array("color", "amount"); - protected function lib_opacify($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 $lib_fade_in = array("color", "amount"); - protected function lib_fade_in($args) { - return $this->lib_opacify($args); - } - - // decreases opacity by amount - protected static $lib_transparentize = array("color", "amount"); - protected function lib_transparentize($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 $lib_fade_out = array("color", "amount"); - protected function lib_fade_out($args) { - return $this->lib_transparentize($args); - } - - protected static $lib_unquote = array("string"); - protected function lib_unquote($args) { - $str = $args[0]; - if ($str[0] == "string") $str[1] = ""; - return $str; - } - - protected static $lib_quote = array("string"); - protected function lib_quote($args) { - $value = $args[0]; - if ($value[0] == "string" && !empty($value[1])) - return $value; - return array("string", '"', array($value)); - } - - protected static $lib_percentage = array("value"); - protected function lib_percentage($args) { - return array("number", - $this->coercePercent($args[0]) * 100, - "%"); - } - - protected static $lib_round = array("value"); - protected function lib_round($args) { - $num = $args[0]; - $num[1] = round($num[1]); - return $num; - } - - protected static $lib_floor = array("value"); - protected function lib_floor($args) { - $num = $args[0]; - $num[1] = floor($num[1]); - return $num; - } - - protected static $lib_ceil = array("value"); - protected function lib_ceil($args) { - $num = $args[0]; - $num[1] = ceil($num[1]); - return $num; - } - - protected static $lib_abs = array("value"); - protected function lib_abs($args) { - $num = $args[0]; - $num[1] = abs($num[1]); - return $num; - } - - protected function lib_min($args) { - $numbers = $this->getNormalizedNumbers($args); - $min = null; - foreach ($numbers as $key => $number) { - if (null === $min || $number <= $min[1]) { - $min = array($key, $number); - } - } - - return $args[$min[0]]; - } - - protected function lib_max($args) { - $numbers = $this->getNormalizedNumbers($args); - $max = null; - foreach ($numbers as $key => $number) { - if (null === $max || $number >= $max[1]) { - $max = array($key, $number); - } - } - - return $args[$max[0]]; - } - - protected function getNormalizedNumbers($args) { - $unit = null; - $originalUnit = null; - $numbers = array(); - foreach ($args as $key => $item) { - if ('number' != $item[0]) { - throw new Exception(sprintf('%s is not a number', $item[0])); - } - $number = $this->normalizeNumber($item); - - if (null === $unit) { - $unit = $number[2]; - } elseif ($unit !== $number[2]) { - throw new Exception(sprintf('Incompatible units: "%s" and "%s".', $originalUnit, $item[2])); - } - - $originalUnit = $item[2]; - $numbers[$key] = $number[1]; - } - - return $numbers; - } - - protected static $lib_length = array("list"); - protected function lib_length($args) { - $list = $this->coerceList($args[0]); - return count($list[2]); - } - - protected static $lib_nth = array("list", "n"); - protected function lib_nth($args) { - $list = $this->coerceList($args[0]); - $n = $this->assertNumber($args[1]) - 1; - return isset($list[2][$n]) ? $list[2][$n] : self::$defaultValue; - } - - - protected function listSeparatorForJoin($list1, $sep) { - if (is_null($sep)) return $list1[1]; - switch ($this->compileValue($sep)) { - case "comma": - return ","; - case "space": - return ""; - default: - return $list1[1]; - } - } - - protected static $lib_join = array("list1", "list2", "separator"); - protected function lib_join($args) { - list($list1, $list2, $sep) = $args; - $list1 = $this->coerceList($list1, " "); - $list2 = $this->coerceList($list2, " "); - $sep = $this->listSeparatorForJoin($list1, $sep); - return array("list", $sep, array_merge($list1[2], $list2[2])); - } - - protected static $lib_append = array("list", "val", "separator"); - protected function lib_append($args) { - list($list1, $value, $sep) = $args; - $list1 = $this->coerceList($list1, " "); - $sep = $this->listSeparatorForJoin($list1, $sep); - return array("list", $sep, array_merge($list1[2], array($value))); - } - - protected function lib_zip($args) { - foreach ($args as $arg) { - $this->assertList($arg); - } - - $lists = array(); - $firstList = array_shift($args); - foreach ($firstList[2] as $key => $item) { - $list = array("list", "", array($item)); - foreach ($args as $arg) { - if (isset($arg[2][$key])) { - $list[2][] = $arg[2][$key]; - } else { - break 2; - } - } - $lists[] = $list; - } - - return array("list", ",", $lists); - } - - protected static $lib_type_of = array("value"); - protected function lib_type_of($args) { - $value = $args[0]; - switch ($value[0]) { - case "keyword": - if ($value == self::$true || $value == self::$false) { - return "bool"; - } - - if ($this->coerceColor($value)) { - return "color"; - } - - return "string"; - default: - return $value[0]; - } - } - - protected static $lib_unit = array("number"); - protected function lib_unit($args) { - $num = $args[0]; - if ($num[0] == "number") { - return array("string", '"', array($num[2])); - } - return ""; - } - - protected static $lib_unitless = array("number"); - protected function lib_unitless($args) { - $value = $args[0]; - return $value[0] == "number" && empty($value[2]); - } - - protected static $lib_comparable = array("number-1", "number-2"); - protected function lib_comparable($args) { - list($number1, $number2) = $args; - if (!isset($number1[0]) || $number1[0] != "number" || !isset($number2[0]) || $number2[0] != "number") { - throw new Exception('Invalid argument(s) for "comparable"'); - } - - $number1 = $this->normalizeNumber($number1); - $number2 = $this->normalizeNumber($number2); - - return $number1[2] == $number2[2] || $number1[2] == "" || $number2[2] == ""; - } - - static protected $cssColors = array( - 'aliceblue' => '240,248,255', - 'antiquewhite' => '250,235,215', - 'aqua' => '0,255,255', - 'aquamarine' => '127,255,212', - 'azure' => '240,255,255', - 'beige' => '245,245,220', - 'bisque' => '255,228,196', - 'black' => '0,0,0', - 'blanchedalmond' => '255,235,205', - 'blue' => '0,0,255', - 'blueviolet' => '138,43,226', - 'brown' => '165,42,42', - 'burlywood' => '222,184,135', - 'cadetblue' => '95,158,160', - 'chartreuse' => '127,255,0', - 'chocolate' => '210,105,30', - 'coral' => '255,127,80', - 'cornflowerblue' => '100,149,237', - 'cornsilk' => '255,248,220', - 'crimson' => '220,20,60', - 'cyan' => '0,255,255', - 'darkblue' => '0,0,139', - 'darkcyan' => '0,139,139', - 'darkgoldenrod' => '184,134,11', - 'darkgray' => '169,169,169', - 'darkgreen' => '0,100,0', - 'darkgrey' => '169,169,169', - 'darkkhaki' => '189,183,107', - 'darkmagenta' => '139,0,139', - 'darkolivegreen' => '85,107,47', - 'darkorange' => '255,140,0', - 'darkorchid' => '153,50,204', - 'darkred' => '139,0,0', - 'darksalmon' => '233,150,122', - 'darkseagreen' => '143,188,143', - 'darkslateblue' => '72,61,139', - 'darkslategray' => '47,79,79', - 'darkslategrey' => '47,79,79', - 'darkturquoise' => '0,206,209', - 'darkviolet' => '148,0,211', - 'deeppink' => '255,20,147', - 'deepskyblue' => '0,191,255', - 'dimgray' => '105,105,105', - 'dimgrey' => '105,105,105', - 'dodgerblue' => '30,144,255', - 'firebrick' => '178,34,34', - 'floralwhite' => '255,250,240', - 'forestgreen' => '34,139,34', - 'fuchsia' => '255,0,255', - 'gainsboro' => '220,220,220', - 'ghostwhite' => '248,248,255', - 'gold' => '255,215,0', - 'goldenrod' => '218,165,32', - 'gray' => '128,128,128', - 'green' => '0,128,0', - 'greenyellow' => '173,255,47', - 'grey' => '128,128,128', - 'honeydew' => '240,255,240', - 'hotpink' => '255,105,180', - 'indianred' => '205,92,92', - 'indigo' => '75,0,130', - 'ivory' => '255,255,240', - 'khaki' => '240,230,140', - 'lavender' => '230,230,250', - 'lavenderblush' => '255,240,245', - 'lawngreen' => '124,252,0', - 'lemonchiffon' => '255,250,205', - 'lightblue' => '173,216,230', - 'lightcoral' => '240,128,128', - 'lightcyan' => '224,255,255', - 'lightgoldenrodyellow' => '250,250,210', - 'lightgray' => '211,211,211', - 'lightgreen' => '144,238,144', - 'lightgrey' => '211,211,211', - 'lightpink' => '255,182,193', - 'lightsalmon' => '255,160,122', - 'lightseagreen' => '32,178,170', - 'lightskyblue' => '135,206,250', - 'lightslategray' => '119,136,153', - 'lightslategrey' => '119,136,153', - 'lightsteelblue' => '176,196,222', - 'lightyellow' => '255,255,224', - 'lime' => '0,255,0', - 'limegreen' => '50,205,50', - 'linen' => '250,240,230', - 'magenta' => '255,0,255', - 'maroon' => '128,0,0', - 'mediumaquamarine' => '102,205,170', - 'mediumblue' => '0,0,205', - 'mediumorchid' => '186,85,211', - 'mediumpurple' => '147,112,219', - 'mediumseagreen' => '60,179,113', - 'mediumslateblue' => '123,104,238', - 'mediumspringgreen' => '0,250,154', - 'mediumturquoise' => '72,209,204', - 'mediumvioletred' => '199,21,133', - 'midnightblue' => '25,25,112', - 'mintcream' => '245,255,250', - 'mistyrose' => '255,228,225', - 'moccasin' => '255,228,181', - 'navajowhite' => '255,222,173', - 'navy' => '0,0,128', - 'oldlace' => '253,245,230', - 'olive' => '128,128,0', - 'olivedrab' => '107,142,35', - 'orange' => '255,165,0', - 'orangered' => '255,69,0', - 'orchid' => '218,112,214', - 'palegoldenrod' => '238,232,170', - 'palegreen' => '152,251,152', - 'paleturquoise' => '175,238,238', - 'palevioletred' => '219,112,147', - 'papayawhip' => '255,239,213', - 'peachpuff' => '255,218,185', - 'peru' => '205,133,63', - 'pink' => '255,192,203', - 'plum' => '221,160,221', - 'powderblue' => '176,224,230', - 'purple' => '128,0,128', - 'red' => '255,0,0', - 'rosybrown' => '188,143,143', - 'royalblue' => '65,105,225', - 'saddlebrown' => '139,69,19', - 'salmon' => '250,128,114', - 'sandybrown' => '244,164,96', - 'seagreen' => '46,139,87', - 'seashell' => '255,245,238', - 'sienna' => '160,82,45', - 'silver' => '192,192,192', - 'skyblue' => '135,206,235', - 'slateblue' => '106,90,205', - 'slategray' => '112,128,144', - 'slategrey' => '112,128,144', - 'snow' => '255,250,250', - 'springgreen' => '0,255,127', - 'steelblue' => '70,130,180', - 'tan' => '210,180,140', - 'teal' => '0,128,128', - 'thistle' => '216,191,216', - 'tomato' => '255,99,71', - 'turquoise' => '64,224,208', - 'violet' => '238,130,238', - 'wheat' => '245,222,179', - 'white' => '255,255,255', - 'whitesmoke' => '245,245,245', - 'yellow' => '255,255,0', - 'yellowgreen' => '154,205,50' - ); -} - -class scss_parser { - static protected $precedence = array( - "or" => 0, - "and" => 1, - - '==' => 2, - '!=' => 2, - '<=' => 2, - '>=' => 2, - '=' => 2, - '<' => 3, - '>' => 2, - - '+' => 3, - '-' => 3, - '*' => 4, - '/' => 4, - '%' => 4, - ); - - static protected $operators = array("+", "-", "*", "/", "%", - "==", "!=", "<=", ">=", "<", ">", "and", "or"); - - static protected $operatorStr; - static protected $whitePattern; - static protected $commentMulti; - - static protected $commentSingle = "//"; - static protected $commentMultiLeft = "/*"; - static protected $commentMultiRight = "*/"; - - function __construct($sourceName = null) { - $this->sourceName = $sourceName; - - if (empty(self::$operatorStr)) { - self::$operatorStr = $this->makeOperatorStr(self::$operators); - - $commentSingle = $this->preg_quote(self::$commentSingle); - $commentMultiLeft = $this->preg_quote(self::$commentMultiLeft); - $commentMultiRight = $this->preg_quote(self::$commentMultiRight); - self::$commentMulti = $commentMultiLeft.'.*?'.$commentMultiRight; - self::$whitePattern = '/'.$commentSingle.'[^\n]*\s*|('.self::$commentMulti.')\s*|\s+/Ais'; - } - } - - static protected function makeOperatorStr($operators) { - return '('.implode('|', array_map(array('scss_parser','preg_quote'), - $operators)).')'; - } - - function parse($buffer) { - $this->count = 0; - $this->env = null; - $this->inParens = false; - $this->pushBlock(null); // root block - $this->eatWhiteDefault = true; - $this->insertComments = true; - - $this->buffer = $buffer; - - $this->whitespace(); - while (false !== $this->parseChunk()); - - if ($this->count != strlen($this->buffer)) - $this->throwParseError(); - - if (!empty($this->env->parent)) { - $this->throwParseError("unclosed block"); - } - - $this->env->isRoot = true; - return $this->env; - } - - protected function parseChunk() { - $s = $this->seek(); - - // the directives - if (isset($this->buffer[$this->count]) && $this->buffer[$this->count] == "@") { - if ($this->literal("@media") && $this->mediaQueryList($mediaQueryList) && $this->literal("{")) { - $media = $this->pushSpecialBlock("media"); - $media->queryList = $mediaQueryList[2]; - return true; - } else { - $this->seek($s); - } - - if ($this->literal("@mixin") && - $this->keyword($mixinName) && - ($this->argumentDef($args) || true) && - $this->literal("{")) - { - $mixin = $this->pushSpecialBlock("mixin"); - $mixin->name = $mixinName; - $mixin->args = $args; - return true; - } else { - $this->seek($s); - } - - if ($this->literal("@include") && - $this->keyword($mixinName) && - ($this->literal("(") && - ($this->argValues($argValues) || true) && - $this->literal(")") || true) && - ($this->end() || - $this->literal("{") && $hasBlock = true)) - { - $child = array("include", - $mixinName, isset($argValues) ? $argValues : null, null); - - if (!empty($hasBlock)) { - $include = $this->pushSpecialBlock("include"); - $include->child = $child; - } else { - $this->append($child); - } - - return true; - } else { - $this->seek($s); - } - - if ($this->literal("@import") && - $this->valueList($importPath) && - $this->end()) - { - $this->append(array("import", $importPath)); - return true; - } else { - $this->seek($s); - } - - if ($this->literal("@extend") && - $this->selectors($selector) && - $this->end()) - { - $this->append(array("extend", $selector)); - return true; - } else { - $this->seek($s); - } - - if ($this->literal("@function") && - $this->keyword($fn_name) && - $this->argumentDef($args) && - $this->literal("{")) - { - $func = $this->pushSpecialBlock("function"); - $func->name = $fn_name; - $func->args = $args; - return true; - } else { - $this->seek($s); - } - - if ($this->literal("@return") && $this->valueList($retVal) && $this->end()) { - $this->append(array("return", $retVal)); - return true; - } else { - $this->seek($s); - } - - if ($this->literal("@each") && - $this->variable($varName) && - $this->literal("in") && - $this->valueList($list) && - $this->literal("{")) - { - $each = $this->pushSpecialBlock("each"); - $each->var = $varName[1]; - $each->list = $list; - return true; - } else { - $this->seek($s); - } - - if ($this->literal("@while") && - $this->expression($cond) && - $this->literal("{")) - { - $while = $this->pushSpecialBlock("while"); - $while->cond = $cond; - return true; - } else { - $this->seek($s); - } - - if ($this->literal("@for") && - $this->variable($varName) && - $this->literal("from") && - $this->expression($start) && - ($this->literal("through") || - ($forUntil = true && $this->literal("to"))) && - $this->expression($end) && - $this->literal("{")) - { - $for = $this->pushSpecialBlock("for"); - $for->var = $varName[1]; - $for->start = $start; - $for->end = $end; - $for->until = isset($forUntil); - return true; - } else { - $this->seek($s); - } - - if ($this->literal("@if") && $this->valueList($cond) && $this->literal("{")) { - $if = $this->pushSpecialBlock("if"); - $if->cond = $cond; - $if->cases = array(); - return true; - } else { - $this->seek($s); - } - - if (($this->literal("@debug") || $this->literal("@warn")) && - $this->valueList($value) && - $this->end()) { - $this->append(array("debug", $value, $s)); - return true; - } else { - $this->seek($s); - } - - if ($this->literal("@content") && $this->end()) { - $this->append(array("mixin_content")); - return true; - } else { - $this->seek($s); - } - - $last = $this->last(); - if (!is_null($last) && $last[0] == "if") { - list(, $if) = $last; - if ($this->literal("@else")) { - if ($this->literal("{")) { - $else = $this->pushSpecialBlock("else"); - } elseif ($this->literal("if") && $this->valueList($cond) && $this->literal("{")) { - $else = $this->pushSpecialBlock("elseif"); - $else->cond = $cond; - } - - if (isset($else)) { - $else->dontAppend = true; - $if->cases[] = $else; - return true; - } - } - - $this->seek($s); - } - - if ($this->literal("@charset") && - $this->valueList($charset) && $this->end()) - { - $this->append(array("charset", $charset)); - return true; - } else { - $this->seek($s); - } - - // doesn't match built in directive, do generic one - if ($this->literal("@", false) && $this->keyword($dirName) && - ($this->openString("{", $dirValue) || true) && - $this->literal("{")) - { - $directive = $this->pushSpecialBlock("directive"); - $directive->name = $dirName; - if (isset($dirValue)) $directive->value = $dirValue; - return true; - } - - $this->seek($s); - return false; - } - - // property shortcut - // captures most properties before having to parse a selector - if ($this->keyword($name, false) && - $this->literal(": ") && - $this->valueList($value) && - $this->end()) - { - $name = array("string", "", array($name)); - $this->append(array("assign", $name, $value)); - return true; - } else { - $this->seek($s); - } - - // variable assigns - if ($this->variable($name) && - $this->literal(":") && - $this->valueList($value) && $this->end()) - { - $defaultVar = false; - // check for !default - if ($value[0] == "list") { - $def = end($value[2]); - if ($def[0] == "keyword" && $def[1] == "!default") { - array_pop($value[2]); - $value = $this->flattenList($value); - $defaultVar = true; - } - } - $this->append(array("assign", $name, $value, $defaultVar)); - return true; - } else { - $this->seek($s); - } - - // misc - if ($this->literal("-->")) { - return true; - } - - // opening css block - $oldComments = $this->insertComments; - $this->insertComments = false; - if ($this->selectors($selectors) && $this->literal("{")) { - $this->pushBlock($selectors); - $this->insertComments = $oldComments; - return true; - } else { - $this->seek($s); - } - $this->insertComments = $oldComments; - - // property assign, or nested assign - if ($this->propertyName($name) && $this->literal(":")) { - $foundSomething = false; - if ($this->valueList($value)) { - $this->append(array("assign", $name, $value)); - $foundSomething = true; - } - - if ($this->literal("{")) { - $propBlock = $this->pushSpecialBlock("nestedprop"); - $propBlock->prefix = $name; - $foundSomething = true; - } elseif ($foundSomething) { - $foundSomething = $this->end(); - } - - if ($foundSomething) { - return true; - } - - $this->seek($s); - } else { - $this->seek($s); - } - - // closing a block - if ($this->literal("}")) { - $block = $this->popBlock(); - if (isset($block->type) && $block->type == "include") { - $include = $block->child; - unset($block->child); - $include[3] = $block; - $this->append($include); - } else if (empty($block->dontAppend)) { - $type = isset($block->type) ? $block->type : "block"; - $this->append(array($type, $block)); - } - return true; - } - - // extra stuff - if ($this->literal(";") || - $this->literal("', 3)) { + return true; + } + + // opening css block + if ($this->selectors($selectors) && $this->matchChar('{', false)) { + $this->pushBlock($selectors, $s); + + if ($this->eatWhiteDefault) { + $this->whitespace(); + $this->append(null); // collect comments at the begining if needed + } + + return true; + } + + $this->seek($s); + + // property assign, or nested assign + if ($this->propertyName($name) && $this->matchChar(':')) { + $foundSomething = false; + + if ($this->valueList($value)) { + if (empty($this->env->parent)) { + $this->throwParseError('expected "{"'); + } + + $this->append([Type::T_ASSIGN, $name, $value], $s); + $foundSomething = true; + } + + if ($this->matchChar('{')) { + $propBlock = $this->pushSpecialBlock(Type::T_NESTED_PROPERTY, $s); + $propBlock->prefix = $name; + $foundSomething = true; + } elseif ($foundSomething) { + $foundSomething = $this->end(); + } + + if ($foundSomething) { + return true; + } + } + + $this->seek($s); + + // closing a block + if ($this->matchChar('}')) { + $block = $this->popBlock(); + + if (isset($block->type) && $block->type === Type::T_INCLUDE) { + $include = $block->child; + unset($block->child); + $include[3] = $block; + $this->append($include, $s); + } elseif (empty($block->dontAppend)) { + $type = isset($block->type) ? $block->type : Type::T_BLOCK; + $this->append([$type, $block], $s); + } + + return true; + } + + // extra stuff + if ($this->matchChar(';') || + $this->literal('> new[:-1] . before . new[-1] . after - - new.len > 1 - before not empty - -