diff --git a/README.md b/README.md index 53a4c7a4..d3fbf154 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,17 @@ -# scssphp -### +# This repo has been archived -[![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. +#### Please go to https://github.com/scssphp/scssphp + +---- + +## 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. @@ -38,7 +41,7 @@ To enable the `scss` compatibility tests: TEST_SCSS_COMPAT=1 vendor/bin/phpunit tests -## Coding Standard +### Coding Standard `scssphp` source conforms to [PSR2](http://www.php-fig.org/psr/psr-2/). diff --git a/composer.json b/composer.json index 62153e7b..eaa8e87b 100644 --- a/composer.json +++ b/composer.json @@ -21,7 +21,7 @@ "psr-4": { "Leafo\\ScssPhp\\Test\\": "tests/" } }, "require": { - "php": "^5.6.0 || ^7" + "php": "^5.4.0 || ^7" }, "require-dev": { "squizlabs/php_codesniffer": "~2.5", @@ -39,5 +39,6 @@ "/phpunit.xml.dist", "/tests" ] - } + }, + "abandoned": "scssphp/scssphp" } diff --git a/scss.inc.php b/scss.inc.php index 8a3cfb83..2a5f0774 100644 --- a/scss.inc.php +++ b/scss.inc.php @@ -1,6 +1,6 @@ $last_modified) - and $cache_time + self::$gc_lifetime > time()) { + if ((is_null($lastModified) || $cacheTime > $lastModified) + && $cacheTime + self::$gcLifetime > time() + ) { $c = file_get_contents($fileCache); $c = unserialize($c); - if (is_array($c) and isset($c['value'])) { + + if (is_array($c) && isset($c['value'])) { return $c['value']; } } @@ -118,43 +118,45 @@ public function getCache($operation, $what, $options = array(), $last_modified = } /** - * Put in cache the result of $operation on $what, which is known as dependant from the content of $options + * Put in cache the result of $operation on $what, + * which is known as dependant from the content of $options * * @param string $operation - * @param $what - * @param $value - * @param array $options + * @param mixed $what + * @param mixed $value + * @param array $options */ - public function setCache($operation, $what, $value, $options = array()) + public function setCache($operation, $what, $value, $options = []) { - $fileCache = self::$cache_dir . self::cacheName($operation, $what, $options); + $fileCache = self::$cacheDir . self::cacheName($operation, $what, $options); - $c = array('value' => $value); + $c = ['value' => $value]; $c = serialize($c); file_put_contents($fileCache, $c); - if (self::$force_refresh === 'once') { + if (self::$forceRefresh === 'once') { self::$refreshed[$fileCache] = true; } } - /** - * get the cachename for the caching of $opetation on $what, which is known as dependant from the content of $options + * Get the cache name for the caching of $operation on $what, + * which is known as dependant from the content of $options + * * @param string $operation - * @param $what - * @param array $options + * @param mixed $what + * @param array $options + * * @return string */ - private static function cacheName($operation, $what, $options = array()) + private static function cacheName($operation, $what, $options = []) { - - $t = array( + $t = [ 'version' => self::CACHE_VERSION, 'operation' => $operation, 'what' => $what, 'options' => $options - ); + ]; $t = self::$prefix . sha1(json_encode($t)) @@ -164,38 +166,35 @@ private static function cacheName($operation, $what, $options = array()) return $t; } - /** - * Check that the cache dir is existing and writeable - * @throws Exception + * Check that the cache dir exists and is writeable + * + * @throws \Exception */ public static function checkCacheDir() { + self::$cacheDir = str_replace('\\', '/', self::$cacheDir); + self::$cacheDir = rtrim(self::$cacheDir, '/') . '/'; - self::$cache_dir = str_replace('\\', '/', self::$cache_dir); - self::$cache_dir = rtrim(self::$cache_dir, '/') . '/'; - - if (! file_exists(self::$cache_dir)) { - if (! mkdir(self::$cache_dir)) { - throw new Exception('Cache directory couldn\'t be created: ' . self::$cache_dir); + if (! file_exists(self::$cacheDir)) { + if (! mkdir(self::$cacheDir)) { + throw new Exception('Cache directory couldn\'t be created: ' . self::$cacheDir); } - } elseif (! is_dir(self::$cache_dir)) { - throw new Exception('Cache directory doesn\'t exist: ' . self::$cache_dir); - } elseif (! is_writable(self::$cache_dir)) { - throw new Exception('Cache directory isn\'t writable: ' . self::$cache_dir); + } elseif (! is_dir(self::$cacheDir)) { + throw new Exception('Cache directory doesn\'t exist: ' . self::$cacheDir); + } elseif (! is_writable(self::$cacheDir)) { + throw new Exception('Cache directory isn\'t writable: ' . self::$cacheDir); } } /** * Delete unused cached files - * */ public static function cleanCache() { static $clean = false; - - if ($clean || empty(self::$cache_dir)) { + if ($clean || empty(self::$cacheDir)) { return; } @@ -203,14 +202,16 @@ public static function cleanCache() // only remove files with extensions created by SCSSPHP Cache // css files removed based on the list files - $remove_types = array('scsscache' => 1); + $removeTypes = ['scsscache' => 1]; + + $files = scandir(self::$cacheDir); - $files = scandir(self::$cache_dir); if (! $files) { return; } - $check_time = time() - self::$gc_lifetime; + $checkTime = time() - self::$gcLifetime; + foreach ($files as $file) { // don't delete if the file wasn't created with SCSSPHP Cache if (strpos($file, self::$prefix) !== 0) { @@ -220,20 +221,19 @@ public static function cleanCache() $parts = explode('.', $file); $type = array_pop($parts); - - if (! isset($remove_types[$type])) { + if (! isset($removeTypes[$type])) { continue; } - $full_path = self::$cache_dir . $file; - $mtime = filemtime($full_path); + $fullPath = self::$cacheDir . $file; + $mtime = filemtime($fullPath); // don't delete if it's a relatively new file - if ($mtime > $check_time) { + if ($mtime > $checkTime) { continue; } - unlink($full_path); + unlink($fullPath); } } } diff --git a/src/Compiler.php b/src/Compiler.php index 50785aac..d22d0a35 100644 --- a/src/Compiler.php +++ b/src/Compiler.php @@ -99,17 +99,17 @@ class Compiler 'function' => '^', ]; - static public $true = [Type::T_KEYWORD, 'true']; - static public $false = [Type::T_KEYWORD, 'false']; - static public $null = [Type::T_NULL]; - static public $nullString = [Type::T_STRING, '', []]; + static public $true = [Type::T_KEYWORD, 'true']; + static public $false = [Type::T_KEYWORD, 'false']; + static public $null = [Type::T_NULL]; + static public $nullString = [Type::T_STRING, '', []]; static public $defaultValue = [Type::T_KEYWORD, '']; static public $selfSelector = [Type::T_SELF]; - static public $emptyList = [Type::T_LIST, '', []]; - static public $emptyMap = [Type::T_MAP, [], []]; - static public $emptyString = [Type::T_STRING, '"', []]; - static public $with = [Type::T_KEYWORD, 'with']; - static public $without = [Type::T_KEYWORD, 'without']; + static public $emptyList = [Type::T_LIST, '', []]; + static public $emptyMap = [Type::T_MAP, [], []]; + static public $emptyString = [Type::T_STRING, '"', []]; + static public $with = [Type::T_KEYWORD, 'with']; + static public $without = [Type::T_KEYWORD, 'without']; protected $importPaths = ['']; protected $importCache = []; @@ -165,27 +165,28 @@ class Compiler /** * Constructor */ - public function __construct($cache_options = null) + public function __construct($cacheOptions = null) { $this->parsedFiles = []; $this->sourceNames = []; - if ($cache_options) { - $this->cache = new Cache($cache_options); + if ($cacheOptions) { + $this->cache = new Cache($cacheOptions); } } public function getCompileOptions() { - $options = array( - 'importPaths' => $this->importPaths, - 'registeredVars' => $this->registeredVars, + $options = [ + 'importPaths' => $this->importPaths, + 'registeredVars' => $this->registeredVars, 'registeredFeatures' => $this->registeredFeatures, - 'encoding' => $this->encoding, - 'sourceMap' => serialize($this->sourceMap), - 'sourceMapOptions' => $this->sourceMapOptions, - 'formater' => $this->formatter, - ); + 'encoding' => $this->encoding, + 'sourceMap' => serialize($this->sourceMap), + 'sourceMapOptions' => $this->sourceMapOptions, + 'formatter' => $this->formatter, + ]; + return $options; } @@ -201,22 +202,25 @@ public function getCompileOptions() */ public function compile($code, $path = null) { - if ($this->cache) { - $cache_key = ($path ? $path : "(stdin)") . ":" . md5($code); - $compile_options = $this->getCompileOptions(); - $cache = $this->cache->getCache("compile", $cache_key, $compile_options); + $cacheKey = ($path ? $path : "(stdin)") . ":" . md5($code); + $compileOptions = $this->getCompileOptions(); + $cache = $this->cache->getCache("compile", $cacheKey, $compileOptions); + if (is_array($cache) - and isset($cache['dependencies']) - and isset($cache['out']) ) { + && isset($cache['dependencies']) + && isset($cache['out']) + ) { // check if any dependency file changed before accepting the cache foreach ($cache['dependencies'] as $file => $mtime) { - if (!file_exists($file) - or filemtime($file) !== $mtime) { + if (! file_exists($file) + || filemtime($file) !== $mtime + ) { unset($cache); break; } } + if (isset($cache)) { return $cache['out']; } @@ -279,12 +283,13 @@ public function compile($code, $path = null) $out .= sprintf('/*# sourceMappingURL=%s */', $sourceMapUrl); } - if ($this->cache and isset($cache_key) and isset($compile_options)) { - $v = array( + if ($this->cache && isset($cacheKey) && isset($compileOptions)) { + $v = [ 'dependencies' => $this->getParsedFiles(), 'out' => &$out, - ); - $this->cache->setCache("compile", $cache_key, $v, $compile_options); + ]; + + $this->cache->setCache("compile", $cacheKey, $v, $compileOptions); } return $out; @@ -490,6 +495,7 @@ protected function flattenSelectors(OutputBlock $block, $parentKey = null) protected function glueFunctionSelectors($parts) { $new = []; + foreach ($parts as $part) { if (is_array($part)) { $part = $this->glueFunctionSelectors($part); @@ -506,6 +512,7 @@ protected function glueFunctionSelectors($parts) } } } + return $new; } @@ -520,6 +527,7 @@ protected function glueFunctionSelectors($parts) protected function matchExtends($selector, &$out, $from = 0, $initial = true) { static $partsPile = []; + $selector = $this->glueFunctionSelectors($selector); foreach ($selector as $i => $part) { @@ -539,8 +547,8 @@ protected function matchExtends($selector, &$out, $from = 0, $initial = true) if ($this->matchExtendsSingle($part, $origin)) { $partsPile[] = $part; - $after = array_slice($selector, $i + 1); - $before = array_slice($selector, 0, $i); + $after = array_slice($selector, $i + 1); + $before = array_slice($selector, 0, $i); list($before, $nonBreakableBefore) = $this->extractRelationshipFromFragment($before); @@ -548,7 +556,7 @@ protected function matchExtends($selector, &$out, $from = 0, $initial = true) $k = 0; // remove shared parts - if (count($new)>1) { + if (count($new) > 1) { while ($k < $i && isset($new[$k]) && $selector[$k] === $new[$k]) { $k++; } @@ -561,7 +569,7 @@ protected function matchExtends($selector, &$out, $from = 0, $initial = true) $slice = []; foreach ($tempReplacement[$l] as $chunk) { - if (!in_array($chunk, $slice)) { + if (! in_array($chunk, $slice)) { $slice[] = $chunk; } } @@ -592,19 +600,19 @@ protected function matchExtends($selector, &$out, $from = 0, $initial = true) $out[] = $result; // recursively check for more matches - $startRecursFrom = count($before) + min(count($nonBreakableBefore), count($mergedBefore)); - $this->matchExtends($result, $out, $startRecursFrom, false); + $startRecurseFrom = count($before) + min(count($nonBreakableBefore), count($mergedBefore)); + $this->matchExtends($result, $out, $startRecurseFrom, false); // selector sequence merging if (! empty($before) && count($new) > 1) { - $sharedParts = $k > 0 ? array_slice($before, 0, $k) : []; + $preSharedParts = $k > 0 ? array_slice($before, 0, $k) : []; $postSharedParts = $k > 0 ? array_slice($before, $k) : $before; - list($injectBetweenSharedParts, $nonBreakable2) = $this->extractRelationshipFromFragment($afterBefore); + list($betweenSharedParts, $nonBreakable2) = $this->extractRelationshipFromFragment($afterBefore); $result2 = array_merge( - $sharedParts, - $injectBetweenSharedParts, + $preSharedParts, + $betweenSharedParts, $postSharedParts, $nonBreakable2, $nonBreakableBefore, @@ -615,6 +623,7 @@ protected function matchExtends($selector, &$out, $from = 0, $initial = true) $out[] = $result2; } } + array_pop($partsPile); } } @@ -671,6 +680,7 @@ protected function matchExtendsSingle($rawSingle, &$outOrigin) foreach ($counts as $idx => $count) { list($target, $origin, /* $block */) = $this->extends[$idx]; + $origin = $this->glueFunctionSelectors($origin); // check count @@ -797,6 +807,7 @@ protected function compileMedia(Block $media) if (! empty($mediaQueries) && $mediaQueries) { $previousScope = $this->scope; $parentScope = $this->mediaParent($this->scope); + foreach ($mediaQueries as $mediaQuery) { $this->scope = $this->makeOutputBlock(Type::T_MEDIA, [$mediaQuery]); @@ -811,9 +822,9 @@ protected function compileMedia(Block $media) $type = $child[0]; if ($type !== Type::T_BLOCK && - $type !== Type::T_MEDIA && - $type !== Type::T_DIRECTIVE && - $type !== Type::T_IMPORT + $type !== Type::T_MEDIA && + $type !== Type::T_DIRECTIVE && + $type !== Type::T_IMPORT ) { $needsWrap = true; break; @@ -1002,7 +1013,7 @@ protected function filterScopeWithout($scope, $without) } } - if (!count($filteredScopes)) { + if (! count($filteredScopes)) { return $this->rootBlock; } @@ -1136,10 +1147,13 @@ protected function filterWithout($envs, $without) foreach ($envs as $e) { if ($e->block && $this->isWithout($without, $e->block)) { - continue; + $ec = clone $e; + $ec->block = null; + $ec->selectors = []; + $filtered[] = $ec; + } else { + $filtered[] = $e; } - - $filtered[] = $e; } return $this->extractEnv($filtered); @@ -1338,7 +1352,7 @@ protected function compileBlock(Block $block) protected function compileComment($block) { $out = $this->makeOutputBlock(Type::T_COMMENT); - $out->lines[] = $block[1]; + $out->lines[] = is_string($block[1]) ? $block[1] : $this->compileValue($block[1]); $this->scope->children[] = $out; } @@ -1413,8 +1427,8 @@ protected function evalSelectorPart($part) /** * Collapse selectors * - * @param array $selectors - * @param bool $selectorFormat + * @param array $selectors + * @param boolean $selectorFormat * if false return a collapsed string * if true return an array description of a structured selector * @@ -1427,6 +1441,7 @@ protected function collapseSelectors($selectors, $selectorFormat = false) foreach ($selectors as $selector) { $output = []; $glueNext = false; + foreach ($selector as $node) { $compound = ''; @@ -1436,6 +1451,7 @@ function ($value, $key) use (&$compound) { $compound .= $value; } ); + if ($selectorFormat && $this->isImmediateRelationshipCombinator($compound)) { if (count($output)) { $output[count($output) - 1] .= ' ' . $compound; @@ -1459,6 +1475,7 @@ function ($value, $key) use (&$compound) { } else { $output = implode(' ', $output); } + $parts[] = $output; } @@ -1489,6 +1506,7 @@ protected function revertSelfSelector($selectors) } } } + return $selectors; } @@ -1604,6 +1622,7 @@ protected function pushCallStack($name = '') Parser::SOURCE_LINE => $this->sourceLine, Parser::SOURCE_COLUMN => $this->sourceColumn ]; + // infinite calling loop if (count($this->callStack) > 25000) { // not displayed but you can var_dump it to deep debug @@ -1630,6 +1649,7 @@ protected function popCallStack() protected function compileChildren($stms, OutputBlock $out, $traceName = '') { $this->pushCallStack($traceName); + foreach ($stms as $stm) { $ret = $this->compileChild($stm, $out); @@ -1637,6 +1657,7 @@ protected function compileChildren($stms, OutputBlock $out, $traceName = '') return $ret; } } + $this->popCallStack(); return null; @@ -1655,6 +1676,7 @@ protected function compileChildren($stms, OutputBlock $out, $traceName = '') protected function compileChildrenNoReturn($stms, OutputBlock $out, $selfParent = null, $traceName = '') { $this->pushCallStack($traceName); + foreach ($stms as $stm) { if ($selfParent && isset($stm[1]) && is_object($stm[1]) && $stm[1] instanceof Block) { $stm[1]->selfParent = $selfParent; @@ -1674,6 +1696,7 @@ protected function compileChildrenNoReturn($stms, OutputBlock $out, $selfParent return; } } + $this->popCallStack(); } @@ -1754,12 +1777,14 @@ protected function compileMediaQuery($queryList) $parts = []; $mediaTypeOnly = true; + foreach ($query as $q) { if ($q[0] !== Type::T_MEDIA_TYPE) { $mediaTypeOnly = false; break; } } + foreach ($query as $q) { switch ($q[0]) { case Type::T_MEDIA_TYPE: @@ -1770,23 +1795,29 @@ protected function compileMediaQuery($queryList) if ($type) { array_unshift($parts, implode(' ', array_filter($type))); } + if (! empty($parts)) { if (strlen($current)) { $current .= $this->formatter->tagSeparator; } + $current .= implode(' and ', $parts); } + if ($current) { $out[] = $start . $current; } + $current = ""; $type = null; $parts = []; } } + if ($newType === ['all'] && $default) { $default = $start . 'all'; } + // all can be safely ignored and mixed with whatever else if ($newType !== ['all']) { if ($type) { @@ -1840,10 +1871,12 @@ protected function compileMediaQuery($queryList) if ($current) { $out[] = $start . $current; } + // no @media type except all, and no conflict? - if (!$out && $default) { + if (! $out && $default) { $out[] = $default; } + return $out; } @@ -1881,6 +1914,7 @@ protected function mergeDirectRelationships($selectors1, $selectors2) } else { $merged = array_merge($selectors1, [$part1], $selectors2, [$part2], $merged); } + break; } @@ -2018,11 +2052,11 @@ 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; - } elseif (is_array($child) and isset($child[1]->sourceLine)) { + } elseif (is_array($child) && isset($child[1]->sourceLine)) { $this->sourceIndex = $child[1]->sourceIndex; $this->sourceLine = $child[1]->sourceLine; $this->sourceColumn = $child[1]->sourceColumn; - } elseif (! empty($out->sourceLine) and ! empty($out->sourceName)) { + } elseif (! empty($out->sourceLine) && ! empty($out->sourceName)) { $this->sourceLine = $out->sourceLine; $this->sourceIndex = array_search($out->sourceName, $this->sourceNames); @@ -2379,7 +2413,8 @@ protected function compileChild($child, OutputBlock $out) break; case Type::T_MIXIN_CONTENT: - $content = $this->get(static::$namespaces['special'] . 'content', false, isset($this->storeEnv) ? $this->storeEnv : $this->env); + $env = isset($this->storeEnv) ? $this->storeEnv : $this->env; + $content = $this->get(static::$namespaces['special'] . 'content', false, $env); if (! $content) { $content = new \stdClass(); @@ -2897,11 +2932,17 @@ protected function opAdd($left, $right) */ protected function opAnd($left, $right, $shouldEval) { + $truthy = ($left === static::$null || $right === static::$null) || + ($left === static::$false || $left === static::$true) && + ($right === static::$false || $right === static::$true); + if (! $shouldEval) { - return null; + if (! $truthy) { + return null; + } } - if ($left !== static::$false and $left !== static::$null) { + if ($left !== static::$false && $left !== static::$null) { return $this->reduce($right, true); } @@ -2919,11 +2960,17 @@ protected function opAnd($left, $right, $shouldEval) */ protected function opOr($left, $right, $shouldEval) { + $truthy = ($left === static::$null || $right === static::$null) || + ($left === static::$false || $left === static::$true) && + ($right === static::$false || $right === static::$true); + if (! $shouldEval) { - return null; + if (! $truthy) { + return null; + } } - if ($left !== static::$false and $left !== static::$null) { + if ($left !== static::$false && $left !== static::$null) { return $left; } @@ -3401,7 +3448,7 @@ protected function multiplySelectors(Environment $env, $selfParent = null) $selfParentSelectors = null; - if (!is_null($selfParent) and $selfParent->selectors) { + if (! is_null($selfParent) && $selfParent->selectors) { $selfParentSelectors = $this->evalSelectors($selfParent->selectors); } @@ -3444,10 +3491,10 @@ protected function multiplySelectors(Environment $env, $selfParent = null) /** * Join selectors; looks for & to replace, or append parent before child * - * @param array $parent - * @param array $child - * @param bool &$stillHasSelf - * @param array $selfParentSelectors + * @param array $parent + * @param array $child + * @param boolean &$stillHasSelf + * @param array $selfParentSelectors * @return array */ @@ -3464,7 +3511,8 @@ protected function joinSelectors($parent, $child, &$stillHasSelf, $selfParentSel if ($p === static::$selfSelector && $setSelf) { $stillHasSelf = true; } - if ($p === static::$selfSelector && !$setSelf) { + + if ($p === static::$selfSelector && ! $setSelf) { $setSelf = true; if (is_null($selfParentSelectors)) { @@ -3485,6 +3533,7 @@ protected function joinSelectors($parent, $child, &$stillHasSelf, $selfParentSel }); $pp = implode($flatten); } + $newPart[] = $pp; } } @@ -3538,7 +3587,11 @@ protected function multiplyMedia(Environment $env = null, $childQueries = null) foreach ($parentQueries as $parentQuery) { foreach ($originalQueries as $childQuery) { - $childQueries []= array_merge($parentQuery, [[Type::T_MEDIA_TYPE, [Type::T_KEYWORD, 'all']]], $childQuery); + $childQueries[] = array_merge( + $parentQuery, + [[Type::T_MEDIA_TYPE, [Type::T_KEYWORD, 'all']]], + $childQuery + ); } } } @@ -3727,6 +3780,7 @@ public function get($name, $shouldThrow = true, Environment $env = null, $unredu if ($maxDepth-- <= 0) { break; } + if (array_key_exists($normalizedName, $env->store)) { if ($unreduced && isset($env->storeUnreduced[$normalizedName])) { return $env->storeUnreduced[$normalizedName]; @@ -4040,7 +4094,7 @@ public function findImport($url) // check urls for normal import paths foreach ($urls as $full) { $separator = ( - !empty($dir) && + ! empty($dir) && substr($dir, -1) !== '/' && substr($full, 0, 1) !== '/' ) ? '/' : ''; @@ -4108,17 +4162,21 @@ public function throwError($msg) return; } + $line = $this->sourceLine; + $column = $this->sourceColumn; + + $loc = isset($this->sourceNames[$this->sourceIndex]) + ? $this->sourceNames[$this->sourceIndex] . " on line $line, at column $column" + : "line: $line, column: $column"; + if (func_num_args() > 1) { $msg = call_user_func_array('sprintf', func_get_args()); } - $line = $this->sourceLine; - $loc = isset($this->sourceNames[$this->sourceIndex]) - ? $this->sourceNames[$this->sourceIndex] . " on line $line" - : "line: $line"; $msg = "$msg: $loc"; $callStackMsg = $this->callStackMessage(); + if ($callStackMsg) { $msg .= "\nCall Stack:\n" . $callStackMsg; } @@ -4127,8 +4185,11 @@ public function throwError($msg) } /** - * @param bool $all - * @param null $limit + * Beautify call stack for output + * + * @param boolean $all + * @param null $limit + * * @return string */ protected function callStackMessage($all = false, $limit = null) @@ -4145,7 +4206,8 @@ protected function callStackMessage($all = false, $limit = null) : '(unknown file)'); $msg .= " on line " . $call[Parser::SOURCE_LINE]; $callStackMsg[] = $msg; - if (!is_null($limit) && $ncall>$limit) { + + if (! is_null($limit) && $ncall>$limit) { break; } } @@ -5974,4 +6036,624 @@ protected function libInspect($args) return $args[0]; } + + /** + * Preprocess selector args + * + * @param array $arg + * + * @return array|boolean + */ + protected function getSelectorArg($arg) + { + static $parser = null; + + if (is_null($parser)) { + $parser = $this->parserFactory(__METHOD__); + } + + $arg = $this->libUnquote([$arg]); + $arg = $this->compileValue($arg); + + $parsedSelector = []; + + if ($parser->parseSelector($arg, $parsedSelector)) { + $selector = $this->evalSelectors($parsedSelector); + $gluedSelector = $this->glueFunctionSelectors($selector); + + return $gluedSelector; + } + + return false; + } + + /** + * Postprocess selector to output in right format + * + * @param array $selectors + * + * @return string + */ + protected function formatOutputSelector($selectors) + { + $selectors = $this->collapseSelectors($selectors, true); + + return $selectors; + } + + protected static $libIsSuperselector = ['super', 'sub']; + protected function libIsSuperselector($args) + { + list($super, $sub) = $args; + + $super = $this->getSelectorArg($super); + $sub = $this->getSelectorArg($sub); + + return $this->isSuperSelector($super, $sub); + } + + /** + * Test a $super selector again $sub + * + * @param array $super + * @param array $sub + * + * @return boolean + */ + protected function isSuperSelector($super, $sub) + { + // one and only one selector for each arg + if (! $super || count($super) !== 1) { + $this->throwError("Invalid super selector for isSuperSelector()"); + } + + if (! $sub || count($sub) !== 1) { + $this->throwError("Invalid sub selector for isSuperSelector()"); + } + + $super = reset($super); + $sub = reset($sub); + + $i = 0; + $nextMustMatch = false; + + foreach ($super as $node) { + $compound = ''; + + array_walk_recursive( + $node, + function ($value, $key) use (&$compound) { + $compound .= $value; + } + ); + + if ($this->isImmediateRelationshipCombinator($compound)) { + if ($node !== $sub[$i]) { + return false; + } + + $nextMustMatch = true; + $i++; + } else { + while ($i < count($sub) && ! $this->isSuperPart($node, $sub[$i])) { + if ($nextMustMatch) { + return false; + } + + $i++; + } + + if ($i >= count($sub)) { + return false; + } + + $nextMustMatch = false; + $i++; + } + } + + return true; + } + + /** + * Test a part of super selector again a part of sub selector + * + * @param array $superParts + * @param array $subParts + * + * @return boolean + */ + protected function isSuperPart($superParts, $subParts) + { + $i = 0; + + foreach ($superParts as $superPart) { + while ($i < count($subParts) && $subParts[$i] !== $superPart) { + $i++; + } + + if ($i >= count($subParts)) { + return false; + } + + $i++; + } + + return true; + } + + //protected static $libSelectorAppend = ['selector...']; + protected function libSelectorAppend($args) + { + if (count($args) < 1) { + $this->throwError("selector-append() needs at least 1 argument"); + } + + $selectors = array_map([$this, 'getSelectorArg'], $args); + + return $this->formatOutputSelector($this->selectorAppend($selectors)); + } + + /** + * Append parts of the last selector in the list to the previous, recursively + * + * @param array $selectors + * + * @return array + * + * @throws \Leafo\ScssPhp\Exception\CompilerException + */ + protected function selectorAppend($selectors) + { + $lastSelectors = array_pop($selectors); + + if (! $lastSelectors) { + $this->throwError("Invalid selector list in selector-append()"); + } + + while (count($selectors)) { + $previousSelectors = array_pop($selectors); + + if (! $previousSelectors) { + $this->throwError("Invalid selector list in selector-append()"); + } + + // do the trick, happening $lastSelector to $previousSelector + $appended = []; + + foreach ($lastSelectors as $lastSelector) { + $previous = $previousSelectors; + + foreach ($lastSelector as $lastSelectorParts) { + foreach ($lastSelectorParts as $lastSelectorPart) { + foreach ($previous as $i => $previousSelector) { + foreach ($previousSelector as $j => $previousSelectorParts) { + $previous[$i][$j][] = $lastSelectorPart; + } + } + } + } + + foreach ($previous as $ps) { + $appended[] = $ps; + } + } + + $lastSelectors = $appended; + } + + return $lastSelectors; + } + + protected static $libSelectorExtend = ['selectors', 'extendee', 'extender']; + protected function libSelectorExtend($args) + { + list($selectors, $extendee, $extender) = $args; + + $selectors = $this->getSelectorArg($selectors); + $extendee = $this->getSelectorArg($extendee); + $extender = $this->getSelectorArg($extender); + + if (! $selectors || ! $extendee || ! $extender) { + $this->throwError("selector-extend() invalid arguments"); + } + + $extended = $this->extendOrReplaceSelectors($selectors, $extendee, $extender); + + return $this->formatOutputSelector($extended); + } + + protected static $libSelectorReplace = ['selectors', 'original', 'replacement']; + protected function libSelectorReplace($args) + { + list($selectors, $original, $replacement) = $args; + + $selectors = $this->getSelectorArg($selectors); + $original = $this->getSelectorArg($original); + $replacement = $this->getSelectorArg($replacement); + + if (! $selectors || ! $original || ! $replacement) { + $this->throwError("selector-replace() invalid arguments"); + } + + $replaced = $this->extendOrReplaceSelectors($selectors, $original, $replacement, true); + + return $this->formatOutputSelector($replaced); + } + + /** + * Extend/replace in selectors + * used by selector-extend and selector-replace that use the same logic + * + * @param array $selectors + * @param array $extendee + * @param array $extender + * @param boolean $replace + * + * @return array + */ + protected function extendOrReplaceSelectors($selectors, $extendee, $extender, $replace = false) + { + $saveExtends = $this->extends; + $saveExtendsMap = $this->extendsMap; + + $this->extends = []; + $this->extendsMap = []; + + foreach ($extendee as $es) { + // only use the first one + $this->pushExtends(reset($es), $extender, null); + } + + $extended = []; + + foreach ($selectors as $selector) { + if (! $replace) { + $extended[] = $selector; + } + + $n = count($extended); + + $this->matchExtends($selector, $extended); + + // if didnt match, keep the original selector if we are in a replace operation + if ($replace and count($extended) === $n) { + $extended[] = $selector; + } + } + + $this->extends = $saveExtends; + $this->extendsMap = $saveExtendsMap; + + return $extended; + } + + //protected static $libSelectorNest = ['selector...']; + protected function libSelectorNest($args) + { + if (count($args) < 1) { + $this->throwError("selector-nest() needs at least 1 argument"); + } + + $selectorsMap = array_map([$this, 'getSelectorArg'], $args); + + $envs = []; + foreach ($selectorsMap as $selectors) { + $env = new Environment(); + $env->selectors = $selectors; + + $envs[] = $env; + } + + $envs = array_reverse($envs); + $env = $this->extractEnv($envs); + $outputSelectors = $this->multiplySelectors($env); + + return $this->formatOutputSelector($outputSelectors); + } + + protected static $libSelectorParse = ['selectors']; + protected function libSelectorParse($args) + { + $selectors = reset($args); + $selectors = $this->getSelectorArg($selectors); + + return $this->formatOutputSelector($selectors); + } + + protected static $libSelectorUnify = ['selectors1', 'selectors2']; + protected function libSelectorUnify($args) + { + list($selectors1, $selectors2) = $args; + + $selectors1 = $this->getSelectorArg($selectors1); + $selectors2 = $this->getSelectorArg($selectors2); + + if (! $selectors1 || ! $selectors2) { + $this->throwError("selector-unify() invalid arguments"); + } + + // only consider the first compound of each + $compound1 = reset($selectors1); + $compound2 = reset($selectors2); + + // unify them and that's it + $unified = $this->unifyCompoundSelectors($compound1, $compound2); + + return $this->formatOutputSelector($unified); + } + + /** + * The selector-unify magic as its best + * (at least works as expected on test cases) + * + * @param array $compound1 + * @param array $compound2 + * @return array|mixed + */ + protected function unifyCompoundSelectors($compound1, $compound2) + { + if (! count($compound1)) { + return $compound2; + } + + if (! count($compound2)) { + return $compound1; + } + + // check that last part are compatible + $lastPart1 = array_pop($compound1); + $lastPart2 = array_pop($compound2); + $last = $this->mergeParts($lastPart1, $lastPart2); + + if (! $last) { + return [[]]; + } + + $unifiedCompound = [$last]; + $unifiedSelectors = [$unifiedCompound]; + + // do the rest + while (count($compound1) || count($compound2)) { + $part1 = end($compound1); + $part2 = end($compound2); + + if ($part1 && ($match2 = $this->matchPartInCompound($part1, $compound2))) { + list($compound2, $part2, $after2) = $match2; + + if ($after2) { + $unifiedSelectors = $this->prependSelectors($unifiedSelectors, $after2); + } + + $c = $this->mergeParts($part1, $part2); + $unifiedSelectors = $this->prependSelectors($unifiedSelectors, [$c]); + $part1 = $part2 = null; + + array_pop($compound1); + } + + if ($part2 && ($match1 = $this->matchPartInCompound($part2, $compound1))) { + list($compound1, $part1, $after1) = $match1; + + if ($after1) { + $unifiedSelectors = $this->prependSelectors($unifiedSelectors, $after1); + } + + $c = $this->mergeParts($part2, $part1); + $unifiedSelectors = $this->prependSelectors($unifiedSelectors, [$c]); + $part1 = $part2 = null; + + array_pop($compound2); + } + + $new = []; + + if ($part1 && $part2) { + array_pop($compound1); + array_pop($compound2); + + $s = $this->prependSelectors($unifiedSelectors, [$part2]); + $new = array_merge($new, $this->prependSelectors($s, [$part1])); + $s = $this->prependSelectors($unifiedSelectors, [$part1]); + $new = array_merge($new, $this->prependSelectors($s, [$part2])); + } elseif ($part1) { + array_pop($compound1); + + $new = array_merge($new, $this->prependSelectors($unifiedSelectors, [$part1])); + } elseif ($part2) { + array_pop($compound2); + + $new = array_merge($new, $this->prependSelectors($unifiedSelectors, [$part2])); + } + + if ($new) { + $unifiedSelectors = $new; + } + } + + return $unifiedSelectors; + } + + /** + * Prepend each selector from $selectors with $parts + * + * @param array $selectors + * @param array $parts + * + * @return array + */ + protected function prependSelectors($selectors, $parts) + { + $new = []; + + foreach ($selectors as $compoundSelector) { + array_unshift($compoundSelector, $parts); + + $new[] = $compoundSelector; + } + + return $new; + } + + /** + * Try to find a matching part in a compound: + * - with same html tag name + * - with some class or id or something in common + * + * @param array $part + * @param array $compound + * + * @return array|boolean + */ + protected function matchPartInCompound($part, $compound) + { + $partTag = $this->findTagName($part); + $before = $compound; + $after = []; + + // try to find a match by tag name first + while (count($before)) { + $p = array_pop($before); + + if ($partTag && $partTag !== '*' && $partTag == $this->findTagName($p)) { + return [$before, $p, $after]; + } + + $after[] = $p; + } + + // try again matching a non empty intersection and a compatible tagname + $before = $compound; + $after = []; + + while (count($before)) { + $p = array_pop($before); + + if ($this->checkCompatibleTags($partTag, $this->findTagName($p))) { + if (count(array_intersect($part, $p))) { + return [$before, $p, $after]; + } + } + + $after[] = $p; + } + + return false; + } + + /** + * Merge two part list taking care that + * - the html tag is coming first - if any + * - the :something are coming last + * + * @param array $parts1 + * @param array $parts2 + * + * @return array + */ + protected function mergeParts($parts1, $parts2) + { + $tag1 = $this->findTagName($parts1); + $tag2 = $this->findTagName($parts2); + $tag = $this->checkCompatibleTags($tag1, $tag2); + + // not compatible tags + if ($tag === false) { + return []; + } + + if ($tag) { + if ($tag1) { + $parts1 = array_diff($parts1, [$tag1]); + } + + if ($tag2) { + $parts2 = array_diff($parts2, [$tag2]); + } + } + + $mergedParts = array_merge($parts1, $parts2); + $mergedOrderedParts = []; + + foreach ($mergedParts as $part) { + if (strpos($part, ':') === 0) { + $mergedOrderedParts[] = $part; + } + } + + $mergedParts = array_diff($mergedParts, $mergedOrderedParts); + $mergedParts = array_merge($mergedParts, $mergedOrderedParts); + + if ($tag) { + array_unshift($mergedParts, $tag); + } + + return $mergedParts; + } + + /** + * Check the compatibility between two tag names: + * if both are defined they should be identical or one has to be '*' + * + * @param string $tag1 + * @param string $tag2 + * + * @return array|boolean + */ + protected function checkCompatibleTags($tag1, $tag2) + { + $tags = [$tag1, $tag2]; + $tags = array_unique($tags); + $tags = array_filter($tags); + + if (count($tags)>1) { + $tags = array_diff($tags, ['*']); + } + + // not compatible nodes + if (count($tags)>1) { + return false; + } + + return $tags; + } + + /** + * Find the html tag name in a selector parts list + * + * @param array $parts + * + * @return mixed|string + */ + protected function findTagName($parts) + { + foreach ($parts as $part) { + if (! preg_match('/^[\[.:#%_-]/', $part)) { + return $part; + } + } + + return ''; + } + + protected static $libSimpleSelectors = ['selector']; + protected function libSimpleSelectors($args) + { + $selector = reset($args); + $selector = $this->getSelectorArg($selector); + + // remove selectors list layer, keeping the first one + $selector = reset($selector); + + // remove parts list layer, keeping the first part + $part = reset($selector); + + $listParts = []; + + foreach ($part as $p) { + $listParts[] = [Type::T_STRING, '', [$p]]; + } + + return [Type::T_LIST, ',', $listParts]; + } } diff --git a/src/Formatter/Compressed.php b/src/Formatter/Compressed.php index 52410b6c..ab38529d 100644 --- a/src/Formatter/Compressed.php +++ b/src/Formatter/Compressed.php @@ -69,8 +69,13 @@ protected function blockSelectors(OutputBlock $block) { $inner = $this->indentStr(); - $this->write($inner - . implode($this->tagSeparator, str_replace(array(' > ', ' + ', ' ~ '), array('>', '+', '~'), $block->selectors)) - . $this->open . $this->break); + $this->write( + $inner + . implode( + $this->tagSeparator, + str_replace([' > ', ' + ', ' ~ '], ['>', '+', '~'], $block->selectors) + ) + . $this->open . $this->break + ); } } diff --git a/src/Formatter/Crunched.php b/src/Formatter/Crunched.php index 31970255..da740ccd 100644 --- a/src/Formatter/Crunched.php +++ b/src/Formatter/Crunched.php @@ -67,8 +67,13 @@ protected function blockSelectors(OutputBlock $block) { $inner = $this->indentStr(); - $this->write($inner - . implode($this->tagSeparator, str_replace(array(' > ', ' + ', ' ~ '), array('>', '+', '~'), $block->selectors)) - . $this->open . $this->break); + $this->write( + $inner + . implode( + $this->tagSeparator, + str_replace([' > ', ' + ', ' ~ '], ['>', '+', '~'], $block->selectors) + ) + . $this->open . $this->break + ); } } diff --git a/src/Parser.php b/src/Parser.php index eb264bce..c05e4ee8 100644 --- a/src/Parser.php +++ b/src/Parser.php @@ -12,6 +12,7 @@ namespace Leafo\ScssPhp; use Leafo\ScssPhp\Block; +use Leafo\ScssPhp\Cache; use Leafo\ScssPhp\Compiler; use Leafo\ScssPhp\Exception\ParserException; use Leafo\ScssPhp\Node; @@ -74,10 +75,10 @@ class Parser * * @api * - * @param string $sourceName - * @param integer $sourceIndex - * @param string $encoding - * @param Cache $cache + * @param string $sourceName + * @param integer $sourceIndex + * @param string $encoding + * @param \Leafo\ScssPhp\Cache $cache */ public function __construct($sourceName, $sourceIndex = 0, $encoding = 'utf-8', $cache = null) { @@ -86,7 +87,7 @@ public function __construct($sourceName, $sourceIndex = 0, $encoding = 'utf-8', $this->charset = null; $this->utf8 = ! $encoding || strtolower($encoding) === 'utf-8'; $this->patternModifiers = $this->utf8 ? 'Aisu' : 'Ais'; - $this->commentsSeen = []; + $this->commentsSeen = []; if (empty(static::$operatorPattern)) { static::$operatorPattern = '([*\/%+-]|[!=]\=|\>\=?|\<\=\>|\<\=?|and|or)'; @@ -129,9 +130,11 @@ public function getSourceName() */ public function throwParseError($msg = 'parse error') { - list($line, /* $column */) = $this->getSourcePosition($this->count); + list($line, $column) = $this->getSourcePosition($this->count); - $loc = empty($this->sourceName) ? "line: $line" : "$this->sourceName on line $line"; + $loc = empty($this->sourceName) + ? "line: $line, column: $column" + : "$this->sourceName on line $line, at column $column"; if ($this->peek("(.*?)(\n|$)", $m, $this->count)) { throw new ParserException("$msg: failed at `$m[1]` $loc"); @@ -151,15 +154,15 @@ public function throwParseError($msg = 'parse error') */ public function parse($buffer) { - if ($this->cache) { - $cache_key = $this->sourceName . ":" . md5($buffer); - $parse_options = array( + $cacheKey = $this->sourceName . ":" . md5($buffer); + $parseOptions = [ 'charset' => $this->charset, 'utf8' => $this->utf8, - ); - $v = $this->cache->getCache("parse", $cache_key, $parse_options); - if (!is_null($v)) { + ]; + $v = $this->cache->getCache("parse", $cacheKey, $parseOptions); + + if (! is_null($v)) { return $v; } } @@ -199,12 +202,10 @@ public function parse($buffer) array_unshift($this->env->children, $this->charset); } - $this->env->isRoot = true; - $this->restoreEncoding(); if ($this->cache) { - $this->cache->setCache("parse", $cache_key, $this->env, $parse_options); + $this->cache->setCache("parse", $cacheKey, $this->env, $parseOptions); } return $this->env; @@ -661,9 +662,14 @@ protected function parseChunk() } // opening css block - if ($this->selectors($selectors) && $this->matchChar('{')) { + 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; } @@ -674,6 +680,10 @@ protected function parseChunk() $foundSomething = false; if ($this->valueList($value)) { + if (empty($this->env->parent)) { + $this->throwParseError('expected "{"'); + } + $this->append([Type::T_ASSIGN, $name, $value], $s); $foundSomething = true; } @@ -932,6 +942,7 @@ protected function matchChar($char, $eatWhitespace = null) if ($eatWhitespace) { $this->whitespace(); } + return true; } @@ -946,7 +957,6 @@ protected function matchChar($char, $eatWhitespace = null) */ protected function literal($what, $len, $eatWhitespace = null) { - if (strcasecmp(substr($this->buffer, $this->count, $len), $what) !== 0) { return false; } @@ -960,6 +970,7 @@ protected function literal($what, $len, $eatWhitespace = null) if ($eatWhitespace) { $this->whitespace(); } + return true; } @@ -974,12 +985,57 @@ protected function whitespace() while (preg_match(static::$whitePattern, $this->buffer, $m, null, $this->count)) { if (isset($m[1]) && empty($this->commentsSeen[$this->count])) { - $this->appendComment([Type::T_COMMENT, $m[1]]); + // comment that are kept in the output CSS + $comment = []; + $endCommentCount = $this->count + strlen($m[1]); + + // find interpolations in comment + $p = strpos($this->buffer, '#{', $this->count); + + while ($p !== false && $p < $endCommentCount) { + $c = substr($this->buffer, $this->count, $p - $this->count); + $comment[] = $c; + $this->count = $p; + $out = null; + + if ($this->interpolation($out)) { + // keep right spaces in the following string part + if ($out[3]) { + while ($this->buffer[$this->count-1] !== '}') { + $this->count--; + } + + $out[3] = ''; + } + + $comment[] = $out; + } else { + $comment[] = substr($this->buffer, $this->count, 2); + + $this->count += 2; + } + + $p = strpos($this->buffer, '#{', $this->count); + } + + // remaining part + $c = substr($this->buffer, $this->count, $endCommentCount - $this->count); + + if (! $comment) { + // single part static comment + $this->appendComment([Type::T_COMMENT, $c]); + } else { + $comment[] = $c; + $this->appendComment([Type::T_COMMENT, [Type::T_STRING, '', $comment]]); + } $this->commentsSeen[$this->count] = true; + $this->count = $endCommentCount; + } else { + // comment that are ignored and not kept in the output css + $this->count += strlen($m[0]); } - $this->count += strlen($m[0]); $gotWhite = true; } @@ -993,7 +1049,9 @@ protected function whitespace() */ protected function appendComment($comment) { - $comment[1] = substr(preg_replace(['/^\s+/m', '/^(.)/m'], ['', ' \1'], $comment[1]), 1); + if ($comment[0] === Type::T_COMMENT && is_string($comment[1])) { + $comment[1] = substr(preg_replace(['/^\s+/m', '/^(.)/m'], ['', ' \1'], $comment[1]), 1); + } $this->env->comments[] = $comment; } @@ -1006,15 +1064,17 @@ protected function appendComment($comment) */ protected function append($statement, $pos = null) { - if ($pos !== null) { - list($line, $column) = $this->getSourcePosition($pos); + if (! is_null($statement)) { + if ($pos !== null) { + list($line, $column) = $this->getSourcePosition($pos); - $statement[static::SOURCE_LINE] = $line; - $statement[static::SOURCE_COLUMN] = $column; - $statement[static::SOURCE_INDEX] = $this->sourceIndex; - } + $statement[static::SOURCE_LINE] = $line; + $statement[static::SOURCE_COLUMN] = $column; + $statement[static::SOURCE_INDEX] = $this->sourceIndex; + } - $this->env->children[] = $statement; + $this->env->children[] = $statement; + } $comments = $this->env->comments; @@ -1235,7 +1295,7 @@ protected function genericList(&$out, $parseItem, $delim = '', $flatten = true) } } - if (!$items) { + if (! $items) { $this->seek($s); return false; @@ -1270,7 +1330,11 @@ protected function expression(&$out) } if ($this->matchChar('[')) { - if ($this->parenExpression($out, $s, "]")) { + if ($this->parenExpression($out, $s, "]", [Type::T_LIST, Type::T_KEYWORD])) { + if ($out[0] !== Type::T_LIST && $out[0] !== Type::T_MAP) { + $out = [Type::T_STRING, '', [ '[', $out, ']' ]]; + } + return true; } @@ -1292,10 +1356,11 @@ protected function expression(&$out) * @param array $out * @param integer $s * @param string $closingParen + * @param array $allowedTypes * * @return boolean */ - protected function parenExpression(&$out, $s, $closingParen = ")") + protected function parenExpression(&$out, $s, $closingParen = ")", $allowedTypes = [Type::T_LIST, Type::T_MAP]) { if ($this->matchChar($closingParen)) { $out = [Type::T_LIST, '', []]; @@ -1303,13 +1368,13 @@ protected function parenExpression(&$out, $s, $closingParen = ")") return true; } - if ($this->valueList($out) && $this->matchChar($closingParen) && $out[0] === Type::T_LIST) { + if ($this->valueList($out) && $this->matchChar($closingParen) && in_array($out[0], $allowedTypes)) { return true; } $this->seek($s); - if ($this->map($out)) { + if (in_array(Type::T_MAP, $allowedTypes) && $this->map($out)) { return true; } @@ -1384,7 +1449,11 @@ protected function value(&$out) $char = $this->buffer[$this->count]; if ($this->literal('url(', 4) && $this->match('data:([a-z]+)\/([a-z0-9.+-]+);base64,', $m, false)) { - $len = strspn($this->buffer, 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwyxz0123456789+/=', $this->count); + $len = strspn( + $this->buffer, + 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwyxz0123456789+/=', + $this->count + ); $this->count += $len; @@ -1398,6 +1467,19 @@ protected function value(&$out) $this->seek($s); + if ($this->literal('url(', 4, false) && $this->match('\s*(\/\/\S+)\s*', $m)) { + $content = 'url(' . $m[1]; + + if ($this->matchChar(')')) { + $content .= ')'; + $out = [Type::T_KEYWORD, $content]; + + return true; + } + } + + $this->seek($s); + // not if ($char === 'n' && $this->literal('not', 3, false)) { if ($this->whitespace() && $this->value($inner)) { @@ -1423,6 +1505,7 @@ protected function value(&$out) if ($this->value($inner)) { $out = [Type::T_UNARY, '+', $inner, $this->inParens]; + return true; } @@ -1437,8 +1520,10 @@ protected function value(&$out) if ($this->variable($inner) || $this->unit($inner) || $this->parenValue($inner)) { $out = [Type::T_UNARY, '-', $inner, $this->inParens]; + return true; } + $this->count--; } @@ -1455,6 +1540,7 @@ protected function value(&$out) if ($this->matchChar('&', true)) { $out = [Type::T_SELF]; + return true; } @@ -1474,6 +1560,12 @@ protected function value(&$out) return true; } + // unicode range with wildcards + if ($this->literal('U+', 2) && $this->match('([0-9A-F]+\?*)(-([0-9A-F]+))?', $m, false)) { + $out = [Type::T_KEYWORD, 'U+' . $m[0]]; + return true; + } + if ($this->keyword($keyword, false)) { if ($this->func($keyword, $out)) { return true; @@ -1642,7 +1734,7 @@ protected function argumentList(&$out) $args[] = [Type::T_STRING, '', [', ']]; } - if (! $this->matchChar(')') || !$args) { + if (! $this->matchChar(')') || ! $args) { $this->seek($s); return false; @@ -1740,7 +1832,7 @@ protected function map(&$out) } } - if (!$keys || ! $this->matchChar(')')) { + if (! $keys || ! $this->matchChar(')')) { $this->seek($s); return false; @@ -1778,6 +1870,7 @@ protected function color(&$out) $color[$i] = $t << 4 | $t; $num >>= 4; } + break; case 6: @@ -1788,6 +1881,7 @@ protected function color(&$out) $color[$i] = $num & 0xff; $num >>= 8; } + break; default: @@ -1884,6 +1978,11 @@ protected function string(&$out) $content[] = $m[2] . "'"; } elseif ($this->literal("\\", 1, false)) { $content[] = $m[2] . "\\"; + } elseif ($this->literal("\r\n", 2, false) + || $this->matchChar("\r", false) + || $this->matchChar("\n", false) + || $this->matchChar("\f", false)) { + // this is a continuation escaping, to be ignored } else { $content[] = $m[2]; } @@ -1950,7 +2049,7 @@ protected function mixedKeyword(&$out) $this->eatWhiteDefault = $oldWhite; - if (!$parts) { + if (! $parts) { return false; } @@ -2016,7 +2115,7 @@ protected function openString($end, &$out, $nestingOpen = null) $this->eatWhiteDefault = $oldWhite; - if (!$content) { + if (! $content) { return false; } @@ -2058,6 +2157,7 @@ protected function interpolation(&$out, $lookWhite = true) $out = [Type::T_INTERPOLATE, $value, $left, $right]; } + $this->eatWhiteDefault = $oldWhite; if ($this->eatWhiteDefault) { @@ -2099,7 +2199,7 @@ protected function propertyName(&$out) continue; } - if (!$parts && $this->match('[:.#]', $m, false)) { + if (! $parts && $this->match('[:.#]', $m, false)) { // css hacks $parts[] = $m[0]; continue; @@ -2110,7 +2210,7 @@ protected function propertyName(&$out) $this->eatWhiteDefault = $oldWhite; - if (!$parts) { + if (! $parts) { return false; } @@ -2142,24 +2242,24 @@ protected function propertyName(&$out) * * @return boolean */ - protected function selectors(&$out) + protected function selectors(&$out, $subSelector = false) { $s = $this->count; $selectors = []; - while ($this->selector($sel)) { + while ($this->selector($sel, $subSelector)) { $selectors[] = $sel; - if (! $this->matchChar(',')) { + if (! $this->matchChar(',', true)) { break; } - while ($this->matchChar(',')) { + while ($this->matchChar(',', true)) { ; // ignore extra } } - if (!$selectors) { + if (! $selectors) { $this->seek($s); return false; @@ -2177,23 +2277,23 @@ protected function selectors(&$out) * * @return boolean */ - protected function selector(&$out) + protected function selector(&$out, $subSelector = false) { $selector = []; for (;;) { - if ($this->match('[>+~]+', $m)) { + if ($this->match('[>+~]+', $m, true)) { $selector[] = [$m[0]]; continue; } - if ($this->selectorSingle($part)) { + if ($this->selectorSingle($part, $subSelector)) { $selector[] = $part; $this->match('\s+', $m); continue; } - if ($this->match('\/[^\/]+\/', $m)) { + if ($this->match('\/[^\/]+\/', $m, true)) { $selector[] = [$m[0]]; continue; } @@ -2201,11 +2301,12 @@ protected function selector(&$out) break; } - if (!$selector) { + if (! $selector) { return false; } $out = $selector; + return true; } @@ -2220,7 +2321,7 @@ protected function selector(&$out) * * @return boolean */ - protected function selectorSingle(&$out) + protected function selectorSingle(&$out, $subSelector = false) { $oldWhite = $this->eatWhiteDefault; $this->eatWhiteDefault = false; @@ -2244,16 +2345,23 @@ protected function selectorSingle(&$out) break; } + // parsing a sub selector in () stop with the closing ) + if ($subSelector && $char === ')') { + break; + } + //self switch ($char) { case '&': $parts[] = Compiler::$selfSelector; $this->count++; continue 2; + case '.': $parts[] = '.'; $this->count++; continue 2; + case '|': $parts[] = '|'; $this->count++; @@ -2307,19 +2415,48 @@ protected function selectorSingle(&$out) $ss = $this->count; - if ($this->matchChar('(') && - ($this->openString(')', $str, '(') || true) && - $this->matchChar(')') + if ($nameParts === ['not'] || $nameParts === ['is'] || + $nameParts === ['has'] || $nameParts === ['where'] ) { - $parts[] = '('; - - if (! empty($str)) { - $parts[] = $str; + if ($this->matchChar('(') && + ($this->selectors($subs, true) || true) && + $this->matchChar(')') + ) { + $parts[] = '('; + + while ($sub = array_shift($subs)) { + while ($ps = array_shift($sub)) { + foreach ($ps as &$p) { + $parts[] = $p; + } + if (count($sub) && reset($sub)) { + $parts[] = ' '; + } + } + if (count($subs) && reset($subs)) { + $parts[] = ', '; + } + } + + $parts[] = ')'; + } else { + $this->seek($ss); } - - $parts[] = ')'; } else { - $this->seek($ss); + if ($this->matchChar('(') && + ($this->openString(')', $str, '(') || true) && + $this->matchChar(')') + ) { + $parts[] = '('; + + if (! empty($str)) { + $parts[] = $str; + } + + $parts[] = ')'; + } else { + $this->seek($ss); + } } continue; @@ -2330,9 +2467,9 @@ protected function selectorSingle(&$out) // attribute selector if ($char === '[' && - $this->matchChar('[') && - ($this->openString(']', $str, '[') || true) && - $this->matchChar(']') + $this->matchChar('[') && + ($this->openString(']', $str, '[') || true) && + $this->matchChar(']') ) { $parts[] = '['; @@ -2341,7 +2478,6 @@ protected function selectorSingle(&$out) } $parts[] = ']'; - continue; } @@ -2363,7 +2499,7 @@ protected function selectorSingle(&$out) $this->eatWhiteDefault = $oldWhite; - if (!$parts) { + if (! $parts) { return false; } @@ -2406,7 +2542,7 @@ protected function keyword(&$word, $eatWhitespace = null) { if ($this->match( $this->utf8 - ? '(([\pL\w_\-\*!"\']|[\\\\].)([\pL\w\-_"\']|[\\\\].)*)' + ? '(([\pL\w\x{00A0}-\x{10FFFF}_\-\*!"\']|[\\\\].)([\pL\w\x{00A0}-\x{10FFFF}\-_"\']|[\\\\].)*)' : '(([\w_\-\*!"\']|[\\\\].)([\w\-_"\']|[\\\\].)*)', $m, $eatWhitespace diff --git a/src/SourceMap/SourceMapGenerator.php b/src/SourceMap/SourceMapGenerator.php index d2001462..fb11a0bf 100644 --- a/src/SourceMap/SourceMapGenerator.php +++ b/src/SourceMap/SourceMapGenerator.php @@ -137,7 +137,9 @@ public function saveMap($content) // directory does not exist if (! is_dir($dir)) { // FIXME: create the dir automatically? - throw new CompilerException(sprintf('The directory "%s" does not exist. Cannot save the source map.', $dir)); + throw new CompilerException( + sprintf('The directory "%s" does not exist. Cannot save the source map.', $dir) + ); } // FIXME: proper saving, with dir write check! diff --git a/src/Version.php b/src/Version.php index 19a6a02c..fe163a78 100644 --- a/src/Version.php +++ b/src/Version.php @@ -18,5 +18,5 @@ */ class Version { - const VERSION = 'v0.8.2'; + const VERSION = 'v0.8.4'; } diff --git a/tests/FailingTest.php b/tests/FailingTest.php index 86865049..95f693b2 100644 --- a/tests/FailingTest.php +++ b/tests/FailingTest.php @@ -62,56 +62,6 @@ public function provideFailing() { // @codingStandardsIgnoreStart return array( - array( - '#67 - weird @extend behavior', <<<'END_OF_SCSS' -.nav-bar { - background: #eee; - > .item { - margin: 0 10px; - } -} - - -header ul { - @extend .nav-bar; - > li { - @extend .item; - } -} -END_OF_SCSS - , << .item, header ul > .item, header ul > li { - margin: 0 10px; } -END_OF_EXPECTED - ), - array( - '#368 - self in selector', <<<'END_OF_SCSS' -.test { - &:last-child:not(+ &:first-child) { - padding-left: 10px; - } -} -END_OF_SCSS - , << p, \ +div > h1 \ +"; + +#{$foo} { + & > strong { + color: red; + } +} diff --git a/tests/inputs/operators.scss b/tests/inputs/operators.scss index 31e65b17..096aeb37 100644 --- a/tests/inputs/operators.scss +++ b/tests/inputs/operators.scss @@ -191,3 +191,37 @@ $gridRowWidth: 20px; { width: (2.5 / $gridRowWidth * 100px * 1% ); } + +$and: true and false; +$or: false or true; + +$and1: null and true; // (null) +$and2: true and null; // (null) +$and3: null and false; // (null) +$and4: false and null; // false +$and5: one and null; // (null) +$and6: null and one; // (null) + +$or1: null or true; // true +$or2: true or null; // true +$or3: null or false; // false +$or4: false or null; // (null) +$or5: one or null; // one +$or6: null or one; // one + +#bools-test { + a: $and; + b: $or; + c: $and1; + d: $and2; + e: $and3; + f: $and4; + g: $and5; + h: $and6; + n: $or1; + o: $or2; + p: $or3; + q: $or4; + r: $or5; + s: $or6; +} diff --git a/tests/inputs/selector_functions.scss b/tests/inputs/selector_functions.scss index e97336e2..258aa8f9 100644 --- a/tests/inputs/selector_functions.scss +++ b/tests/inputs/selector_functions.scss @@ -12,3 +12,137 @@ } } +.is-superselector { + content: is-superselector(".is-superselector", ".selector"); /* false */ +} +.is-superselector1 { + content: is-superselector("a", "a.disabled"); /*true*/ +} +.is-superselector2 { + content: is-superselector("a", "a:not(:visited)"); /*true*/ +} + +.is-superselector3 { + content: is-superselector("a", "a[href]"); /*true*/ +} + +.is-superselector4 { + content: is-superselector("a.disabled", "a"); /*false*/ +} + +.is-superselector5 { + content: is-superselector("a.disabled", "a.foo.disabled"); /* true*/ +} + +.is-superselector6 { + content: is-superselector("a.disabled", "a.foo#disabled"); /* false*/ +} + +.is-superselector7 { + content: is-superselector("a", "sidebar a"); /* true*/ +} +.is-superselector8 { + content: is-superselector("sidebar a", "a"); /* false*/ +} +.is-superselector9 { + content: is-superselector("a", "a"); /* true*/ +} + +#{selector-append("a", ".disabled")} { + content:'a.disabled'; +} + +#{selector-append(".accordion", "__copy")} { + content:'.accordion__copy'; +} + +#{selector-append(".accordion", "__copy, __image")} { + content:'.accordion__copy, .accordion__image'; +} + +#{selector-append(".accordion, .slider", "__copy, __image")} { + content:'.accordion__copy, .slider__copy, .accordion__image, .slider__image'; +} + +#{selector-extend("a.disabled", "a", ".link")} { + content: "a.disabled, .link.disabled"; +} +#{selector-extend("a.disabled", "h1", "h2")} { + content: "a.disabled"; +} + +#{selector-extend(".guide .info", ".info", ".content nav.sidebar")} { + content: ".guide .info, .guide .content nav.sidebar, .content .guide nav.sidebar"; +} + +#{selector-nest("ul", "li")} { + content: "ul li"; +} + +#{selector-nest(".alert, .warning", "p")} { + content: ".alert p, .warning p"; +} + +#{selector-nest(".alert", "&:hover")} { + content: ".alert:hover"; +} + +#{selector-nest(".accordion", "&__copy")} { + content: ".accordion__copy"; +} + +@each $selector in selector-parse(".main aside:hover, .sidebar p") { + #{$selector} { + @each $part in $selector { + content: "#{$part}"; + } + } +} + +#{selector-replace("a.disabled", "a", ".link")} { + content: ".link.disabled"; +} +#{selector-replace("a.disabled", "h1", "h2")} { + content: "a.disabled"; +} + +#{selector-replace(".guide .info", ".info", ".content nav.sidebar")} { + content: ".guide .content nav.sidebar, .content .guide nav.sidebar"; +} + +#{simple-selectors("a.disabled")} { + /* a, .disabled */ + @each $part in simple-selectors("a.disabled") { + content:"#{$part}"; + } +} +#{simple-selectors("main.blog:after")} { + /* main, .blog, :after */ + @each $part in simple-selectors("main.blog:after") { + content:"#{$part}"; + } +} + +#{selector-unify("a", ".disabled")} { + content:"a.disabled"; +} + +#{selector-unify("a.disabled", "a.outgoing")} { + content: "a.disabled.outgoing"; +} + +#{selector-unify("a", "h1")} { + content: "null"; +} + +#{selector-unify(".warning a", "main a")} { + content: ".warning main a, main .warning a"; +} + +#{selector-unify("main.warning a", "main a.disabled")} { + content: "main.warning a.disabled"; +} + +#{selector-unify("main .warning a", "main a")} { + content: "main .warning a"; +} diff --git a/tests/inputs/selectors.scss b/tests/inputs/selectors.scss index 36ae5a2b..0b9092c2 100644 --- a/tests/inputs/selectors.scss +++ b/tests/inputs/selectors.scss @@ -287,3 +287,13 @@ ul, ol { display: block; } } + +// unicode +.👤 { + display: inline-block; +} + +.❮ { + display:inline; + content:'↦'; +} diff --git a/tests/inputs/values.scss b/tests/inputs/values.scss index 8279447d..b4111ddb 100644 --- a/tests/inputs/values.scss +++ b/tests/inputs/values.scss @@ -6,6 +6,8 @@ width: 80%; color: "hello world"; height: url("http://google.com"); + background-image: url(//host/image.png +); dads: url(http://leafo.net); padding: 10px 10px 10px 10px, 3px 3px 3px; textblock: "This is a \ @@ -15,6 +17,7 @@ multiline block \ content: "This is a \ multiline string."; border-radius: -1px -1px -1px black; + grid-template-columns: [avatar] 2fr [body] 6fr; } #subtraction { @@ -46,3 +49,11 @@ multiline string."; $self2: &; content:'#{$self2}'; } + +@font-face { + unicode-range: U+26; + unicode-range: U+0-7F; + unicode-range: U+0025-00FF; + unicode-range: U+4??; + unicode-range: U+0025-00FF, U+4??; +} diff --git a/tests/outputs/at_root.css b/tests/outputs/at_root.css index c80f3594..6fe091bf 100644 --- a/tests/outputs/at_root.css +++ b/tests/outputs/at_root.css @@ -107,3 +107,7 @@ a.badge { .btn:hover, .btn:focus { text-decoration: none; } + +.other { + content: "bar"; + content: "foo"; } diff --git a/tests/outputs/comments.css b/tests/outputs/comments.css index f6f8d184..c66b30bf 100644 --- a/tests/outputs/comments.css +++ b/tests/outputs/comments.css @@ -30,3 +30,13 @@ a { /* comment 6 */ } /* comment 7 */ /* коммент */ +/* This is a comment with vars references: +color:red +and a fake #{ doing nothing +radius:2px + */ +.textimage-background-fullheight .background { + /* @noflip */ + display: block; + /* hop */ + width: 100%; } diff --git a/tests/outputs/interpolation.css b/tests/outputs/interpolation.css index d9fd10e5..1b61ba1f 100644 --- a/tests/outputs/interpolation.css +++ b/tests/outputs/interpolation.css @@ -77,3 +77,6 @@ a.badge { .element { border: 2px solid #000; } + +div > p > strong, div > h1 > strong { + color: red; } diff --git a/tests/outputs/operators.css b/tests/outputs/operators.css index 50aaa8c0..1ec77ea0 100644 --- a/tests/outputs/operators.css +++ b/tests/outputs/operators.css @@ -173,3 +173,13 @@ div { .foo { width: 12.5%; } + +#bools-test { + a: false; + b: true; + f: false; + n: true; + o: true; + p: false; + r: one; + s: one; } diff --git a/tests/outputs/selector_functions.css b/tests/outputs/selector_functions.css index d608dc9d..b071ef6b 100644 --- a/tests/outputs/selector_functions.css +++ b/tests/outputs/selector_functions.css @@ -7,3 +7,120 @@ .main aside:hover, .sidebar p { content: ".main aside:hover"; content: ".sidebar p"; } + +.is-superselector { + content: false; + /* false */ } + +.is-superselector1 { + content: true; + /*true*/ } + +.is-superselector2 { + content: true; + /*true*/ } + +.is-superselector3 { + content: true; + /*true*/ } + +.is-superselector4 { + content: false; + /*false*/ } + +.is-superselector5 { + content: true; + /* true*/ } + +.is-superselector6 { + content: false; + /* false*/ } + +.is-superselector7 { + content: true; + /* true*/ } + +.is-superselector8 { + content: false; + /* false*/ } + +.is-superselector9 { + content: true; + /* true*/ } + +a.disabled { + content: 'a.disabled'; } + +.accordion__copy { + content: '.accordion__copy'; } + +.accordion__copy, .accordion__image { + content: '.accordion__copy, .accordion__image'; } + +.accordion__copy, .slider__copy, .accordion__image, .slider__image { + content: '.accordion__copy, .slider__copy, .accordion__image, .slider__image'; } + +a.disabled, .link.disabled { + content: "a.disabled, .link.disabled"; } + +a.disabled { + content: "a.disabled"; } + +.guide .info, .guide .content nav.sidebar, .content .guide nav.sidebar { + content: ".guide .info, .guide .content nav.sidebar, .content .guide nav.sidebar"; } + +ul li { + content: "ul li"; } + +.alert p, .warning p { + content: ".alert p, .warning p"; } + +.alert:hover { + content: ".alert:hover"; } + +.accordion__copy { + content: ".accordion__copy"; } + .main aside:hover { + content: ".main"; + content: "aside:hover"; } + .sidebar p { + content: ".sidebar"; + content: "p"; } + +.link.disabled { + content: ".link.disabled"; } + +a.disabled { + content: "a.disabled"; } + +.guide .content nav.sidebar, .content .guide nav.sidebar { + content: ".guide .content nav.sidebar, .content .guide nav.sidebar"; } + +a, .disabled { + /* a, .disabled */ + content: "a"; + content: ".disabled"; } + +main, .blog, :after { + /* main, .blog, :after */ + content: "main"; + content: ".blog"; + content: ":after"; } + +a.disabled { + content: "a.disabled"; } + +a.disabled.outgoing { + content: "a.disabled.outgoing"; } + + { + content: "null"; } + +.warning main a, main .warning a { + content: ".warning main a, main .warning a"; } + +main.warning a.disabled { + content: "main.warning a.disabled"; } + +main .warning a { + content: "main .warning a"; } diff --git a/tests/outputs/selectors.css b/tests/outputs/selectors.css index 64d55926..66a6ab15 100644 --- a/tests/outputs/selectors.css +++ b/tests/outputs/selectors.css @@ -370,3 +370,10 @@ span a, p a, div a { ul ul, ul ol, ol ul, ol ol { display: block; } + +.👤 { + display: inline-block; } + +.❮ { + display: inline; + content: '↦'; } diff --git a/tests/outputs/values.css b/tests/outputs/values.css index d6937374..473ce76b 100644 --- a/tests/outputs/values.css +++ b/tests/outputs/values.css @@ -5,15 +5,14 @@ width: 80%; color: "hello world"; height: url("http://google.com"); + background-image: url(//host/image.png); dads: url(http://leafo.net); padding: 10px 10px 10px 10px, 3px 3px 3px; - textblock: "This is a \ -multiline block \ -#not { color: #eee;}"; + textblock: "This is a multiline block #not { color: #eee;}"; margin: 4, 3, 1; - content: "This is a \ -multiline string."; - border-radius: -1px -1px -1px black; } + content: "This is a multiline string."; + border-radius: -1px -1px -1px black; + grid-template-columns: [avatar] 2fr [body] 6fr; } #subtraction { lit: 10 -11; @@ -35,3 +34,10 @@ multiline string."; #self { content: "#self"; } + +@font-face { + unicode-range: U+26; + unicode-range: U+0-7F; + unicode-range: U+0025-00FF; + unicode-range: U+4??; + unicode-range: U+0025-00FF, U+4??; } diff --git a/tests/outputs_numbered/at_root.css b/tests/outputs_numbered/at_root.css index d57926ab..84d79764 100644 --- a/tests/outputs_numbered/at_root.css +++ b/tests/outputs_numbered/at_root.css @@ -160,3 +160,9 @@ a.badge:hover, a.badge:focus { .btn:hover, .btn:focus { text-decoration: none; } +/* line 226, inputs/at_root.scss */ +/* line 229, inputs/at_root.scss */ + +.other { + content: "bar"; + content: "foo"; } diff --git a/tests/outputs_numbered/comments.css b/tests/outputs_numbered/comments.css index 3669d1d8..f7256abb 100644 --- a/tests/outputs_numbered/comments.css +++ b/tests/outputs_numbered/comments.css @@ -32,3 +32,15 @@ a { /* comment 6 */ } /* comment 7 */ /* коммент */ +/* This is a comment with vars references: +color:red +and a fake #{ doing nothing +radius:2px + */ +/* line 60, inputs/comments.scss */ +/* line 61, inputs/comments.scss */ + .textimage-background-fullheight .background { + /* @noflip */ + display: block; + /* hop */ + width: 100%; } diff --git a/tests/outputs_numbered/interpolation.css b/tests/outputs_numbered/interpolation.css index da509491..256dde01 100644 --- a/tests/outputs_numbered/interpolation.css +++ b/tests/outputs_numbered/interpolation.css @@ -96,3 +96,8 @@ a.badge { /* line 140, inputs/interpolation.scss */ .element { border: 2px solid #000; } +/* line 149, inputs/interpolation.scss */ +/* line 150, inputs/interpolation.scss */ + +div > p > strong, div > h1 > strong { + color: red; } diff --git a/tests/outputs_numbered/operators.css b/tests/outputs_numbered/operators.css index ad4d678e..9a956a2c 100644 --- a/tests/outputs_numbered/operators.css +++ b/tests/outputs_numbered/operators.css @@ -174,3 +174,13 @@ div { /* line 190, inputs/operators.scss */ .foo { width: 12.5%; } +/* line 212, inputs/operators.scss */ +#bools-test { + a: false; + b: true; + f: false; + n: true; + o: true; + p: false; + r: one; + s: one; } diff --git a/tests/outputs_numbered/selector_functions.css b/tests/outputs_numbered/selector_functions.css index 7bd5f1b4..c036813c 100644 --- a/tests/outputs_numbered/selector_functions.css +++ b/tests/outputs_numbered/selector_functions.css @@ -8,3 +8,122 @@ .main aside:hover, .sidebar p { content: ".main aside:hover"; content: ".sidebar p"; } +/* line 15, inputs/selector_functions.scss */ +.is-superselector { + content: false; + /* false */ } +/* line 18, inputs/selector_functions.scss */ +.is-superselector1 { + content: true; + /*true*/ } +/* line 21, inputs/selector_functions.scss */ +.is-superselector2 { + content: true; + /*true*/ } +/* line 25, inputs/selector_functions.scss */ +.is-superselector3 { + content: true; + /*true*/ } +/* line 29, inputs/selector_functions.scss */ +.is-superselector4 { + content: false; + /*false*/ } +/* line 33, inputs/selector_functions.scss */ +.is-superselector5 { + content: true; + /* true*/ } +/* line 37, inputs/selector_functions.scss */ +.is-superselector6 { + content: false; + /* false*/ } +/* line 41, inputs/selector_functions.scss */ +.is-superselector7 { + content: true; + /* true*/ } +/* line 44, inputs/selector_functions.scss */ +.is-superselector8 { + content: false; + /* false*/ } +/* line 47, inputs/selector_functions.scss */ +.is-superselector9 { + content: true; + /* true*/ } +/* line 51, inputs/selector_functions.scss */ +a.disabled { + content: 'a.disabled'; } +/* line 55, inputs/selector_functions.scss */ +.accordion__copy { + content: '.accordion__copy'; } +/* line 59, inputs/selector_functions.scss */ +.accordion__copy, .accordion__image { + content: '.accordion__copy, .accordion__image'; } +/* line 63, inputs/selector_functions.scss */ +.accordion__copy, .slider__copy, .accordion__image, .slider__image { + content: '.accordion__copy, .slider__copy, .accordion__image, .slider__image'; } +/* line 67, inputs/selector_functions.scss */ +a.disabled, .link.disabled { + content: "a.disabled, .link.disabled"; } +/* line 70, inputs/selector_functions.scss */ +a.disabled { + content: "a.disabled"; } +/* line 74, inputs/selector_functions.scss */ +.guide .info, .guide .content nav.sidebar, .content .guide nav.sidebar { + content: ".guide .info, .guide .content nav.sidebar, .content .guide nav.sidebar"; } +/* line 78, inputs/selector_functions.scss */ +ul li { + content: "ul li"; } +/* line 82, inputs/selector_functions.scss */ +.alert p, .warning p { + content: ".alert p, .warning p"; } +/* line 86, inputs/selector_functions.scss */ +.alert:hover { + content: ".alert:hover"; } +/* line 90, inputs/selector_functions.scss */ +.accordion__copy { + content: ".accordion__copy"; } +/* line 95, inputs/selector_functions.scss */ +.main aside:hover { + content: ".main"; + content: "aside:hover"; } +/* line 95, inputs/selector_functions.scss */ +.sidebar p { + content: ".sidebar"; + content: "p"; } +/* line 102, inputs/selector_functions.scss */ +.link.disabled { + content: ".link.disabled"; } +/* line 105, inputs/selector_functions.scss */ +a.disabled { + content: "a.disabled"; } +/* line 109, inputs/selector_functions.scss */ +.guide .content nav.sidebar, .content .guide nav.sidebar { + content: ".guide .content nav.sidebar, .content .guide nav.sidebar"; } +/* line 113, inputs/selector_functions.scss */ +a, .disabled { + /* a, .disabled */ + content: "a"; + content: ".disabled"; } +/* line 119, inputs/selector_functions.scss */ +main, .blog, :after { + /* main, .blog, :after */ + content: "main"; + content: ".blog"; + content: ":after"; } +/* line 126, inputs/selector_functions.scss */ +a.disabled { + content: "a.disabled"; } +/* line 130, inputs/selector_functions.scss */ +a.disabled.outgoing { + content: "a.disabled.outgoing"; } +/* line 134, inputs/selector_functions.scss */ + { + content: "null"; } +/* line 138, inputs/selector_functions.scss */ +.warning main a, main .warning a { + content: ".warning main a, main .warning a"; } +/* line 142, inputs/selector_functions.scss */ +main.warning a.disabled { + content: "main.warning a.disabled"; } +/* line 146, inputs/selector_functions.scss */ +main .warning a { + content: "main .warning a"; } diff --git a/tests/outputs_numbered/selectors.css b/tests/outputs_numbered/selectors.css index d2572233..7b6a8571 100644 --- a/tests/outputs_numbered/selectors.css +++ b/tests/outputs_numbered/selectors.css @@ -407,3 +407,10 @@ span a, p a, div a { ul ul, ul ol, ol ul, ol ol { display: block; } +/* line 292, inputs/selectors.scss */ +.👤 { + display: inline-block; } +/* line 296, inputs/selectors.scss */ +.❮ { + display: inline; + content: '↦'; } diff --git a/tests/outputs_numbered/values.css b/tests/outputs_numbered/values.css index af231709..e218a64e 100644 --- a/tests/outputs_numbered/values.css +++ b/tests/outputs_numbered/values.css @@ -6,16 +6,15 @@ width: 80%; color: "hello world"; height: url("http://google.com"); + background-image: url(//host/image.png); dads: url(http://leafo.net); padding: 10px 10px 10px 10px, 3px 3px 3px; - textblock: "This is a \ -multiline block \ -#not { color: #eee;}"; + textblock: "This is a multiline block #not { color: #eee;}"; margin: 4, 3, 1; - content: "This is a \ -multiline string."; - border-radius: -1px -1px -1px black; } -/* line 20, inputs/values.scss */ + content: "This is a multiline string."; + border-radius: -1px -1px -1px black; + grid-template-columns: [avatar] 2fr [body] 6fr; } +/* line 23, inputs/values.scss */ #subtraction { lit: 10 -11; lit: -1; @@ -25,14 +24,21 @@ multiline string."; var: -90; var: -90; var: -90; } -/* line 34, inputs/values.scss */ +/* line 37, inputs/values.scss */ #special { a: 12px expression(1 + (3 / Foo.bar("baz" + "bang") + function() {return 12;}) % 12); } -/* line 38, inputs/values.scss */ +/* line 41, inputs/values.scss */ #unary { b: 0.5em; c: -foo(12px); d: +foo(12px); } -/* line 44, inputs/values.scss */ +/* line 47, inputs/values.scss */ #self { content: "#self"; } + +@font-face { + unicode-range: U+26; + unicode-range: U+0-7F; + unicode-range: U+0025-00FF; + unicode-range: U+4??; + unicode-range: U+0025-00FF, U+4??; } diff --git a/tests/scss_test.rb b/tests/scss_test.rb index 3d849d8a..41fa8e75 100644 --- a/tests/scss_test.rb +++ b/tests/scss_test.rb @@ -1,4 +1,3 @@ -#!/usr/bin/env ruby # -*- coding: utf-8 -*- require File.dirname(__FILE__) + '/test_helper' @@ -1015,7 +1014,8 @@ def test_function_args end def test_disallowed_function_names - assert_warning(<; } SCSS assert(false, "Expected syntax error") rescue Sass::SyntaxError => e - assert_equal 'Invalid CSS after " *bar:baz ": expected ";", was "[fail]; }"', e.message + assert_equal 'Invalid CSS after " *bar:baz ": expected expression (e.g. 1px, bold), was "; }"', e.message assert_equal 2, e.sass_line end def test_uses_property_exception_with_colon_hack render <; } SCSS assert(false, "Expected syntax error") rescue Sass::SyntaxError => e - assert_equal 'Invalid CSS after " :bar:baz ": expected ";", was "[fail]; }"', e.message + assert_equal 'Invalid CSS after " :bar:baz ": expected expression (e.g. 1px, bold), was "; }"', e.message assert_equal 2, e.sass_line end @@ -3420,22 +3421,22 @@ def test_uses_rule_exception_with_dot_hack def test_uses_property_exception_with_space_after_name render <; } SCSS assert(false, "Expected syntax error") rescue Sass::SyntaxError => e - assert_equal 'Invalid CSS after " bar: baz ": expected ";", was "[fail]; }"', e.message + assert_equal 'Invalid CSS after " bar: baz ": expected expression (e.g. 1px, bold), was "; }"', e.message assert_equal 2, e.sass_line end def test_uses_property_exception_with_non_identifier_after_name render <; } SCSS assert(false, "Expected syntax error") rescue Sass::SyntaxError => e - assert_equal 'Invalid CSS after " bar:1px ": expected ";", was "[fail]; }"', e.message + assert_equal 'Invalid CSS after " bar:1px ": expected expression (e.g. 1px, bold), was "; }"', e.message assert_equal 2, e.sass_line end @@ -3649,6 +3650,40 @@ def test_raw_newline_warning # Regression + # Regression test for #2031. + def test_no_interpolation_warning_in_nested_selector + assert_no_warning {assert_equal(<