diff --git a/CHANGELOG.md b/CHANGELOG.md index efc00c0c..870ead9f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,8 @@ # v1.10.15 ## mm/dd/2021 +1. [](#new) + * Updated SCSS compiler to v1.5 1. [](#improved) * Updated node modules dev dependencies * Package.json scripts cleanup diff --git a/classes/plugin/AdminController.php b/classes/plugin/AdminController.php index b60c68c6..f14097a9 100644 --- a/classes/plugin/AdminController.php +++ b/classes/plugin/AdminController.php @@ -702,7 +702,7 @@ class AdminController extends AdminBaseController $this->admin->setMessage($this->admin::translate('PLUGIN_ADMIN.SUCCESSFULLY_CHANGED_THEME'), 'info'); Cache::clearCache('invalidate'); - + $this->post = ['_redirect' => 'themes/' . $name ]; return true; diff --git a/composer.lock b/composer.lock index be1227c8..f8b96334 100644 --- a/composer.lock +++ b/composer.lock @@ -188,16 +188,16 @@ }, { "name": "scssphp/scssphp", - "version": "v1.4.1", + "version": "v1.5.2", "source": { "type": "git", "url": "https://github.com/scssphp/scssphp.git", - "reference": "ba86c963b94ec7ebd6e19d90cdab90d89667dbf7" + "reference": "6fe16f169f55f5e793474fb210aac0a4481619e5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/scssphp/scssphp/zipball/ba86c963b94ec7ebd6e19d90cdab90d89667dbf7", - "reference": "ba86c963b94ec7ebd6e19d90cdab90d89667dbf7", + "url": "https://api.github.com/repos/scssphp/scssphp/zipball/6fe16f169f55f5e793474fb210aac0a4481619e5", + "reference": "6fe16f169f55f5e793474fb210aac0a4481619e5", "shasum": "" }, "require": { @@ -206,13 +206,19 @@ "php": ">=5.6.0" }, "require-dev": { + "bamarni/composer-bin-plugin": "^1.4", "phpunit/phpunit": "^5.7 || ^6.5 || ^7.5 || ^8.3 || ^9.4", - "sass/sass-spec": "2020.12.29", + "sass/sass-spec": "*", "squizlabs/php_codesniffer": "~3.5", "symfony/phpunit-bridge": "^5.1", - "twbs/bootstrap": "~4.3", + "twbs/bootstrap": "~5.0", + "twbs/bootstrap4": "4.6.0", "zurb/foundation": "~6.5" }, + "suggest": { + "ext-iconv": "Can be used as fallback when ext-mbstring is not available", + "ext-mbstring": "For best performance, mbstring should be installed as it is faster than ext-iconv" + }, "bin": [ "bin/pscss" ], @@ -249,9 +255,9 @@ ], "support": { "issues": "https://github.com/scssphp/scssphp/issues", - "source": "https://github.com/scssphp/scssphp/tree/v1.4.1" + "source": "https://github.com/scssphp/scssphp/tree/v1.5.2" }, - "time": "2021-01-04T13:23:23+00:00" + "time": "2021-05-18T00:05:58+00:00" } ], "packages-dev": [ @@ -814,16 +820,16 @@ }, { "name": "guzzlehttp/psr7", - "version": "1.8.1", + "version": "1.8.2", "source": { "type": "git", "url": "https://github.com/guzzle/psr7.git", - "reference": "35ea11d335fd638b5882ff1725228b3d35496ab1" + "reference": "dc960a912984efb74d0a90222870c72c87f10c91" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/psr7/zipball/35ea11d335fd638b5882ff1725228b3d35496ab1", - "reference": "35ea11d335fd638b5882ff1725228b3d35496ab1", + "url": "https://api.github.com/repos/guzzle/psr7/zipball/dc960a912984efb74d0a90222870c72c87f10c91", + "reference": "dc960a912984efb74d0a90222870c72c87f10c91", "shasum": "" }, "require": { @@ -883,9 +889,9 @@ ], "support": { "issues": "https://github.com/guzzle/psr7/issues", - "source": "https://github.com/guzzle/psr7/tree/1.8.1" + "source": "https://github.com/guzzle/psr7/tree/1.8.2" }, - "time": "2021-03-21T16:25:00+00:00" + "time": "2021-04-26T09:17:50+00:00" }, { "name": "myclabs/deep-copy", @@ -2470,16 +2476,16 @@ }, { "name": "symfony/browser-kit", - "version": "v4.4.20", + "version": "v4.4.22", "source": { "type": "git", "url": "https://github.com/symfony/browser-kit.git", - "reference": "cfa8d92f95294747e3abc04969efee51ed374424" + "reference": "4c8b42b4aae93517e8f67d68c5cbe69413e3e3c1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/browser-kit/zipball/cfa8d92f95294747e3abc04969efee51ed374424", - "reference": "cfa8d92f95294747e3abc04969efee51ed374424", + "url": "https://api.github.com/repos/symfony/browser-kit/zipball/4c8b42b4aae93517e8f67d68c5cbe69413e3e3c1", + "reference": "4c8b42b4aae93517e8f67d68c5cbe69413e3e3c1", "shasum": "" }, "require": { @@ -2521,7 +2527,7 @@ "description": "Simulates the behavior of a web browser, allowing you to make requests, click on links and submit forms programmatically", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/browser-kit/tree/v4.4.20" + "source": "https://github.com/symfony/browser-kit/tree/v4.4.22" }, "funding": [ { @@ -2537,20 +2543,20 @@ "type": "tidelift" } ], - "time": "2021-02-18T10:52:56+00:00" + "time": "2021-04-08T07:40:10+00:00" }, { "name": "symfony/console", - "version": "v4.4.21", + "version": "v4.4.23", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "1ba4560dbbb9fcf5ae28b61f71f49c678086cf23" + "reference": "1ab187ac21d41d7d34a4f529091a1f5d0bb2924f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/1ba4560dbbb9fcf5ae28b61f71f49c678086cf23", - "reference": "1ba4560dbbb9fcf5ae28b61f71f49c678086cf23", + "url": "https://api.github.com/repos/symfony/console/zipball/1ab187ac21d41d7d34a4f529091a1f5d0bb2924f", + "reference": "1ab187ac21d41d7d34a4f529091a1f5d0bb2924f", "shasum": "" }, "require": { @@ -2610,7 +2616,7 @@ "description": "Eases the creation of beautiful and testable command line interfaces", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/console/tree/v4.4.21" + "source": "https://github.com/symfony/console/tree/v4.4.23" }, "funding": [ { @@ -2626,20 +2632,20 @@ "type": "tidelift" } ], - "time": "2021-03-26T09:23:24+00:00" + "time": "2021-05-10T12:53:15+00:00" }, { "name": "symfony/css-selector", - "version": "v4.4.20", + "version": "v4.4.22", "source": { "type": "git", "url": "https://github.com/symfony/css-selector.git", - "reference": "f907d3e53ecb2a5fad8609eb2f30525287a734c8" + "reference": "01c77324d1d47efbfd7891f62a7c256c69330115" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/css-selector/zipball/f907d3e53ecb2a5fad8609eb2f30525287a734c8", - "reference": "f907d3e53ecb2a5fad8609eb2f30525287a734c8", + "url": "https://api.github.com/repos/symfony/css-selector/zipball/01c77324d1d47efbfd7891f62a7c256c69330115", + "reference": "01c77324d1d47efbfd7891f62a7c256c69330115", "shasum": "" }, "require": { @@ -2675,7 +2681,7 @@ "description": "Converts CSS selectors to XPath expressions", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/css-selector/tree/v4.4.20" + "source": "https://github.com/symfony/css-selector/tree/v4.4.22" }, "funding": [ { @@ -2691,7 +2697,7 @@ "type": "tidelift" } ], - "time": "2021-01-27T09:09:26+00:00" + "time": "2021-04-07T15:47:03+00:00" }, { "name": "symfony/dom-crawler", @@ -2930,16 +2936,16 @@ }, { "name": "symfony/finder", - "version": "v4.4.20", + "version": "v4.4.23", "source": { "type": "git", "url": "https://github.com/symfony/finder.git", - "reference": "2543795ab1570df588b9bbd31e1a2bd7037b94f6" + "reference": "67b77716f517e3f864759232e1201e7aa2ab0e82" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/finder/zipball/2543795ab1570df588b9bbd31e1a2bd7037b94f6", - "reference": "2543795ab1570df588b9bbd31e1a2bd7037b94f6", + "url": "https://api.github.com/repos/symfony/finder/zipball/67b77716f517e3f864759232e1201e7aa2ab0e82", + "reference": "67b77716f517e3f864759232e1201e7aa2ab0e82", "shasum": "" }, "require": { @@ -2971,7 +2977,7 @@ "description": "Finds files and directories via an intuitive fluent interface", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/finder/tree/v4.4.20" + "source": "https://github.com/symfony/finder/tree/v4.4.23" }, "funding": [ { @@ -2987,7 +2993,7 @@ "type": "tidelift" } ], - "time": "2021-02-12T10:48:09+00:00" + "time": "2021-05-09T09:13:09+00:00" }, { "name": "symfony/polyfill-ctype", @@ -3404,16 +3410,16 @@ }, { "name": "symfony/process", - "version": "v4.4.20", + "version": "v4.4.22", "source": { "type": "git", "url": "https://github.com/symfony/process.git", - "reference": "7e950b6366d4da90292c2e7fa820b3c1842b965a" + "reference": "f5481b22729d465acb1cea3455fc04ce84b0148b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/process/zipball/7e950b6366d4da90292c2e7fa820b3c1842b965a", - "reference": "7e950b6366d4da90292c2e7fa820b3c1842b965a", + "url": "https://api.github.com/repos/symfony/process/zipball/f5481b22729d465acb1cea3455fc04ce84b0148b", + "reference": "f5481b22729d465acb1cea3455fc04ce84b0148b", "shasum": "" }, "require": { @@ -3445,7 +3451,7 @@ "description": "Executes commands in sub-processes", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/process/tree/v4.4.20" + "source": "https://github.com/symfony/process/tree/v4.4.22" }, "funding": [ { @@ -3461,25 +3467,25 @@ "type": "tidelift" } ], - "time": "2021-01-27T09:09:26+00:00" + "time": "2021-04-07T16:22:29+00:00" }, { "name": "symfony/service-contracts", - "version": "v2.2.0", + "version": "v2.4.0", "source": { "type": "git", "url": "https://github.com/symfony/service-contracts.git", - "reference": "d15da7ba4957ffb8f1747218be9e1a121fd298a1" + "reference": "f040a30e04b57fbcc9c6cbcf4dbaa96bd318b9bb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/service-contracts/zipball/d15da7ba4957ffb8f1747218be9e1a121fd298a1", - "reference": "d15da7ba4957ffb8f1747218be9e1a121fd298a1", + "url": "https://api.github.com/repos/symfony/service-contracts/zipball/f040a30e04b57fbcc9c6cbcf4dbaa96bd318b9bb", + "reference": "f040a30e04b57fbcc9c6cbcf4dbaa96bd318b9bb", "shasum": "" }, "require": { "php": ">=7.2.5", - "psr/container": "^1.0" + "psr/container": "^1.1" }, "suggest": { "symfony/service-implementation": "" @@ -3487,7 +3493,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "2.2-dev" + "dev-main": "2.4-dev" }, "thanks": { "name": "symfony/contracts", @@ -3524,7 +3530,7 @@ "standards" ], "support": { - "source": "https://github.com/symfony/service-contracts/tree/master" + "source": "https://github.com/symfony/service-contracts/tree/v2.4.0" }, "funding": [ { @@ -3540,20 +3546,20 @@ "type": "tidelift" } ], - "time": "2020-09-07T11:33:47+00:00" + "time": "2021-04-01T10:43:52+00:00" }, { "name": "symfony/yaml", - "version": "v4.4.21", + "version": "v4.4.22", "source": { "type": "git", "url": "https://github.com/symfony/yaml.git", - "reference": "3871c720871029f008928244e56cf43497da7e9d" + "reference": "1c2fd24147961525eaefb65b11987cab75adab59" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/yaml/zipball/3871c720871029f008928244e56cf43497da7e9d", - "reference": "3871c720871029f008928244e56cf43497da7e9d", + "url": "https://api.github.com/repos/symfony/yaml/zipball/1c2fd24147961525eaefb65b11987cab75adab59", + "reference": "1c2fd24147961525eaefb65b11987cab75adab59", "shasum": "" }, "require": { @@ -3595,7 +3601,7 @@ "description": "Loads and dumps YAML files", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/yaml/tree/v4.4.21" + "source": "https://github.com/symfony/yaml/tree/v4.4.22" }, "funding": [ { @@ -3611,7 +3617,7 @@ "type": "tidelift" } ], - "time": "2021-03-05T17:58:50+00:00" + "time": "2021-04-23T12:09:37+00:00" }, { "name": "theseer/tokenizer", diff --git a/vendor/composer/InstalledVersions.php b/vendor/composer/InstalledVersions.php index 8c65da1f..b70bdc63 100644 --- a/vendor/composer/InstalledVersions.php +++ b/vendor/composer/InstalledVersions.php @@ -29,7 +29,7 @@ private static $installed = array ( 'aliases' => array ( ), - 'reference' => '1acb94e85744589877484a13af715394b3558bd2', + 'reference' => '6e5839ded659cc193765e47cfdb9ccfeb31d8050', 'name' => 'getgrav/grav-plugin-admin', ), 'versions' => @@ -41,7 +41,7 @@ private static $installed = array ( 'aliases' => array ( ), - 'reference' => '1acb94e85744589877484a13af715394b3558bd2', + 'reference' => '6e5839ded659cc193765e47cfdb9ccfeb31d8050', ), 'laminas/laminas-xml' => array ( @@ -79,12 +79,12 @@ private static $installed = array ( ), 'scssphp/scssphp' => array ( - 'pretty_version' => 'v1.4.1', - 'version' => '1.4.1.0', + 'pretty_version' => 'v1.5.2', + 'version' => '1.5.2.0', 'aliases' => array ( ), - 'reference' => 'ba86c963b94ec7ebd6e19d90cdab90d89667dbf7', + 'reference' => '6fe16f169f55f5e793474fb210aac0a4481619e5', ), 'symfony/polyfill-php72' => array ( diff --git a/vendor/composer/installed.json b/vendor/composer/installed.json index 37706b14..19869b38 100644 --- a/vendor/composer/installed.json +++ b/vendor/composer/installed.json @@ -191,17 +191,17 @@ }, { "name": "scssphp/scssphp", - "version": "v1.4.1", - "version_normalized": "1.4.1.0", + "version": "v1.5.2", + "version_normalized": "1.5.2.0", "source": { "type": "git", "url": "https://github.com/scssphp/scssphp.git", - "reference": "ba86c963b94ec7ebd6e19d90cdab90d89667dbf7" + "reference": "6fe16f169f55f5e793474fb210aac0a4481619e5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/scssphp/scssphp/zipball/ba86c963b94ec7ebd6e19d90cdab90d89667dbf7", - "reference": "ba86c963b94ec7ebd6e19d90cdab90d89667dbf7", + "url": "https://api.github.com/repos/scssphp/scssphp/zipball/6fe16f169f55f5e793474fb210aac0a4481619e5", + "reference": "6fe16f169f55f5e793474fb210aac0a4481619e5", "shasum": "" }, "require": { @@ -210,14 +210,20 @@ "php": ">=5.6.0" }, "require-dev": { + "bamarni/composer-bin-plugin": "^1.4", "phpunit/phpunit": "^5.7 || ^6.5 || ^7.5 || ^8.3 || ^9.4", - "sass/sass-spec": "2020.12.29", + "sass/sass-spec": "*", "squizlabs/php_codesniffer": "~3.5", "symfony/phpunit-bridge": "^5.1", - "twbs/bootstrap": "~4.3", + "twbs/bootstrap": "~5.0", + "twbs/bootstrap4": "4.6.0", "zurb/foundation": "~6.5" }, - "time": "2021-01-04T13:23:23+00:00", + "suggest": { + "ext-iconv": "Can be used as fallback when ext-mbstring is not available", + "ext-mbstring": "For best performance, mbstring should be installed as it is faster than ext-iconv" + }, + "time": "2021-05-18T00:05:58+00:00", "bin": [ "bin/pscss" ], @@ -255,7 +261,7 @@ ], "support": { "issues": "https://github.com/scssphp/scssphp/issues", - "source": "https://github.com/scssphp/scssphp/tree/v1.4.1" + "source": "https://github.com/scssphp/scssphp/tree/v1.5.2" }, "install-path": "../scssphp/scssphp" } diff --git a/vendor/composer/installed.php b/vendor/composer/installed.php index a5eb8233..29c73405 100644 --- a/vendor/composer/installed.php +++ b/vendor/composer/installed.php @@ -6,7 +6,7 @@ 'aliases' => array ( ), - 'reference' => '1acb94e85744589877484a13af715394b3558bd2', + 'reference' => '6e5839ded659cc193765e47cfdb9ccfeb31d8050', 'name' => 'getgrav/grav-plugin-admin', ), 'versions' => @@ -18,7 +18,7 @@ 'aliases' => array ( ), - 'reference' => '1acb94e85744589877484a13af715394b3558bd2', + 'reference' => '6e5839ded659cc193765e47cfdb9ccfeb31d8050', ), 'laminas/laminas-xml' => array ( @@ -56,12 +56,12 @@ ), 'scssphp/scssphp' => array ( - 'pretty_version' => 'v1.4.1', - 'version' => '1.4.1.0', + 'pretty_version' => 'v1.5.2', + 'version' => '1.5.2.0', 'aliases' => array ( ), - 'reference' => 'ba86c963b94ec7ebd6e19d90cdab90d89667dbf7', + 'reference' => '6fe16f169f55f5e793474fb210aac0a4481619e5', ), 'symfony/polyfill-php72' => array ( diff --git a/vendor/scssphp/scssphp/README.md b/vendor/scssphp/scssphp/README.md index 54557344..65bb93ea 100644 --- a/vendor/scssphp/scssphp/README.md +++ b/vendor/scssphp/scssphp/README.md @@ -45,3 +45,27 @@ To enable the full `sass-spec` compatibility tests: Run the following command from the root directory to check the code for "sniffs". vendor/bin/phpcs --standard=PSR12 --extensions=php bin src tests *.php + +## Static Analysis + +`scssphp` uses [phpstan](https://phpstan.org/) for static analysis. + +Run the following command from the root directory to analyse the codebase: + + make phpstan + +As most of the codebase is composed of legacy code which cannot be type-checked +fully, the setup contains a baseline file with all errors we want to ignore. In +particular, we ignore all errors related to not specifying the types inside arrays +when these arrays correspond to the representation of Sass values and Sass AST nodes +in the parser and compiler. +When contributing, the proper process to deal with static analysis is the following: + +1. Make your change in the codebase +2. Run `make phpstan` +3. Fix errors reported by phpstan when possible +4. Repeat step 2 and 3 until nothing gets fixed anymore at step 3 +5. Run `make phpstan-baseline` to regenerate the phpstan baseline + +Additions to the baseline will be reviewed to avoid ignoring errors that should have +been fixed. diff --git a/vendor/scssphp/scssphp/bin/pscss b/vendor/scssphp/scssphp/bin/pscss index 8b2e4660..18c136ac 100755 --- a/vendor/scssphp/scssphp/bin/pscss +++ b/vendor/scssphp/scssphp/bin/pscss @@ -32,6 +32,8 @@ $inputFile = null; $changeDir = false; $encoding = false; $sourceMap = false; +$embedSources = false; +$embedSourceMap = false; /** * Parse argument @@ -60,24 +62,28 @@ function parseArgument(&$i, $options) { } } +$arguments = []; + for ($i = 1; $i < $argc; $i++) { if ($argv[$i] === '-?' || $argv[$i] === '-h' || $argv[$i] === '--help') { $exe = $argv[0]; $HELP = <<parse($data)), true)); + fwrite(STDERR, 'Warning: the --dump-tree option is deprecated. Use proper debugging tools instead.'); + exit(); } @@ -184,8 +200,26 @@ if ($style) { } } +$outputFile = isset($arguments[1]) ? $arguments[1] : null; +$sourceMapFile = null; + if ($sourceMap) { - $scss->setSourceMap(Compiler::SOURCE_MAP_INLINE); + $sourceMapOptions = array( + 'outputSourceFiles' => $embedSources, + ); + if ($embedSourceMap || $outputFile === null) { + $scss->setSourceMap(Compiler::SOURCE_MAP_INLINE); + } else { + $sourceMapFile = $outputFile . '.map'; + $sourceMapOptions['sourceMapWriteTo'] = $sourceMapFile; + $sourceMapOptions['sourceMapURL'] = basename($sourceMapFile); + $sourceMapOptions['sourceMapBasepath'] = getcwd(); + $sourceMapOptions['sourceMapFilename'] = basename($outputFile); + + $scss->setSourceMap(Compiler::SOURCE_MAP_FILE); + } + + $scss->setSourceMapOptions($sourceMapOptions); } if ($encoding) { @@ -193,8 +227,18 @@ if ($encoding) { } try { - echo $scss->compile($data, $inputFile); + $result = $scss->compileString($data, $inputFile); } catch (SassException $e) { - fwrite(STDERR, $e->getMessage()."\n"); + fwrite(STDERR, 'Error: '.$e->getMessage()."\n"); exit(1); } + +if ($outputFile) { + file_put_contents($outputFile, $result->getCss()); + + if ($sourceMapFile !== null && $result->getSourceMap() !== null) { + file_put_contents($sourceMapFile, $result->getSourceMap()); + } +} else { + echo $result->getCss(); +} diff --git a/vendor/scssphp/scssphp/composer.json b/vendor/scssphp/scssphp/composer.json index e4c47d34..faa40432 100644 --- a/vendor/scssphp/scssphp/composer.json +++ b/vendor/scssphp/scssphp/composer.json @@ -30,12 +30,18 @@ "ext-json": "*", "ext-ctype": "*" }, + "suggest": { + "ext-mbstring": "For best performance, mbstring should be installed as it is faster than ext-iconv", + "ext-iconv": "Can be used as fallback when ext-mbstring is not available" + }, "require-dev": { + "bamarni/composer-bin-plugin": "^1.4", "phpunit/phpunit": "^5.7 || ^6.5 || ^7.5 || ^8.3 || ^9.4", - "sass/sass-spec": "2020.12.29", + "sass/sass-spec": "*", "squizlabs/php_codesniffer": "~3.5", "symfony/phpunit-bridge": "^5.1", - "twbs/bootstrap": "~4.3", + "twbs/bootstrap": "~5.0", + "twbs/bootstrap4": "4.6.0", "zurb/foundation": "~6.5" }, "repositories": [ @@ -43,16 +49,34 @@ "type": "package", "package": { "name": "sass/sass-spec", - "version": "2020.12.29", + "version": "2021.05.10", "source": { "type": "git", "url": "https://github.com/sass/sass-spec.git", - "reference": "d975d33146fb679a6b359ceca329012f02e4a794" + "reference": "b9bf24a936528f333fb30ee59ca550c6da551c11" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sass/sass-spec/zipball/d975d33146fb679a6b359ceca329012f02e4a794", - "reference": "d975d33146fb679a6b359ceca329012f02e4a794", + "url": "https://api.github.com/repos/sass/sass-spec/zipball/b9bf24a936528f333fb30ee59ca550c6da551c11", + "reference": "b9bf24a936528f333fb30ee59ca550c6da551c11", + "shasum": "" + } + } + }, + { + "type": "package", + "package": { + "name": "twbs/bootstrap4", + "version": "v4.6.0", + "source": { + "type": "git", + "url": "https://github.com/twbs/bootstrap.git", + "reference": "6ffb0b48e455430f8a5359ed689ad64c1143fac2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/twbs/bootstrap/zipball/6ffb0b48e455430f8a5359ed689ad64c1143fac2", + "reference": "6ffb0b48e455430f8a5359ed689ad64c1143fac2", "shasum": "" } } diff --git a/vendor/scssphp/scssphp/scss.inc.php b/vendor/scssphp/scssphp/scss.inc.php index 6b39d320..45983780 100644 --- a/vendor/scssphp/scssphp/scss.inc.php +++ b/vendor/scssphp/scssphp/scss.inc.php @@ -4,36 +4,18 @@ if (version_compare(PHP_VERSION, '5.6') < 0) { throw new \Exception('scssphp requires PHP 5.6 or above'); } -if (! class_exists('ScssPhp\ScssPhp\Version', false)) { - include_once __DIR__ . '/src/Base/Range.php'; - include_once __DIR__ . '/src/OutputStyle.php'; - include_once __DIR__ . '/src/Block.php'; - include_once __DIR__ . '/src/Cache.php'; - include_once __DIR__ . '/src/Colors.php'; - include_once __DIR__ . '/src/Compiler.php'; - include_once __DIR__ . '/src/Compiler/Environment.php'; - include_once __DIR__ . '/src/Exception/SassException.php'; - include_once __DIR__ . '/src/Exception/SassScriptException.php'; - include_once __DIR__ . '/src/Exception/CompilerException.php'; - include_once __DIR__ . '/src/Exception/ParserException.php'; - include_once __DIR__ . '/src/Exception/RangeException.php'; - include_once __DIR__ . '/src/Exception/ServerException.php'; - include_once __DIR__ . '/src/Formatter.php'; - include_once __DIR__ . '/src/Formatter/Compact.php'; - include_once __DIR__ . '/src/Formatter/Compressed.php'; - include_once __DIR__ . '/src/Formatter/Crunched.php'; - include_once __DIR__ . '/src/Formatter/Debug.php'; - include_once __DIR__ . '/src/Formatter/Expanded.php'; - include_once __DIR__ . '/src/Formatter/Nested.php'; - include_once __DIR__ . '/src/Formatter/OutputBlock.php'; - include_once __DIR__ . '/src/Node.php'; - include_once __DIR__ . '/src/Node/Number.php'; - include_once __DIR__ . '/src/Parser.php'; - include_once __DIR__ . '/src/SourceMap/Base64.php'; - include_once __DIR__ . '/src/SourceMap/Base64VLQ.php'; - include_once __DIR__ . '/src/SourceMap/SourceMapGenerator.php'; - include_once __DIR__ . '/src/Type.php'; - include_once __DIR__ . '/src/Util/Path.php'; - include_once __DIR__ . '/src/Util.php'; - include_once __DIR__ . '/src/Version.php'; +if (! class_exists('ScssPhp\ScssPhp\Version')) { + spl_autoload_register(function ($class) { + if (0 !== strpos($class, 'ScssPhp\ScssPhp\\')) { + // Not a ScssPhp class + return; + } + + $subClass = substr($class, strlen('ScssPhp\ScssPhp\\')); + $path = __DIR__ . '/src/' . str_replace('\\', '/', $subClass) . '.php'; + + if (file_exists($path)) { + require $path; + } + }); } diff --git a/vendor/scssphp/scssphp/src/Base/Range.php b/vendor/scssphp/scssphp/src/Base/Range.php index 2846746d..0c5f5840 100644 --- a/vendor/scssphp/scssphp/src/Base/Range.php +++ b/vendor/scssphp/scssphp/src/Base/Range.php @@ -16,10 +16,19 @@ namespace ScssPhp\ScssPhp\Base; * Range * * @author Anthon Pang + * + * @internal */ class Range { + /** + * @var float|int + */ public $first; + + /** + * @var float|int + */ public $last; /** diff --git a/vendor/scssphp/scssphp/src/Block.php b/vendor/scssphp/scssphp/src/Block.php index f7f4571e..3ae49d00 100644 --- a/vendor/scssphp/scssphp/src/Block.php +++ b/vendor/scssphp/scssphp/src/Block.php @@ -16,6 +16,8 @@ namespace ScssPhp\ScssPhp; * Block * * @author Anthon Pang + * + * @internal */ class Block { @@ -65,7 +67,7 @@ class Block public $children; /** - * @var \ScssPhp\ScssPhp\Block + * @var \ScssPhp\ScssPhp\Block|null */ public $selfParent; } diff --git a/vendor/scssphp/scssphp/src/Cache.php b/vendor/scssphp/scssphp/src/Cache.php index 4738ee4e..9731c60a 100644 --- a/vendor/scssphp/scssphp/src/Cache.php +++ b/vendor/scssphp/scssphp/src/Cache.php @@ -30,30 +30,54 @@ use ScssPhp\ScssPhp\Version; * SCSS cache * * @author Cedric Morin + * + * @internal */ class Cache { const CACHE_VERSION = 1; - // directory used for storing data + /** + * directory used for storing data + * + * @var string|false + */ public static $cacheDir = false; - // prefix for the storing data + /** + * prefix for the storing data + * + * @var string + */ public static $prefix = 'scssphp_'; - // force a refresh : 'once' for refreshing the first hit on a cache only, true to never use the cache in this hit + /** + * force a refresh : 'once' for refreshing the first hit on a cache only, true to never use the cache in this hit + * + * @var bool|string + */ public static $forceRefresh = false; - // specifies the number of seconds after which data cached will be seen as 'garbage' and potentially cleaned up + /** + * specifies the number of seconds after which data cached will be seen as 'garbage' and potentially cleaned up + * + * @var int + */ public static $gcLifetime = 604800; - // array of already refreshed cache if $forceRefresh==='once' + /** + * array of already refreshed cache if $forceRefresh==='once' + * + * @var array + */ protected static $refreshed = []; /** * Constructor * * @param array $options + * + * @phpstan-param array{cacheDir?: string, prefix?: string, forceRefresh?: string} $options */ public function __construct($options) { @@ -85,10 +109,10 @@ class Cache * Get the cached result of $operation on $what, * which is known as dependant from the content of $options * - * @param string $operation parse, compile... - * @param mixed $what content key (e.g., filename to be treated) - * @param array $options any option that affect the operation result on the content - * @param integer $lastModified last modified timestamp + * @param string $operation parse, compile... + * @param mixed $what content key (e.g., filename to be treated) + * @param array $options any option that affect the operation result on the content + * @param int|null $lastModified last modified timestamp * * @return mixed * @@ -128,6 +152,8 @@ class Cache * @param mixed $what * @param mixed $value * @param array $options + * + * @return void */ public function setCache($operation, $what, $value, $options = []) { @@ -174,6 +200,8 @@ class Cache /** * Check that the cache dir exists and is writeable * + * @return void + * * @throws \Exception */ public static function checkCacheDir() @@ -192,6 +220,8 @@ class Cache /** * Delete unused cached files + * + * @return void */ public static function cleanCache() { diff --git a/vendor/scssphp/scssphp/src/Colors.php b/vendor/scssphp/scssphp/src/Colors.php index 4b62c361..e836e3f4 100644 --- a/vendor/scssphp/scssphp/src/Colors.php +++ b/vendor/scssphp/scssphp/src/Colors.php @@ -16,6 +16,8 @@ namespace ScssPhp\ScssPhp; * CSS Colors * * @author Leaf Corcoran + * + * @internal */ class Colors { @@ -24,13 +26,13 @@ class Colors * * @see http://www.w3.org/TR/css3-color * - * @var array + * @var array */ protected static $cssColors = [ 'aliceblue' => '240,248,255', 'antiquewhite' => '250,235,215', - 'cyan' => '0,255,255', 'aqua' => '0,255,255', + 'cyan' => '0,255,255', 'aquamarine' => '127,255,212', 'azure' => '240,255,255', 'beige' => '245,245,220', @@ -75,8 +77,8 @@ class Colors 'firebrick' => '178,34,34', 'floralwhite' => '255,250,240', 'forestgreen' => '34,139,34', - 'magenta' => '255,0,255', 'fuchsia' => '255,0,255', + 'magenta' => '255,0,255', 'gainsboro' => '220,220,220', 'ghostwhite' => '248,248,255', 'gold' => '255,215,0', @@ -183,7 +185,7 @@ class Colors * * @param string $colorName * - * @return array|null + * @return int[]|null */ public static function colorNameToRGBa($colorName) { @@ -205,7 +207,7 @@ class Colors * @param integer $r * @param integer $g * @param integer $b - * @param integer $a + * @param integer|float $a * * @return string|null */ diff --git a/vendor/scssphp/scssphp/src/CompilationResult.php b/vendor/scssphp/scssphp/src/CompilationResult.php new file mode 100644 index 00000000..36adb0da --- /dev/null +++ b/vendor/scssphp/scssphp/src/CompilationResult.php @@ -0,0 +1,69 @@ +css = $css; + $this->sourceMap = $sourceMap; + $this->includedFiles = $includedFiles; + } + + /** + * @return string + */ + public function getCss() + { + return $this->css; + } + + /** + * @return string[] + */ + public function getIncludedFiles() + { + return $this->includedFiles; + } + + /** + * The sourceMap content, if it was generated + * + * @return null|string + */ + public function getSourceMap() + { + return $this->sourceMap; + } +} diff --git a/vendor/scssphp/scssphp/src/Compiler.php b/vendor/scssphp/scssphp/src/Compiler.php index 0997814e..ed71e4b2 100644 --- a/vendor/scssphp/scssphp/src/Compiler.php +++ b/vendor/scssphp/scssphp/src/Compiler.php @@ -13,13 +13,17 @@ namespace ScssPhp\ScssPhp; use ScssPhp\ScssPhp\Base\Range; +use ScssPhp\ScssPhp\Compiler\CachedResult; use ScssPhp\ScssPhp\Compiler\Environment; use ScssPhp\ScssPhp\Exception\CompilerException; use ScssPhp\ScssPhp\Exception\ParserException; +use ScssPhp\ScssPhp\Exception\SassException; use ScssPhp\ScssPhp\Exception\SassScriptException; use ScssPhp\ScssPhp\Formatter\Compressed; use ScssPhp\ScssPhp\Formatter\Expanded; use ScssPhp\ScssPhp\Formatter\OutputBlock; +use ScssPhp\ScssPhp\Logger\LoggerInterface; +use ScssPhp\ScssPhp\Logger\StreamLogger; use ScssPhp\ScssPhp\Node\Number; use ScssPhp\ScssPhp\SourceMap\SourceMapGenerator; use ScssPhp\ScssPhp\Util\Path; @@ -55,6 +59,8 @@ use ScssPhp\ScssPhp\Util\Path; * SCSS compiler * * @author Leaf Corcoran + * + * @final Extending the Compiler is deprecated */ class Compiler { @@ -140,11 +146,20 @@ class Compiler * @var array */ protected $importCache = []; + /** * @var string[] */ protected $importedFiles = []; + + /** + * @var array + * @phpstan-var array + */ protected $userFunctions = []; + /** + * @var array + */ protected $registeredVars = []; /** * @var array @@ -161,6 +176,7 @@ class Compiler */ protected $encoding = null; /** + * @var null * @deprecated */ protected $lineNumberStyle = null; @@ -170,6 +186,11 @@ class Compiler * @phpstan-var self::SOURCE_MAP_*|SourceMapGenerator */ protected $sourceMap = self::SOURCE_MAP_NONE; + + /** + * @var array + * @phpstan-var array{sourceRoot?: string, sourceMapFilename?: string|null, sourceMapURL?: string|null, sourceMapWriteTo?: string|null, outputSourceFiles?: bool, sourceMapRootpath?: string, sourceMapBasepath?: string} + */ protected $sourceMapOptions = []; /** @@ -203,7 +224,7 @@ class Compiler */ protected $charsetSeen; /** - * @var array + * @var array */ protected $sourceNames; @@ -212,6 +233,11 @@ class Compiler */ protected $cache; + /** + * @var bool + */ + protected $cacheCheckImportResolutions = false; + /** * @var int */ @@ -224,10 +250,12 @@ class Compiler * @var array */ protected $extendsMap; + /** * @var array */ - protected $parsedFiles; + protected $parsedFiles = []; + /** * @var Parser|null */ @@ -244,10 +272,6 @@ class Compiler * @var int|null */ protected $sourceColumn; - /** - * @var resource - */ - protected $stderr; /** * @var bool|null */ @@ -267,6 +291,12 @@ class Compiler */ protected $callStack = []; + /** + * @var array + * @phpstan-var list + */ + private $resolvedImports = []; + /** * The directory of the currently processed file * @@ -281,29 +311,47 @@ class Compiler */ private $rootDirectory; + /** + * @var bool + */ private $legacyCwdImportPath = true; + /** + * @var LoggerInterface + */ + private $logger; + + /** + * @var array + */ + private $warnedChildFunctions = []; + /** * Constructor * * @param array|null $cacheOptions + * @phpstan-param array{cacheDir?: string, prefix?: string, forceRefresh?: string, checkImportResolutions?: bool}|null $cacheOptions */ public function __construct($cacheOptions = null) { - $this->parsedFiles = []; $this->sourceNames = []; if ($cacheOptions) { $this->cache = new Cache($cacheOptions); + if (!empty($cacheOptions['checkImportResolutions'])) { + $this->cacheCheckImportResolutions = true; + } } - $this->stderr = fopen('php://stderr', 'w'); + $this->logger = new StreamLogger(fopen('php://stderr', 'w'), true); } /** * Get compiler options * * @return array + * + * @internal */ public function getCompileOptions() { @@ -321,50 +369,90 @@ class Compiler return $options; } + /** + * Sets an alternative logger. + * + * Changing the logger in the middle of the compilation is not + * supported and will result in an undefined behavior. + * + * @param LoggerInterface $logger + * + * @return void + */ + public function setLogger(LoggerInterface $logger) + { + $this->logger = $logger; + } + /** * Set an alternative error output stream, for testing purpose only * * @param resource $handle * * @return void + * + * @deprecated Use {@see setLogger} instead */ public function setErrorOuput($handle) { - $this->stderr = $handle; + @trigger_error('The method "setErrorOuput" is deprecated. Use "setLogger" instead.', E_USER_DEPRECATED); + + $this->logger = new StreamLogger($handle); } /** * Compile scss * - * @api - * - * @param string $code - * @param string $path + * @param string $code + * @param string|null $path * * @return string + * + * @throws SassException when the source fails to compile + * + * @deprecated Use {@see compileString} instead. */ public function compile($code, $path = null) { - if ($this->cache) { - $cacheKey = ($path ? $path : '(stdin)') . ':' . md5($code); - $compileOptions = $this->getCompileOptions(); - $cache = $this->cache->getCache('compile', $cacheKey, $compileOptions); + @trigger_error(sprintf('The "%s" method is deprecated. Use "compileString" instead.', __METHOD__), E_USER_DEPRECATED); - if (\is_array($cache) && isset($cache['dependencies']) && isset($cache['out'])) { - // check if any dependency file changed before accepting the cache - foreach ($cache['dependencies'] as $file => $mtime) { - if (! is_file($file) || filemtime($file) !== $mtime) { - unset($cache); - break; - } - } + $result = $this->compileString($code, $path); - if (isset($cache)) { - return $cache['out']; - } + $sourceMap = $result->getSourceMap(); + + if ($sourceMap !== null) { + if ($this->sourceMap instanceof SourceMapGenerator) { + $this->sourceMap->saveMap($sourceMap); + } elseif ($this->sourceMap === self::SOURCE_MAP_FILE) { + $sourceMapGenerator = new SourceMapGenerator($this->sourceMapOptions); + $sourceMapGenerator->saveMap($sourceMap); } } + return $result->getCss(); + } + + /** + * Compile scss + * + * @param string $source + * @param string|null $path + * + * @return CompilationResult + * + * @throws SassException when the source fails to compile + */ + public function compileString($source, $path = null) + { + if ($this->cache) { + $cacheKey = ($path ? $path : '(stdin)') . ':' . md5($source); + $compileOptions = $this->getCompileOptions(); + $cachedResult = $this->cache->getCache('compile', $cacheKey, $compileOptions); + + if ($cachedResult instanceof CachedResult && $this->isFreshCachedResult($cachedResult)) { + return $cachedResult->getResult(); + } + } $this->indentLevel = -1; $this->extends = []; @@ -378,6 +466,9 @@ class Compiler $this->charsetSeen = null; $this->shouldEvaluate = null; $this->ignoreCallStackMessage = false; + $this->parsedFiles = []; + $this->importedFiles = []; + $this->resolvedImports = []; if (!\is_null($path) && is_file($path)) { $path = realpath($path) ?: $path; @@ -390,16 +481,25 @@ class Compiler try { $this->parser = $this->parserFactory($path); - $tree = $this->parser->parse($code); + $tree = $this->parser->parse($source); $this->parser = null; $this->formatter = new $this->formatter(); $this->rootBlock = null; $this->rootEnv = $this->pushEnv($tree); - $this->injectVariables($this->registeredVars); - $this->compileRoot($tree); - $this->popEnv(); + $warnCallback = function ($message, $deprecation) { + $this->logger->warn($message, $deprecation); + }; + $previousWarnCallback = Warn::setCallback($warnCallback); + + try { + $this->injectVariables($this->registeredVars); + $this->compileRoot($tree); + $this->popEnv(); + } finally { + Warn::setCallback($previousWarnCallback); + } $sourceMapGenerator = null; @@ -423,8 +523,10 @@ class Compiler } } + $sourceMap = null; + if (! empty($out) && $this->sourceMap && $this->sourceMap !== self::SOURCE_MAP_NONE) { - $sourceMap = $sourceMapGenerator->generateJson($prefix); + $sourceMap = $sourceMapGenerator->generateJson($prefix); $sourceMapUrl = null; switch ($this->sourceMap) { @@ -433,32 +535,80 @@ class Compiler break; case self::SOURCE_MAP_FILE: - $sourceMapUrl = $sourceMapGenerator->saveMap($sourceMap); + if (isset($this->sourceMapOptions['sourceMapURL'])) { + $sourceMapUrl = $this->sourceMapOptions['sourceMapURL']; + } break; } - $out .= sprintf('/*# sourceMappingURL=%s */', $sourceMapUrl); + if ($sourceMapUrl !== null) { + $out .= sprintf('/*# sourceMappingURL=%s */', $sourceMapUrl); + } } } catch (SassScriptException $e) { - throw $this->error($e->getMessage()); + throw new CompilerException($this->addLocationToMessage($e->getMessage()), 0, $e); } + $includedFiles = []; + + foreach ($this->resolvedImports as $resolvedImport) { + $includedFiles[$resolvedImport['filePath']] = $resolvedImport['filePath']; + } + + $result = new CompilationResult($out, $sourceMap, array_values($includedFiles)); + if ($this->cache && isset($cacheKey) && isset($compileOptions)) { - $v = [ - 'dependencies' => $this->getParsedFiles(), - 'out' => &$out, - ]; - - $this->cache->setCache('compile', $cacheKey, $v, $compileOptions); + $this->cache->setCache('compile', $cacheKey, new CachedResult($result, $this->parsedFiles, $this->resolvedImports), $compileOptions); } - return $out; + // Reset state to free memory + // TODO in 2.0, reset parsedFiles as well when the getter is removed. + $this->resolvedImports = []; + $this->importedFiles = []; + + return $result; + } + + /** + * @param CachedResult $result + * + * @return bool + */ + private function isFreshCachedResult(CachedResult $result) + { + // check if any dependency file changed since the result was compiled + foreach ($result->getParsedFiles() as $file => $mtime) { + if (! is_file($file) || filemtime($file) !== $mtime) { + return false; + } + } + + if ($this->cacheCheckImportResolutions) { + $resolvedImports = []; + + foreach ($result->getResolvedImports() as $import) { + $currentDir = $import['currentDir']; + $path = $import['path']; + // store the check across all the results in memory to avoid multiple findImport() on the same path + // with same context. + // this is happening in a same hit with multiple compilations (especially with big frameworks) + if (empty($resolvedImports[$currentDir][$path])) { + $resolvedImports[$currentDir][$path] = $this->findImport($path, $currentDir); + } + + if ($resolvedImports[$currentDir][$path] !== $import['filePath']) { + return false; + } + } + } + + return true; } /** * Instantiate parser * - * @param string $path + * @param string|null $path * * @return \ScssPhp\ScssPhp\Parser */ @@ -471,11 +621,11 @@ class Compiler // Otherwise, the CSS will be rendered as-is. It can even be extended! $cssOnly = false; - if (substr($path, '-4') === '.css') { + if ($path !== null && substr($path, -4) === '.css') { $cssOnly = true; } - $parser = new Parser($path, \count($this->sourceNames), $this->encoding, $this->cache, $cssOnly); + $parser = new Parser($path, \count($this->sourceNames), $this->encoding, $this->cache, $cssOnly, $this->logger); $this->sourceNames[] = $path; $this->addParsedFile($path); @@ -528,8 +678,8 @@ class Compiler /** * Make output block * - * @param string $type - * @param array $selectors + * @param string|null $type + * @param string[]|null $selectors * * @return \ScssPhp\ScssPhp\Formatter\OutputBlock */ @@ -661,7 +811,7 @@ class Compiler } /** - * Glue parts of :not( or :nth-child( ... that are in general splitted in selectors parts + * Glue parts of :not( or :nth-child( ... that are in general split in selectors parts * * @param array $parts * @@ -1119,7 +1269,7 @@ class Compiler $mediaQueries = $this->compileMediaQuery($this->multiplyMedia($this->env)); - if (! empty($mediaQueries) && $mediaQueries) { + if (! empty($mediaQueries)) { $previousScope = $this->scope; $parentScope = $this->mediaParent($this->scope); @@ -1192,7 +1342,7 @@ class Compiler /** * Compile directive * - * @param \ScssPhp\ScssPhp\Block|array $block + * @param \ScssPhp\ScssPhp\Block|array $directive * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $out * * @return void @@ -1237,7 +1387,7 @@ class Compiler * directive names can include some interpolation * * @param string|array $directiveName - * @return array|string + * @return string * @throws CompilerException */ protected function compileDirectiveName($directiveName) @@ -1280,9 +1430,10 @@ class Compiler } $selfParent = $block->selfParent; + assert($selfParent !== null, 'at-root blocks must have a selfParent set.'); if ( - ! $block->selfParent->selectors && + ! $selfParent->selectors && isset($block->parent) && $block->parent && isset($block->parent->selectors) && $block->parent->selectors ) { @@ -1318,12 +1469,12 @@ class Compiler $filteredScopes = []; $childStash = []; - if ($scope->type === TYPE::T_ROOT) { + if ($scope->type === Type::T_ROOT) { return $scope; } // start from the root - while ($scope->parent && $scope->parent->type !== TYPE::T_ROOT) { + while ($scope->parent && $scope->parent->type !== Type::T_ROOT) { array_unshift($childStash, $scope); $scope = $scope->parent; } @@ -1451,7 +1602,7 @@ class Compiler } } - if ($this->libMapHasKey([$withCondition, static::$with])) { + if ($this->mapHasKey($withCondition, static::$with)) { $without = []; // cancel the default $list = $this->coerceList($this->libMapGet([$withCondition, static::$with])); @@ -1462,7 +1613,7 @@ class Compiler } } - if ($this->libMapHasKey([$withCondition, static::$without])) { + if ($this->mapHasKey($withCondition, static::$without)) { $without = []; // cancel the default $list = $this->coerceList($this->libMapGet([$withCondition, static::$without])); @@ -1578,7 +1729,7 @@ class Compiler * Compile keyframe block * * @param \ScssPhp\ScssPhp\Block $block - * @param array $selectors + * @param string[] $selectors * * @return void */ @@ -1644,7 +1795,7 @@ class Compiler * Compile nested block * * @param \ScssPhp\ScssPhp\Block $block - * @param array $selectors + * @param string[] $selectors * * @return void */ @@ -1760,17 +1911,16 @@ class Compiler $this->pushEnv(); } - $ignoreCallStackMessage = $this->ignoreCallStackMessage; - $this->ignoreCallStackMessage = true; - try { $c = $this->compileValue($value[2]); - } catch (\Exception $e) { + } catch (SassScriptException $e) { + $this->logger->warn('Ignoring interpolation errors in multiline comments is deprecated and will be removed in ScssPhp 2.0. ' . $this->addLocationToMessage($e->getMessage()), true); + // ignore error in comment compilation which are only interpolation + } catch (SassException $e) { + $this->logger->warn('Ignoring interpolation errors in multiline comments is deprecated and will be removed in ScssPhp 2.0. ' . $e->getMessage(), true); // ignore error in comment compilation which are only interpolation } - $this->ignoreCallStackMessage = $ignoreCallStackMessage; - if ($pushEnv) { $this->popEnv(); } @@ -1871,14 +2021,44 @@ class Compiler /** * Collapse selectors * - * @param array $selectors - * @param boolean $selectorFormat - * if false return a collapsed string - * if true return an array description of a structured selector + * @param array $selectors * * @return string */ - protected function collapseSelectors($selectors, $selectorFormat = false) + protected function collapseSelectors($selectors) + { + $parts = []; + + foreach ($selectors as $selector) { + $output = []; + + foreach ($selector as $node) { + $compound = ''; + + array_walk_recursive( + $node, + function ($value, $key) use (&$compound) { + $compound .= $value; + } + ); + + $output[] = $compound; + } + + $parts[] = implode(' ', $output); + } + + return implode(', ', $parts); + } + + /** + * Collapse selectors + * + * @param array $selectors + * + * @return array + */ + private function collapseSelectorsAsList($selectors) { $parts = []; @@ -1896,7 +2076,7 @@ class Compiler } ); - if ($selectorFormat && $this->isImmediateRelationshipCombinator($compound)) { + if ($this->isImmediateRelationshipCombinator($compound)) { if (\count($output)) { $output[\count($output) - 1] .= ' ' . $compound; } else { @@ -1912,32 +2092,21 @@ class Compiler } } - if ($selectorFormat) { - foreach ($output as &$o) { - $o = [Type::T_STRING, '', [$o]]; - } - - $output = [Type::T_LIST, ' ', $output]; - } else { - $output = implode(' ', $output); + foreach ($output as &$o) { + $o = [Type::T_STRING, '', [$o]]; } - $parts[] = $output; + $parts[] = [Type::T_LIST, ' ', $output]; } - if ($selectorFormat) { - $parts = [Type::T_LIST, ',', $parts]; - } else { - $parts = implode(', ', $parts); - } - - return $parts; + return [Type::T_LIST, ',', $parts]; } /** * Parse down the selector and revert [self] to "&" before a reparsing * - * @param array $selectors + * @param array $selectors + * @param string|null $replace * * @return array */ @@ -2104,7 +2273,7 @@ class Compiler * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $out * @param string $traceName * - * @return array|null + * @return array|Number|null */ protected function compileChildren($stms, OutputBlock $out, $traceName = '') { @@ -2146,7 +2315,7 @@ class Compiler $stm[1]->selfParent = $selfParent; $ret = $this->compileChild($stm, $out); $stm[1]->selfParent = null; - } elseif ($selfParent && \in_array($stm[0], [TYPE::T_INCLUDE, TYPE::T_EXTEND])) { + } elseif ($selfParent && \in_array($stm[0], [Type::T_INCLUDE, Type::T_EXTEND])) { $stm['selfParent'] = $selfParent; $ret = $this->compileChild($stm, $out); unset($stm['selfParent']); @@ -2234,7 +2403,7 @@ class Compiler * * @param array $queryList * - * @return array + * @return string[] */ protected function compileMediaQuery($queryList) { @@ -2472,10 +2641,12 @@ class Compiler if ($rawPath[0] === Type::T_STRING) { $path = $this->compileStringContent($rawPath); - if (strpos($path, 'url(') !== 0 && $path = $this->findImport($path)) { - if (! $once || ! \in_array($path, $this->importedFiles)) { - $this->importFile($path, $out); - $this->importedFiles[] = $path; + if (strpos($path, 'url(') !== 0 && $filePath = $this->findImport($path, $this->currentDirectory)) { + $this->registerImport($this->currentDirectory, $path, $filePath); + + if (! $once || ! \in_array($filePath, $this->importedFiles)) { + $this->importFile($filePath, $out); + $this->importedFiles[] = $filePath; } return true; @@ -2616,7 +2787,7 @@ class Compiler * * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $out * @param string $type - * @param string|mixed $line + * @param string $line * * @return void */ @@ -2667,12 +2838,13 @@ class Compiler $this->sourceColumn = $child[1]->sourceColumn; } elseif (! empty($out->sourceLine) && ! empty($out->sourceName)) { $this->sourceLine = $out->sourceLine; - $this->sourceIndex = array_search($out->sourceName, $this->sourceNames); + $sourceIndex = array_search($out->sourceName, $this->sourceNames); $this->sourceColumn = $out->sourceColumn; - if ($this->sourceIndex === false) { - $this->sourceIndex = null; + if ($sourceIndex === false) { + $sourceIndex = null; } + $this->sourceIndex = $sourceIndex; } switch ($child[0]) { @@ -2879,12 +3051,21 @@ class Compiler case Type::T_EXTEND: foreach ($child[1] as $sel) { - $sel = $this->replaceSelfSelector($sel); + $replacedSel = $this->replaceSelfSelector($sel); + + if ($replacedSel !== $sel) { + throw $this->error('Parent selectors aren\'t allowed here.'); + } + $results = $this->evalSelectors([$sel]); foreach ($results as $result) { + if (\count($result) !== 1) { + throw $this->error('complex selectors may not be extended.'); + } + // only use the first one - $result = current($result); + $result = $result[0]; $selectors = $out->selectors; if (! $selectors && isset($child['selfParent'])) { @@ -2962,24 +3143,15 @@ class Compiler case Type::T_FOR: list(, $for) = $child; - $start = $this->reduce($for->start, true); - $end = $this->reduce($for->end, true); + $startNumber = $this->assertNumber($this->reduce($for->start, true)); + $endNumber = $this->assertNumber($this->reduce($for->end, true)); - if (! $start instanceof Number) { - throw $this->error('%s is not a number', $start[0]); - } + $start = $this->assertInteger($startNumber); - if (! $end instanceof Number) { - throw $this->error('%s is not a number', $end[0]); - } + $numeratorUnits = $startNumber->getNumeratorUnits(); + $denominatorUnits = $startNumber->getDenominatorUnits(); - $start->assertSameUnitOrUnitless($end); - - $numeratorUnits = $start->getNumeratorUnits(); - $denominatorUnits = $start->getDenominatorUnits(); - - $start = $start->getDimension(); - $end = $end->getDimension(); + $end = $this->assertInteger($endNumber->coerce($numeratorUnits, $denominatorUnits)); $d = $start < $end ? 1 : -1; @@ -3131,7 +3303,7 @@ class Compiler $line = $this->sourceLine; $value = $this->compileDebugValue($value); - fwrite($this->stderr, "$fname:$line DEBUG: $value\n"); + $this->logger->debug("$fname:$line DEBUG: $value"); break; case Type::T_WARN: @@ -3141,7 +3313,7 @@ class Compiler $line = $this->sourceLine; $value = $this->compileDebugValue($value); - fwrite($this->stderr, "WARNING: $value\n on line $line of $fname\n\n"); + $this->logger->warn("$value\n on line $line of $fname"); break; case Type::T_ERROR: @@ -3204,7 +3376,7 @@ class Compiler * * @return boolean */ - protected function isTruthy($value) + public function isTruthy($value) { return $value !== static::$false && $value !== static::$null; } @@ -3251,12 +3423,12 @@ class Compiler * @param array|Number $value * @param boolean $inExp * - * @return null|string|array|Number + * @return array|Number */ protected function reduce($value, $inExp = false) { - if (\is_null($value)) { - return null; + if ($value instanceof Number) { + return $value; } switch ($value[0]) { @@ -3357,6 +3529,14 @@ class Compiler foreach ($value[2] as &$item) { $item = $this->reduce($item); } + unset($item); + + if (isset($value[3]) && \is_array($value[3])) { + foreach ($value[3] as &$item) { + $item = $this->reduce($item); + } + unset($item); + } return $value; @@ -3373,7 +3553,7 @@ class Compiler case Type::T_STRING: foreach ($value[2] as &$item) { - if (\is_array($item) || $item instanceof \ArrayAccess) { + if (\is_array($item) || $item instanceof Number) { $item = $this->reduce($item); } } @@ -3384,7 +3564,7 @@ class Compiler $value[1] = $this->reduce($value[1]); if ($inExp) { - return $value[1]; + return [Type::T_KEYWORD, $this->compileValue($value, false)]; } return $value; @@ -3395,7 +3575,7 @@ class Compiler case Type::T_SELF: $selfParent = ! empty($this->env->block->selfParent) ? $this->env->block->selfParent : null; $selfSelector = $this->multiplySelectors($this->env, $selfParent); - $selfSelector = $this->collapseSelectors($selfSelector, true); + $selfSelector = $this->collapseSelectorsAsList($selfSelector); return $selfSelector; @@ -3407,8 +3587,8 @@ class Compiler /** * Function caller * - * @param string $name - * @param array $argValues + * @param string|array $functionReference + * @param array $argValues * * @return array|Number */ @@ -3455,7 +3635,7 @@ class Compiler // special cases of css valid functions min/max $name = strtolower($name); - if (\in_array($name, ['min', 'max'])) { + if (\in_array($name, ['min', 'max']) && count($argValues) >= 1) { $cssFunction = $this->cssValidArg( [Type::T_FUNCTION_CALL, $name, $argValues], ['min', 'max', 'calc', 'env', 'var'] @@ -3477,8 +3657,19 @@ class Compiler } } + /** + * @param array|Number $arg + * @param string[] $allowed_function + * @param bool $inFunction + * + * @return array|Number|false + */ protected function cssValidArg($arg, $allowed_function = [], $inFunction = false) { + if ($arg instanceof Number) { + return $this->stringifyFncallArgs($arg); + } + switch ($arg[0]) { case Type::T_INTERPOLATE: return [Type::T_KEYWORD, $this->CompileValue($arg)]; @@ -3523,9 +3714,6 @@ class Compiler } return $this->stringifyFncallArgs($arg); - case Type::T_NUMBER: - return $this->stringifyFncallArgs($arg); - case Type::T_LIST: if (!$inFunction) { return false; @@ -3564,12 +3752,15 @@ class Compiler /** * Reformat fncall arguments to proper css function output * - * @param $arg + * @param array|Number $arg * - * @return array|\ArrayAccess|Number|string|null + * @return array|Number */ protected function stringifyFncallArgs($arg) { + if ($arg instanceof Number) { + return $arg; + } switch ($arg[0]) { case Type::T_LIST: @@ -3634,6 +3825,21 @@ class Compiler $libName = $f[1]; $prototype = isset(static::$$libName) ? static::$$libName : null; + if (\get_class($this) !== __CLASS__ && !isset($this->warnedChildFunctions[$libName])) { + $r = new \ReflectionMethod($this, $libName); + $declaringClass = $r->getDeclaringClass()->name; + + $needsWarning = $this->warnedChildFunctions[$libName] = $declaringClass !== __CLASS__; + + if ($needsWarning) { + if (method_exists(__CLASS__, $libName)) { + @trigger_error(sprintf('Overriding the "%s" core function by extending the Compiler is deprecated and will be unsupported in 2.0. Remove the "%s::%s" method.', $normalizedName, $declaringClass, $libName), E_USER_DEPRECATED); + } else { + @trigger_error(sprintf('Registering custom functions by extending the Compiler and using the lib* discovery mechanism is deprecated and will be removed in 2.0. Replace the "%s::%s" method with registering the "%s" function through "Compiler::registerFunction".', $declaringClass, $libName, $normalizedName), E_USER_DEPRECATED); + } + } + } + return [Type::T_FUNCTION_REFERENCE, 'native', $name, $f, $prototype]; } @@ -3656,6 +3862,8 @@ class Compiler /** * Normalize value * + * @internal + * * @param array|Number $value * * @return array|Number @@ -3664,6 +3872,10 @@ class Compiler { $value = $this->coerceForExpression($this->reduce($value)); + if ($value instanceof Number) { + return $value; + } + switch ($value[0]) { case Type::T_LIST: $value = $this->extractInterpolation($value); @@ -3864,7 +4076,7 @@ class Compiler $fname = $this->getPrettyPath($this->sourceNames[$this->sourceIndex]); $line = $this->sourceLine; - fwrite($this->stderr, "DEPRECATION WARNING: $warning\n on line $line of $fname\n\n"); + Warn::deprecation("$warning\n on line $line of $fname"); } $out = [Type::T_COLOR]; @@ -4103,7 +4315,7 @@ class Compiler * * @api * - * @param mixed $thing + * @param bool $thing * * @return array */ @@ -4114,7 +4326,12 @@ class Compiler /** * Escape non printable chars in strings output as in dart-sass + * + * @internal + * * @param string $string + * @param bool $inKeyword + * * @return string */ public function escapeNonPrintableChars($string, $inKeyword = false) @@ -4167,20 +4384,22 @@ class Compiler * * @api * - * @param array|Number|string $value + * @param array|Number $value + * @param bool $quote * * @return string */ - public function compileValue($value) + public function compileValue($value, $quote = true) { $value = $this->reduce($value); + if ($value instanceof Number) { + return $value->output($this); + } + switch ($value[0]) { case Type::T_KEYWORD: - if (is_string($value[1])) { - $value[1] = $this->escapeNonPrintableChars($value[1], true); - } - return $value[1]; + return $this->escapeNonPrintableChars($value[1], true); case Type::T_COLOR: // [1] - red component (either number for a %) @@ -4232,13 +4451,10 @@ class Compiler return $h; - case Type::T_NUMBER: - return $value->output($this); - case Type::T_STRING: - $content = $this->compileStringContent($value); + $content = $this->compileStringContent($value, $quote); - if ($value[1]) { + if ($value[1] && $quote) { $content = str_replace('\\', '\\\\', $content); $content = $this->escapeNonPrintableChars($content); @@ -4263,7 +4479,7 @@ class Compiler return $value[1] . $content . $value[1]; case Type::T_FUNCTION: - $args = ! empty($value[2]) ? $this->compileValue($value[2]) : ''; + $args = ! empty($value[2]) ? $this->compileValue($value[2], $quote) : ''; return "$value[1]($args)"; @@ -4276,7 +4492,7 @@ class Compiler $value = $this->extractInterpolation($value); if ($value[0] !== Type::T_LIST) { - return $this->compileValue($value); + return $this->compileValue($value, $quote); } list(, $delim, $items) = $value; @@ -4329,7 +4545,7 @@ class Compiler $item[1] = $same_string_quote; } - $compiled = $this->compileValue($item); + $compiled = $this->compileValue($item, $quote); if ($prefix_value && \strlen($compiled)) { $compiled = $prefix_value . $compiled; @@ -4346,7 +4562,7 @@ class Compiler $filtered = []; for ($i = 0, $s = \count($keys); $i < $s; $i++) { - $filtered[$this->compileValue($keys[$i])] = $this->compileValue($values[$i]); + $filtered[$this->compileValue($keys[$i], $quote)] = $this->compileValue($values[$i], $quote); } array_walk($filtered, function (&$value, $key) { @@ -4367,7 +4583,7 @@ class Compiler } $left = \count($left[2]) > 0 - ? $this->compileValue($left) . $delim . $whiteLeft + ? $this->compileValue($left, $quote) . $delim . $whiteLeft : ''; $delim = $right[1]; @@ -4377,14 +4593,18 @@ class Compiler } $right = \count($right[2]) > 0 ? - $whiteRight . $delim . $this->compileValue($right) : ''; + $whiteRight . $delim . $this->compileValue($right, $quote) : ''; - return $left . $this->compileValue($interpolate) . $right; + return $left . $this->compileValue($interpolate, $quote) . $right; case Type::T_INTERPOLATE: // strip quotes if it's a string $reduced = $this->reduce($value[1]); + if ($reduced instanceof Number) { + return $this->compileValue($reduced, $quote); + } + switch ($reduced[0]) { case Type::T_LIST: $reduced = $this->extractInterpolation($reduced); @@ -4406,14 +4626,12 @@ class Compiler continue; } - $temp = $this->compileValue([Type::T_KEYWORD, $item]); - - if ($temp[0] === Type::T_STRING) { - $filtered[] = $this->compileStringContent($temp); - } elseif ($temp[0] === Type::T_KEYWORD) { - $filtered[] = $temp[1]; + if ($item[0] === Type::T_STRING) { + $filtered[] = $this->compileStringContent($item, $quote); + } elseif ($item[0] === Type::T_KEYWORD) { + $filtered[] = $item[1]; } else { - $filtered[] = $this->compileValue($temp); + $filtered[] = $this->compileValue($item, $quote); } } @@ -4428,7 +4646,7 @@ class Compiler $reduced = [Type::T_KEYWORD, '']; } - return $this->compileValue($reduced); + return $this->compileValue($reduced, $quote); case Type::T_NULL: return 'null'; @@ -4442,14 +4660,18 @@ class Compiler } /** - * @param array $value + * @param array|Number $value * - * @return array|string + * @return string */ protected function compileDebugValue($value) { $value = $this->reduce($value, true); + if ($value instanceof Number) { + return $this->compileValue($value); + } + switch ($value[0]) { case Type::T_STRING: return $this->compileStringContent($value); @@ -4465,26 +4687,50 @@ class Compiler * @param array $list * * @return string + * + * @deprecated */ protected function flattenList($list) { + @trigger_error(sprintf('The "%s" method is deprecated.', __METHOD__), E_USER_DEPRECATED); + return $this->compileValue($list); } + /** + * Gets the text of a Sass string + * + * Calling this method on anything else than a SassString is unsupported. Use {@see assertString} first + * to ensure that the value is indeed a string. + * + * @param array $value + * + * @return string + */ + public function getStringText(array $value) + { + if ($value[0] !== Type::T_STRING) { + throw new \InvalidArgumentException('The argument is not a sass string. Did you forgot to use "assertString"?'); + } + + return $this->compileStringContent($value); + } + /** * Compile string content * * @param array $string + * @param bool $quote * * @return string */ - protected function compileStringContent($string) + protected function compileStringContent($string, $quote = true) { $parts = []; foreach ($string[2] as $part) { - if (\is_array($part) || $part instanceof \ArrayAccess) { - $parts[] = $this->compileValue($part); + if (\is_array($part) || $part instanceof Number) { + $parts[] = $this->compileValue($part, $quote); } else { $parts[] = $part; } @@ -4898,7 +5144,7 @@ class Compiler /** * Get variable * - * @api + * @internal * * @param string $name * @param boolean $shouldThrow @@ -4997,7 +5243,7 @@ class Compiler $name = substr($name, 1); } - if (! $parser->parseValue($strValue, $value)) { + if (!\is_string($strValue) || ! $parser->parseValue($strValue, $value)) { $value = $this->coerceValue($strValue); } @@ -5005,6 +5251,43 @@ class Compiler } } + /** + * Replaces variables. + * + * @param array $variables + * + * @return void + */ + public function replaceVariables(array $variables) + { + $this->registeredVars = []; + $this->addVariables($variables); + } + + /** + * Replaces variables. + * + * @param array $variables + * + * @return void + */ + public function addVariables(array $variables) + { + $triggerWarning = false; + + foreach ($variables as $name => $value) { + if (!$value instanceof Number && !\is_array($value)) { + $triggerWarning = true; + } + + $this->registeredVars[$name] = $value; + } + + if ($triggerWarning) { + @trigger_error('Passing raw values to as custom variables to the Compiler is deprecated. Use "\ScssPhp\ScssPhp\ValueConverter::parseValue" or "\ScssPhp\ScssPhp\ValueConverter::fromPhp" to convert them instead.', E_USER_DEPRECATED); + } + } + /** * Set variables * @@ -5013,10 +5296,14 @@ class Compiler * @param array $variables * * @return void + * + * @deprecated Use "addVariables" or "replaceVariables" instead. */ public function setVariables(array $variables) { - $this->registeredVars = array_merge($this->registeredVars, $variables); + @trigger_error('The method "setVariables" of the Compiler is deprecated. Use the "addVariables" method for the equivalent behavior or "replaceVariables" if merging with previous variables was not desired.'); + + $this->addVariables($variables); } /** @@ -5048,15 +5335,15 @@ class Compiler /** * Adds to list of parsed files * - * @api + * @internal * - * @param string $path + * @param string|null $path * * @return void */ public function addParsedFile($path) { - if (isset($path) && is_file($path)) { + if (! \is_null($path) && is_file($path)) { $this->parsedFiles[realpath($path)] = filemtime($path); } } @@ -5064,12 +5351,12 @@ class Compiler /** * Returns list of parsed files * - * @api - * - * @return array + * @deprecated + * @return array */ public function getParsedFiles() { + @trigger_error('The method "getParsedFiles" of the Compiler is deprecated. Use the "getIncludedFiles" method on the CompilationResult instance returned by compileString() instead. Be careful that the signature of the method is different.', E_USER_DEPRECATED); return $this->parsedFiles; } @@ -5108,7 +5395,7 @@ class Compiler $this->legacyCwdImportPath = \count($actualImportPaths) !== \count($paths); if ($this->legacyCwdImportPath) { - @trigger_error('Passing an empty string in the import paths to refer to the current working directory is deprecated. If that\'s the intended behavior, the value of "getcwd()" should be used directly instead. If this was used for resolving relative imports of the input alongside "chdir" with the source directory, the path of the input file should be passed to "compile()" instead.', E_USER_DEPRECATED); + @trigger_error('Passing an empty string in the import paths to refer to the current working directory is deprecated. If that\'s the intended behavior, the value of "getcwd()" should be used directly instead. If this was used for resolving relative imports of the input alongside "chdir" with the source directory, the path of the input file should be passed to "compileString()" instead.', E_USER_DEPRECATED); } $this->importPaths = $actualImportPaths; @@ -5219,6 +5506,8 @@ class Compiler * * @param array $sourceMapOptions * + * @phpstan-param array{sourceRoot?: string, sourceMapFilename?: string|null, sourceMapURL?: string|null, sourceMapWriteTo?: string|null, outputSourceFiles?: bool, sourceMapRootpath?: string, sourceMapBasepath?: string} $sourceMapOptions + * * @return void */ public function setSourceMapOptions($sourceMapOptions) @@ -5231,15 +5520,23 @@ class Compiler * * @api * - * @param string $name - * @param callable $func - * @param array|null $prototype + * @param string $name + * @param callable $callback + * @param string[]|null $argumentDeclaration * * @return void */ - public function registerFunction($name, $func, $prototype = null) + public function registerFunction($name, $callback, $argumentDeclaration = null) { - $this->userFunctions[$this->normalizeName($name)] = [$func, $prototype]; + if (self::isNativeFunction($name)) { + @trigger_error(sprintf('The "%s" function is a core sass function. Overriding it with a custom implementation through "%s" is deprecated and won\'t be supported in ScssPhp 2.0 anymore.', $name, __METHOD__), E_USER_DEPRECATED); + } + + if ($argumentDeclaration === null) { + @trigger_error('Omitting the argument declaration when registering custom function is deprecated and won\'t be supported in ScssPhp 2.0 anymore.', E_USER_DEPRECATED); + } + + $this->userFunctions[$this->normalizeName($name)] = [$callback, $argumentDeclaration]; } /** @@ -5288,6 +5585,15 @@ class Compiler // see if tree is cached $realPath = realpath($path); + if (substr($path, -5) === '.sass') { + $this->sourceIndex = \count($this->sourceNames); + $this->sourceNames[] = $path; + $this->sourceLine = 1; + $this->sourceColumn = 1; + + throw $this->error('The Sass indented syntax is not implemented.'); + } + if (isset($this->importCache[$realPath])) { $this->handleImportLoop($realPath); @@ -5309,19 +5615,51 @@ class Compiler } /** - * Return the file path for an import url if it exists + * Save the imported files with their resolving path context * - * @api + * @param string|null $currentDirectory + * @param string $path + * @param string $filePath + * + * @return void + */ + private function registerImport($currentDirectory, $path, $filePath) + { + $this->resolvedImports[] = ['currentDir' => $currentDirectory, 'path' => $path, 'filePath' => $filePath]; + } + + /** + * Detects whether the import is a CSS import. + * + * For legacy reasons, custom importers are called for those, allowing them + * to replace them with an actual Sass import. However this behavior is + * deprecated. Custom importers are expected to return null when they receive + * a CSS import. * * @param string $url * + * @return bool + */ + public static function isCssImport($url) + { + return 1 === preg_match('~\.css$|^https?://|^//~', $url); + } + + /** + * Return the file path for an import url if it exists + * + * @internal + * + * @param string $url + * @param string|null $currentDir + * * @return string|null */ - public function findImport($url) + public function findImport($url, $currentDir = null) { - // for "normal" scss imports (ignore vanilla css and external requests) + // Vanilla css and external requests. These are not meant to be Sass imports. // Callback importers are still called for BC. - if (preg_match('~\.css$|^https?://|^//~', $url)) { + if (self::isCssImport($url)) { foreach ($this->importPaths as $dir) { if (\is_string($dir)) { continue; @@ -5332,6 +5670,24 @@ class Compiler $file = \call_user_func($dir, $url); if (! \is_null($file)) { + if (\is_array($dir)) { + $callableDescription = (\is_object($dir[0]) ? \get_class($dir[0]) : $dir[0]).'::'.$dir[1]; + } elseif ($dir instanceof \Closure) { + $r = new \ReflectionFunction($dir); + if (false !== strpos($r->name, '{closure}')) { + $callableDescription = sprintf('closure{%s:%s}', $r->getFileName(), $r->getStartLine()); + } elseif ($class = $r->getClosureScopeClass()) { + $callableDescription = $class->name.'::'.$r->name; + } else { + $callableDescription = $r->name; + } + } elseif (\is_object($dir)) { + $callableDescription = \get_class($dir) . '::__invoke'; + } else { + $callableDescription = 'callable'; // Fallback if we don't have a dedicated description + } + @trigger_error(sprintf('Returning a file to import for CSS or external references in custom importer callables is deprecated and will not be supported anymore in ScssPhp 2.0. This behavior is not compliant with the Sass specification. Update your "%s" importer.', $callableDescription), E_USER_DEPRECATED); + return $file; } } @@ -5339,8 +5695,8 @@ class Compiler return null; } - if (!\is_null($this->currentDirectory)) { - $relativePath = $this->resolveImportPath($url, $this->currentDirectory); + if (!\is_null($currentDir)) { + $relativePath = $this->resolveImportPath($url, $currentDir); if (!\is_null($relativePath)) { return $relativePath; @@ -5368,7 +5724,7 @@ class Compiler $path = $this->resolveImportPath($url, getcwd()); if (!\is_null($path)) { - @trigger_error('Resolving imports relatively to the current working directory is deprecated. If that\'s the intended behavior, the value of "getcwd()" should be added as an import path explicitly instead. If this was used for resolving relative imports of the input alongside "chdir" with the source directory, the path of the input file should be passed to "compile()" instead.', E_USER_DEPRECATED); + @trigger_error('Resolving imports relatively to the current working directory is deprecated. If that\'s the intended behavior, the value of "getcwd()" should be added as an import path explicitly instead. If this was used for resolving relative imports of the input alongside "chdir" with the source directory, the path of the input file should be passed to "compileString()" instead.', E_USER_DEPRECATED); return $path; } @@ -5387,7 +5743,7 @@ class Compiler { $path = Path::join($baseDir, $url); - $hasExtension = preg_match('/.scss$/', $url); + $hasExtension = preg_match('/.s[ac]ss$/', $url); if ($hasExtension) { return $this->checkImportPathConflicts($this->tryImportPath($path)); @@ -5433,7 +5789,10 @@ class Compiler */ private function tryImportPathWithExtensions($path) { - $result = $this->tryImportPath($path.'.scss'); + $result = array_merge( + $this->tryImportPath($path.'.sass'), + $this->tryImportPath($path.'.scss') + ); if ($result) { return $result; @@ -5479,12 +5838,16 @@ class Compiler } /** - * @param string $path + * @param string|null $path * * @return string */ private function getPrettyPath($path) { + if ($path === null) { + return '(unknown file)'; + } + $normalizedPath = $path; $normalizedRootDirectory = $this->rootDirectory.'/'; @@ -5505,12 +5868,20 @@ class Compiler * * @api * - * @param string $encoding + * @param string|null $encoding * * @return void + * + * @deprecated Non-compliant support for other encodings than UTF-8 is deprecated. */ public function setEncoding($encoding) { + if (!$encoding || strtolower($encoding) === 'utf-8') { + @trigger_error(sprintf('The "%s" method is deprecated.', __METHOD__), E_USER_DEPRECATED); + } else { + @trigger_error(sprintf('The "%s" method is deprecated. Parsing will only support UTF-8 in ScssPhp 2.0. The non-UTF-8 parsing of ScssPhp 1.x is not spec compliant.', __METHOD__), E_USER_DEPRECATED); + } + $this->encoding = $encoding; } @@ -5538,9 +5909,13 @@ class Compiler * @api * * @return array + * + * @deprecated */ public function getSourcePosition() { + @trigger_error(sprintf('The "%s" method is deprecated.', __METHOD__), E_USER_DEPRECATED); + $sourceFile = isset($this->sourceNames[$this->sourceIndex]) ? $this->sourceNames[$this->sourceIndex] : ''; return [$sourceFile, $this->sourceLine, $this->sourceColumn]; @@ -5570,7 +5945,7 @@ class Compiler /** * Build an error (exception) * - * @api + * @internal * * @param string $msg Message with optional sprintf()-style vararg parameters * @@ -5583,33 +5958,49 @@ class Compiler } if (! $this->ignoreCallStackMessage) { - $line = $this->sourceLine; - $column = $this->sourceColumn; - - $loc = isset($this->sourceNames[$this->sourceIndex]) - ? $this->getPrettyPath($this->sourceNames[$this->sourceIndex]) . " on line $line, at column $column" - : "line: $line, column: $column"; - - $msg = "$msg: $loc"; - - $callStackMsg = $this->callStackMessage(); - - if ($callStackMsg) { - $msg .= "\nCall Stack:\n" . $callStackMsg; - } + $msg = $this->addLocationToMessage($msg); } return new CompilerException($msg); } + /** + * @param string $msg + * + * @return string + */ + private function addLocationToMessage($msg) + { + $line = $this->sourceLine; + $column = $this->sourceColumn; + + $loc = isset($this->sourceNames[$this->sourceIndex]) + ? $this->getPrettyPath($this->sourceNames[$this->sourceIndex]) . " on line $line, at column $column" + : "line: $line, column: $column"; + + $msg = "$msg: $loc"; + + $callStackMsg = $this->callStackMessage(); + + if ($callStackMsg) { + $msg .= "\nCall Stack:\n" . $callStackMsg; + } + + return $msg; + } + /** * @param string $functionName * @param array $ExpectedArgs * @param int $nbActual * @return CompilerException + * + * @deprecated */ public function errorArgsNumber($functionName, $ExpectedArgs, $nbActual) { + @trigger_error(sprintf('The "%s" method is deprecated.', __METHOD__), E_USER_DEPRECATED); + $nbExpected = \count($ExpectedArgs); if ($nbActual > $nbExpected) { @@ -5638,8 +6029,8 @@ class Compiler /** * Beautify call stack for output * - * @param boolean $all - * @param null $limit + * @param boolean $all + * @param int|null $limit * * @return string */ @@ -5685,6 +6076,10 @@ class Compiler $file = $this->sourceNames[$env->block->sourceIndex]; + if ($file === null) { + continue; + } + if (realpath($file) === $name) { throw $this->error('An @import loop has been found: %s imports %s', $file, basename($file)); } @@ -5697,7 +6092,7 @@ class Compiler * @param Object $func * @param array $argValues * - * @return array + * @return array|Number */ protected function callScssFunction($func, $argValues) { @@ -5737,7 +6132,7 @@ class Compiler * Call built-in and registered (PHP) functions * * @param string $name - * @param string|array $function + * @param callable $function * @param array $prototype * @param array $args * @@ -5754,14 +6149,10 @@ class Compiler @list($sorted, $kwargs) = $sorted_kwargs; if ($name !== 'if') { - $inExp = true; - - if ($name === 'join') { - $inExp = false; - } - foreach ($sorted as &$val) { - $val = $this->reduce($val, $inExp); + if ($val !== null) { + $val = $this->reduce($val, true); + } } } @@ -5771,6 +6162,12 @@ class Compiler return null; } + if (\is_array($returnValue) || $returnValue instanceof Number) { + return $returnValue; + } + + @trigger_error(sprintf('Returning a PHP value from the "%s" custom function is deprecated. A sass value must be returned instead.', $name), E_USER_DEPRECATED); + return $this->coerceValue($returnValue); } @@ -5789,7 +6186,11 @@ class Compiler /** * Normalize native function name + * + * @internal + * * @param string $name + * * @return string */ public static function normalizeNativeFunctionName($name) @@ -5807,7 +6208,11 @@ class Compiler /** * Check if a function is a native built-in scss function, for css parsing + * + * @internal + * * @param string $name + * * @return bool */ public static function isNativeFunction($name) @@ -5819,7 +6224,7 @@ class Compiler * Sorts keyword arguments * * @param string $functionName - * @param array $prototypes + * @param array|null $prototypes * @param array $args * * @return array|null @@ -5855,140 +6260,417 @@ class Compiler // notation 100 127 255 / 0 is in fact a simple list of 4 values foreach ($args as $k => $arg) { if ($arg[1][0] === Type::T_LIST && \count($arg[1][2]) === 3) { - $last = end($arg[1][2]); - - if ($last[0] === Type::T_EXPRESSION && $last[1] === '/') { - array_pop($arg[1][2]); - $arg[1][2][] = $last[2]; - $arg[1][2][] = $last[3]; - $args[$k] = $arg; - } + $args[$k][1][2] = $this->extractSlashAlphaInColorFunction($arg[1][2]); } } } - $finalArgs = []; + list($positionalArgs, $namedArgs, $names, $separator, $hasSplat) = $this->evaluateArguments($args, false); if (! \is_array(reset($prototypes))) { $prototypes = [$prototypes]; } + $parsedPrototypes = array_map([$this, 'parseFunctionPrototype'], $prototypes); + assert(!empty($parsedPrototypes)); + $matchedPrototype = $this->selectFunctionPrototype($parsedPrototypes, \count($positionalArgs), $names); + + $this->verifyPrototype($matchedPrototype, \count($positionalArgs), $names, $hasSplat); + + $vars = $this->applyArgumentsToDeclaration($matchedPrototype, $positionalArgs, $namedArgs, $separator); + + $finalArgs = []; $keyArgs = []; - // trying each prototypes - $prototypeHasMatch = false; - $exceptionMessage = ''; + foreach ($matchedPrototype['arguments'] as $argument) { + list($normalizedName, $originalName, $default) = $argument; - foreach ($prototypes as $prototype) { - $argDef = []; - - foreach ($prototype as $i => $p) { - $default = null; - $p = explode(':', $p, 2); - $name = array_shift($p); - - if (\count($p)) { - $p = trim(reset($p)); - - if ($p === 'null') { - // differentiate this null from the static::$null - $default = [Type::T_KEYWORD, 'null']; - } else { - if (\is_null($parser)) { - $parser = $this->parserFactory(__METHOD__); - } - - $parser->parseValue($p, $default); - } - } - - $isVariable = false; - - if (substr($name, -3) === '...') { - $isVariable = true; - $name = substr($name, 0, -3); - } - - $argDef[] = [$name, $default, $isVariable]; + if (isset($vars[$normalizedName])) { + $value = $vars[$normalizedName]; + } else { + $value = $default; } - $ignoreCallStackMessage = $this->ignoreCallStackMessage; - $this->ignoreCallStackMessage = true; - - try { - if (\count($args) > \count($argDef)) { - $lastDef = end($argDef); - - // check that last arg is not a ... - if (empty($lastDef[2])) { - throw $this->errorArgsNumber($functionName, $argDef, \count($args)); - } - } - $vars = $this->applyArguments($argDef, $args, false, false); - - // ensure all args are populated - foreach ($prototype as $i => $p) { - $name = explode(':', $p)[0]; - - if (! isset($finalArgs[$i])) { - $finalArgs[$i] = null; - } - } - - // apply positional args - foreach (array_values($vars) as $i => $val) { - $finalArgs[$i] = $val; - } - - $keyArgs = array_merge($keyArgs, $vars); - $prototypeHasMatch = true; - - // overwrite positional args with keyword args - foreach ($prototype as $i => $p) { - $name = explode(':', $p)[0]; - - if (isset($keyArgs[$name])) { - $finalArgs[$i] = $keyArgs[$name]; - } - - // special null value as default: translate to real null here - if ($finalArgs[$i] === [Type::T_KEYWORD, 'null']) { - $finalArgs[$i] = null; - } - } - // should we break if this prototype seems fulfilled? - } catch (CompilerException $e) { - $exceptionMessage = $e->getMessage(); + // special null value as default: translate to real null here + if ($value === [Type::T_KEYWORD, 'null']) { + $value = null; } - $this->ignoreCallStackMessage = $ignoreCallStackMessage; + + $finalArgs[] = $value; + $keyArgs[$originalName] = $value; } - if ($exceptionMessage && ! $prototypeHasMatch) { - if (\in_array($functionName, ['libRgb', 'libRgba', 'libHsl', 'libHsla'])) { - // if var() or calc() is used as an argument, return as a css function - foreach ($args as $arg) { - if ($arg[1][0] === Type::T_FUNCTION_CALL && in_array($arg[1][1], ['var'])) { - return null; - } - } - } + if ($matchedPrototype['rest_argument'] !== null) { + $value = $vars[$matchedPrototype['rest_argument']]; - throw $this->error($exceptionMessage); + $finalArgs[] = $value; + $keyArgs[$matchedPrototype['rest_argument']] = $value; } return [$finalArgs, $keyArgs]; } + /** + * Parses a function prototype to the internal representation of arguments. + * + * The input is an array of strings describing each argument, as supported + * in {@see registerFunction}. Argument names don't include the `$`. + * The output contains the list of positional argument, with their normalized + * name (underscores are replaced by dashes), their original name (to be used + * in case of error reporting) and their default value. The output also contains + * the normalized name of the rest argument, or null if the function prototype + * is not variadic. + * + * @param string[] $prototype + * + * @return array + * @phpstan-return array{arguments: list, rest_argument: string|null} + */ + private function parseFunctionPrototype(array $prototype) + { + static $parser = null; + + $arguments = []; + $restArgument = null; + + foreach ($prototype as $p) { + if (null !== $restArgument) { + throw new \InvalidArgumentException('The argument declaration is invalid. The rest argument must be the last one.'); + } + + $default = null; + $p = explode(':', $p, 2); + $name = str_replace('_', '-', $p[0]); + + if (isset($p[1])) { + $defaultSource = trim($p[1]); + + if ($defaultSource === 'null') { + // differentiate this null from the static::$null + $default = [Type::T_KEYWORD, 'null']; + } else { + if (\is_null($parser)) { + $parser = $this->parserFactory(__METHOD__); + } + + $parser->parseValue($defaultSource, $default); + } + } + + if (substr($name, -3) === '...') { + $restArgument = substr($name, 0, -3); + } else { + $arguments[] = [$name, $p[0], $default]; + } + } + + return [ + 'arguments' => $arguments, + 'rest_argument' => $restArgument, + ]; + } + + /** + * Returns the function prototype for the given positional and named arguments. + * + * If no exact match is found, finds the closest approximation. Note that this + * doesn't guarantee that $positional and $names are valid for the returned + * prototype. + * + * @param array[] $prototypes + * @param int $positional + * @param array $names A set of names, as both keys and values + * + * @return array + * + * @phpstan-param non-empty-list, rest_argument: string|null}> $prototypes + * @phpstan-return array{arguments: list, rest_argument: string|null} + */ + private function selectFunctionPrototype(array $prototypes, $positional, array $names) + { + $fuzzyMatch = null; + $minMismatchDistance = null; + + foreach ($prototypes as $prototype) { + // Ideally, find an exact match. + if ($this->checkPrototypeMatches($prototype, $positional, $names)) { + return $prototype; + } + + $mismatchDistance = \count($prototype['arguments']) - $positional; + + if ($minMismatchDistance !== null) { + if (abs($mismatchDistance) > abs($minMismatchDistance)) { + continue; + } + + // If two overloads have the same mismatch distance, favor the overload + // that has more arguments. + if (abs($mismatchDistance) === abs($minMismatchDistance) && $mismatchDistance < 0) { + continue; + } + } + + $minMismatchDistance = $mismatchDistance; + $fuzzyMatch = $prototype; + } + + return $fuzzyMatch; + } + + /** + * Checks whether the argument invocation matches the callable prototype. + * + * The rules are similar to {@see verifyPrototype}. The boolean return value + * avoids the overhead of building and catching exceptions when the reason of + * not matching the prototype does not need to be known. + * + * @param array $prototype + * @param int $positional + * @param array $names + * + * @return bool + * + * @phpstan-param array{arguments: list, rest_argument: string|null} $prototype + */ + private function checkPrototypeMatches(array $prototype, $positional, array $names) + { + $nameUsed = 0; + + foreach ($prototype['arguments'] as $i => $argument) { + list ($name, $originalName, $default) = $argument; + + if ($i < $positional) { + if (isset($names[$name])) { + return false; + } + } elseif (isset($names[$name])) { + $nameUsed++; + } elseif ($default === null) { + return false; + } + } + + if ($prototype['rest_argument'] !== null) { + return true; + } + + if ($positional > \count($prototype['arguments'])) { + return false; + } + + if ($nameUsed < \count($names)) { + return false; + } + + return true; + } + + /** + * Verifies that the argument invocation is valid for the callable prototype. + * + * @param array $prototype + * @param int $positional + * @param array $names + * @param bool $hasSplat + * + * @return void + * + * @throws SassScriptException + * + * @phpstan-param array{arguments: list, rest_argument: string|null} $prototype + */ + private function verifyPrototype(array $prototype, $positional, array $names, $hasSplat) + { + $nameUsed = 0; + + foreach ($prototype['arguments'] as $i => $argument) { + list ($name, $originalName, $default) = $argument; + + if ($i < $positional) { + if (isset($names[$name])) { + throw new SassScriptException(sprintf('Argument $%s was passed both by position and by name.', $originalName)); + } + } elseif (isset($names[$name])) { + $nameUsed++; + } elseif ($default === null) { + throw new SassScriptException(sprintf('Missing argument $%s', $originalName)); + } + } + + if ($prototype['rest_argument'] !== null) { + return; + } + + if ($positional > \count($prototype['arguments'])) { + $message = sprintf( + 'Only %d %sargument%s allowed, but %d %s passed.', + \count($prototype['arguments']), + empty($names) ? '' : 'positional ', + \count($prototype['arguments']) === 1 ? '' : 's', + $positional, + $positional === 1 ? 'was' : 'were' + ); + if (!$hasSplat) { + throw new SassScriptException($message); + } + + $message = $this->addLocationToMessage($message); + $message .= "\nThis will be an error in future versions of Sass."; + $this->logger->warn($message, true); + } + + if ($nameUsed < \count($names)) { + $unknownNames = array_values(array_diff($names, array_column($prototype['arguments'], 0))); + $lastName = array_pop($unknownNames); + $message = sprintf( + 'No argument%s named $%s%s.', + $unknownNames ? 's' : '', + $unknownNames ? implode(', $', $unknownNames) . ' or $' : '', + $lastName + ); + throw new SassScriptException($message); + } + } + + /** + * Evaluates the argument from the invocation. + * + * This returns several things about this invocation: + * - the list of positional arguments + * - the map of named arguments, indexed by normalized names + * - the set of names used in the arguments (that's an array using the normalized names as keys for O(1) access) + * - the separator used by the list using the splat operator, if any + * - a boolean indicator whether any splat argument (list or map) was used, to support the incomplete error reporting. + * + * @param array[] $args + * @param bool $reduce Whether arguments should be reduced to their value + * + * @return array + * + * @throws SassScriptException + * + * @phpstan-return array{0: list, 1: array, 2: array, 3: string|null, 4: bool} + */ + private function evaluateArguments(array $args, $reduce = true) + { + // this represents trailing commas + if (\count($args) && end($args) === static::$null) { + array_pop($args); + } + + $splatSeparator = null; + $keywordArgs = []; + $names = []; + $positionalArgs = []; + $hasKeywordArgument = false; + $hasSplat = false; + + foreach ($args as $arg) { + if (!empty($arg[0])) { + $hasKeywordArgument = true; + + assert(\is_string($arg[0][1])); + $name = str_replace('_', '-', $arg[0][1]); + + if (isset($keywordArgs[$name])) { + throw new SassScriptException(sprintf('Duplicate named argument $%s.', $arg[0][1])); + } + + $keywordArgs[$name] = $this->maybeReduce($reduce, $arg[1]); + $names[$name] = $name; + } elseif (! empty($arg[2])) { + // $arg[2] means a var followed by ... in the arg ($list... ) + $val = $this->reduce($arg[1], true); + $hasSplat = true; + + if ($val[0] === Type::T_LIST) { + foreach ($val[2] as $item) { + if (\is_null($splatSeparator)) { + $splatSeparator = $val[1]; + } + + $positionalArgs[] = $this->maybeReduce($reduce, $item); + } + + if (isset($val[3]) && \is_array($val[3])) { + foreach ($val[3] as $name => $item) { + assert(\is_string($name)); + + $normalizedName = str_replace('_', '-', $name); + + if (isset($keywordArgs[$normalizedName])) { + throw new SassScriptException(sprintf('Duplicate named argument $%s.', $name)); + } + + $keywordArgs[$normalizedName] = $this->maybeReduce($reduce, $item); + $names[$normalizedName] = $normalizedName; + $hasKeywordArgument = true; + } + } + } elseif ($val[0] === Type::T_MAP) { + foreach ($val[1] as $i => $name) { + $name = $this->compileStringContent($this->coerceString($name)); + $item = $val[2][$i]; + + if (! is_numeric($name)) { + $normalizedName = str_replace('_', '-', $name); + + if (isset($keywordArgs[$normalizedName])) { + throw new SassScriptException(sprintf('Duplicate named argument $%s.', $name)); + } + + $keywordArgs[$normalizedName] = $this->maybeReduce($reduce, $item); + $names[$normalizedName] = $normalizedName; + $hasKeywordArgument = true; + } else { + if (\is_null($splatSeparator)) { + $splatSeparator = $val[1]; + } + + $positionalArgs[] = $this->maybeReduce($reduce, $item); + } + } + } elseif ($val[0] !== Type::T_NULL) { // values other than null are treated a single-element lists, while null is the empty list + $positionalArgs[] = $this->maybeReduce($reduce, $val); + } + } elseif ($hasKeywordArgument) { + throw new SassScriptException('Positional arguments must come before keyword arguments.'); + } else { + $positionalArgs[] = $this->maybeReduce($reduce, $arg[1]); + } + } + + return [$positionalArgs, $keywordArgs, $names, $splatSeparator, $hasSplat]; + } + + /** + * @param bool $reduce + * @param array|Number $value + * + * @return array|Number + */ + private function maybeReduce($reduce, $value) + { + if ($reduce) { + return $this->reduce($value, true); + } + + return $value; + } + /** * Apply argument values per definition * - * @param array $argDef - * @param array $argValues + * @param array[] $argDef + * @param array|null $argValues * @param boolean $storeInEnv * @param boolean $reduce * only used if $storeInEnv = false * - * @return array + * @return array + * + * @phpstan-param list $argDef * * @throws \Exception */ @@ -5996,8 +6678,8 @@ class Compiler { $output = []; - if (\is_array($argValues) && \count($argValues) && end($argValues) === static::$null) { - array_pop($argValues); + if (\is_null($argValues)) { + $argValues = []; } if ($storeInEnv) { @@ -6007,151 +6689,47 @@ class Compiler $env->store = $storeEnv->store; } - $hasVariable = false; - $args = []; + $prototype = ['arguments' => [], 'rest_argument' => null]; + $originalRestArgumentName = null; foreach ($argDef as $i => $arg) { - list($name, $default, $isVariable) = $argDef[$i]; - - $args[$name] = [$i, $name, $default, $isVariable]; - $hasVariable |= $isVariable; - } - - $splatSeparator = null; - $keywordArgs = []; - $deferredKeywordArgs = []; - $deferredNamedKeywordArgs = []; - $remaining = []; - $hasKeywordArgument = false; - - // assign the keyword args - foreach ((array) $argValues as $arg) { - if (! empty($arg[0])) { - $hasKeywordArgument = true; - - $name = $arg[0][1]; - - if (! isset($args[$name])) { - foreach (array_keys($args) as $an) { - if (str_replace('_', '-', $an) === str_replace('_', '-', $name)) { - $name = $an; - break; - } - } - } - - if (! isset($args[$name]) || $args[$name][3]) { - if ($hasVariable) { - $deferredNamedKeywordArgs[$name] = $arg[1]; - } else { - throw $this->error("Mixin or function doesn't have an argument named $%s.", $arg[0][1]); - } - } elseif ($args[$name][0] < \count($remaining)) { - throw $this->error("The argument $%s was passed both by position and by name.", $arg[0][1]); - } else { - $keywordArgs[$name] = $arg[1]; - } - } elseif (! empty($arg[2])) { - // $arg[2] means a var followed by ... in the arg ($list... ) - $val = $this->reduce($arg[1], true); - - if ($val[0] === Type::T_LIST) { - foreach ($val[2] as $name => $item) { - if (! is_numeric($name)) { - if (! isset($args[$name])) { - foreach (array_keys($args) as $an) { - if (str_replace('_', '-', $an) === str_replace('_', '-', $name)) { - $name = $an; - break; - } - } - } - - if ($hasVariable) { - $deferredKeywordArgs[$name] = $item; - } else { - $keywordArgs[$name] = $item; - } - } else { - if (\is_null($splatSeparator)) { - $splatSeparator = $val[1]; - } - - $remaining[] = $item; - } - } - } elseif ($val[0] === Type::T_MAP) { - foreach ($val[1] as $i => $name) { - $name = $this->compileStringContent($this->coerceString($name)); - $item = $val[2][$i]; - - if (! is_numeric($name)) { - if (! isset($args[$name])) { - foreach (array_keys($args) as $an) { - if (str_replace('_', '-', $an) === str_replace('_', '-', $name)) { - $name = $an; - break; - } - } - } - - if ($hasVariable) { - $deferredKeywordArgs[$name] = $item; - } else { - $keywordArgs[$name] = $item; - } - } else { - if (\is_null($splatSeparator)) { - $splatSeparator = $val[1]; - } - - $remaining[] = $item; - } - } - } else { - $remaining[] = $val; - } - } elseif ($hasKeywordArgument) { - throw $this->error('Positional arguments must come before keyword arguments.'); - } else { - $remaining[] = $arg[1]; - } - } - - foreach ($args as $arg) { - list($i, $name, $default, $isVariable) = $arg; + list($name, $default, $isVariable) = $arg; + $normalizedName = str_replace('_', '-', $name); if ($isVariable) { - // only if more than one arg : can not be passed as position and value - // see https://github.com/sass/libsass/issues/2927 - if (count($args) > 1) { - if (isset($remaining[$i]) && isset($deferredNamedKeywordArgs[$name])) { - throw $this->error("The argument $%s was passed both by position and by name.", $name); - } - } - - $val = [Type::T_LIST, \is_null($splatSeparator) ? ',' : $splatSeparator , [], $isVariable]; - - for ($count = \count($remaining); $i < $count; $i++) { - $val[2][] = $remaining[$i]; - } - - foreach ($deferredKeywordArgs as $itemName => $item) { - $val[2][$itemName] = $item; - } - - foreach ($deferredNamedKeywordArgs as $itemName => $item) { - $val[2][$itemName] = $item; - } - } elseif (isset($remaining[$i])) { - $val = $remaining[$i]; - } elseif (isset($keywordArgs[$name])) { - $val = $keywordArgs[$name]; - } elseif (! empty($default)) { - continue; + $originalRestArgumentName = $name; + $prototype['rest_argument'] = $normalizedName; } else { - throw $this->error("Missing argument $name"); + $prototype['arguments'][] = [$normalizedName, $name, !empty($default) ? $default : null]; } + } + + list($positionalArgs, $namedArgs, $names, $splatSeparator, $hasSplat) = $this->evaluateArguments($argValues, $reduce); + + $this->verifyPrototype($prototype, \count($positionalArgs), $names, $hasSplat); + + $vars = $this->applyArgumentsToDeclaration($prototype, $positionalArgs, $namedArgs, $splatSeparator); + + foreach ($prototype['arguments'] as $argument) { + list($normalizedName, $name) = $argument; + + if (!isset($vars[$normalizedName])) { + continue; + } + + $val = $vars[$normalizedName]; + + if ($storeInEnv) { + $this->set($name, $this->reduce($val, true), true, $env); + } else { + $output[$name] = ($reduce ? $this->reduce($val, true) : $val); + } + } + + if ($prototype['rest_argument'] !== null) { + assert($originalRestArgumentName !== null); + $name = $originalRestArgumentName; + $val = $vars[$prototype['rest_argument']]; if ($storeInEnv) { $this->set($name, $this->reduce($val, true), true, $env); @@ -6164,12 +6742,13 @@ class Compiler $storeEnv->store = $env->store; } - foreach ($args as $arg) { - list($i, $name, $default, $isVariable) = $arg; + foreach ($prototype['arguments'] as $argument) { + list($normalizedName, $name, $default) = $argument; - if ($isVariable || isset($remaining[$i]) || isset($keywordArgs[$name]) || empty($default)) { + if (isset($vars[$normalizedName])) { continue; } + assert($default !== null); if ($storeInEnv) { $this->set($name, $this->reduce($default, true), true); @@ -6181,6 +6760,67 @@ class Compiler return $output; } + /** + * Apply argument values per definition. + * + * This method assumes that the arguments are valid for the provided prototype. + * The validation with {@see verifyPrototype} must have been run before calling + * it. + * Arguments are returned as a map from the normalized argument names to the + * value. Additional arguments are collected in a sass argument list available + * under the name of the rest argument in the result. + * + * Defaults are not applied as they are resolved in a different environment. + * + * @param array $prototype + * @param array $positionalArgs + * @param array $namedArgs + * @param string|null $splatSeparator + * + * @return array + * + * @phpstan-param array{arguments: list, rest_argument: string|null} $prototype + */ + private function applyArgumentsToDeclaration(array $prototype, array $positionalArgs, array $namedArgs, $splatSeparator) + { + $output = []; + $minLength = min(\count($positionalArgs), \count($prototype['arguments'])); + + for ($i = 0; $i < $minLength; $i++) { + list($name) = $prototype['arguments'][$i]; + $val = $positionalArgs[$i]; + + $output[$name] = $val; + } + + $restNamed = $namedArgs; + + for ($i = \count($positionalArgs); $i < \count($prototype['arguments']); $i++) { + $argument = $prototype['arguments'][$i]; + list($name) = $argument; + + if (isset($namedArgs[$name])) { + $val = $namedArgs[$name]; + unset($restNamed[$name]); + } else { + continue; + } + + $output[$name] = $val; + } + + if ($prototype['rest_argument'] !== null) { + $name = $prototype['rest_argument']; + $rest = array_values(array_slice($positionalArgs, \count($prototype['arguments']))); + + $val = [Type::T_LIST, \is_null($splatSeparator) ? ',' : $splatSeparator , $rest, $restNamed]; + + $output[$name] = $val; + } + + return $output; + } + /** * Coerce a php value into a scss one * @@ -6190,7 +6830,7 @@ class Compiler */ protected function coerceValue($value) { - if (\is_array($value) || $value instanceof \ArrayAccess) { + if (\is_array($value) || $value instanceof Number) { return $value; } @@ -6234,9 +6874,8 @@ class Compiler } if ( - $item[0] === static::$emptyList[0] && - $item[1] === static::$emptyList[1] && - $item[2] === static::$emptyList[2] + $item[0] === Type::T_LIST && + $item[2] === [] ) { return static::$emptyMap; } @@ -6247,15 +6886,19 @@ class Compiler /** * Coerce something to list * - * @param array $item - * @param string $delim - * @param boolean $removeTrailingNull + * @param array|Number $item + * @param string $delim + * @param boolean $removeTrailingNull * * @return array */ protected function coerceList($item, $delim = ',', $removeTrailingNull = false) { - if (isset($item) && $item[0] === Type::T_LIST) { + if ($item instanceof Number) { + return [Type::T_LIST, $delim, [$item]]; + } + + if ($item[0] === Type::T_LIST) { // remove trailing null from the list if ($removeTrailingNull && end($item[2]) === static::$null) { array_pop($item[2]); @@ -6264,7 +6907,7 @@ class Compiler return $item; } - if (isset($item) && $item[0] === Type::T_MAP) { + if ($item[0] === Type::T_MAP) { $keys = $item[1]; $values = $item[2]; $list = []; @@ -6295,7 +6938,7 @@ class Compiler return [Type::T_LIST, ',', $list]; } - return [Type::T_LIST, $delim, ! isset($item) ? [] : [$item]]; + return [Type::T_LIST, $delim, [$item]]; } /** @@ -6324,6 +6967,10 @@ class Compiler */ protected function coerceColor($value, $inRGBFunction = false) { + if ($value instanceof Number) { + return null; + } + switch ($value[0]) { case Type::T_COLOR: for ($i = 1; $i <= 3; $i++) { @@ -6510,16 +7157,21 @@ class Compiler } /** - * Assert value is a string (or keyword) + * Assert value is a string + * + * This method deals with internal implementation details of the value + * representation where unquoted strings can sometimes be stored under + * other types. + * The returned value is always using the T_STRING type. * * @api * * @param array|Number $value - * @param string $varName + * @param string|null $varName * * @return array * - * @throws \Exception + * @throws SassScriptException */ public function assertString($value, $varName = null) { @@ -6530,13 +7182,10 @@ class Compiler if (! \in_array($value[0], [Type::T_STRING, Type::T_KEYWORD])) { $value = $this->compileValue($value); - $var_display = ($varName ? " \${$varName}:" : ''); - throw $this->error("Error:{$var_display} $value is not a string."); + throw SassScriptException::forArgument("$value is not a string.", $varName); } - $value = $this->coerceString($value); - - return $value; + return $this->coerceString($value); } /** @@ -6565,17 +7214,20 @@ class Compiler * @api * * @param array|Number $value + * @param string|null $varName * * @return array * - * @throws \Exception + * @throws SassScriptException */ - public function assertMap($value) + public function assertMap($value, $varName = null) { $value = $this->coerceMap($value); if ($value[0] !== Type::T_MAP) { - throw $this->error('expecting map, %s received', $value[0]); + $value = $this->compileValue($value); + + throw SassScriptException::forArgument("$value is not a map.", $varName); } return $value; @@ -6601,24 +7253,48 @@ class Compiler return $value; } + /** + * Gets the keywords of an argument list. + * + * Keys in the returned array are normalized names (underscores are replaced with dashes) + * without the leading `$`. + * Calling this helper with anything that an argument list received for a rest argument + * of the function argument declaration is not supported. + * + * @param array|Number $value + * + * @return array + */ + public function getArgumentListKeywords($value) + { + if ($value[0] !== Type::T_LIST || !isset($value[3]) || !\is_array($value[3])) { + throw new \InvalidArgumentException('The argument is not a sass argument list.'); + } + + return $value[3]; + } + /** * Assert value is a color * * @api * * @param array|Number $value + * @param string|null $varName * * @return array * - * @throws \Exception + * @throws SassScriptException */ - public function assertColor($value) + public function assertColor($value, $varName = null) { if ($color = $this->coerceColor($value)) { return $color; } - throw $this->error('expecting color, %s received', $value[0]); + $value = $this->compileValue($value); + + throw SassScriptException::forArgument("$value is not a color.", $varName); } /** @@ -6627,18 +7303,17 @@ class Compiler * @api * * @param array|Number $value - * @param string $varName + * @param string|null $varName * * @return Number * - * @throws \Exception + * @throws SassScriptException */ public function assertNumber($value, $varName = null) { if (!$value instanceof Number) { $value = $this->compileValue($value); - $var_display = ($varName ? " \${$varName}:" : ''); - throw $this->error("Error:{$var_display} $value is not a number."); + throw SassScriptException::forArgument("$value is not a number.", $varName); } return $value; @@ -6650,24 +7325,40 @@ class Compiler * @api * * @param array|Number $value - * @param string $varName + * @param string|null $varName * * @return integer * - * @throws \Exception + * @throws SassScriptException */ public function assertInteger($value, $varName = null) { - $value = $this->assertNumber($value, $varName)->getDimension(); if (round($value - \intval($value), Number::PRECISION) > 0) { - $var_display = ($varName ? " \${$varName}:" : ''); - throw $this->error("Error:{$var_display} $value is not an integer."); + throw SassScriptException::forArgument("$value is not an integer.", $varName); } return intval($value); } + /** + * Extract the ... / alpha on the last argument of channel arg + * in color functions + * + * @param array $args + * @return array + */ + private function extractSlashAlphaInColorFunction($args) + { + $last = end($args); + if (\count($args) === 3 && $last[0] === Type::T_EXPRESSION && $last[1] === '/') { + array_pop($args); + $args[] = $last[2]; + $args[] = $last[3]; + } + return $args; + } + /** * Make sure a color's components don't go out of bounds @@ -6686,6 +7377,10 @@ class Compiler if ($c[$i] > 255) { $c[$i] = 255; } + + if (!\is_int($c[$i])) { + $c[$i] = round($c[$i]); + } } return $c; @@ -6694,7 +7389,7 @@ class Compiler /** * Convert RGB to HSL * - * @api + * @internal * * @param integer $red * @param integer $green @@ -6766,11 +7461,11 @@ class Compiler /** * Convert HSL to RGB * - * @api + * @internal * - * @param integer $hue H from 0 to 360 - * @param integer $saturation S from 0 to 100 - * @param integer $lightness L from 0 to 100 + * @param int|float $hue H from 0 to 360 + * @param int|float $saturation S from 0 to 100 + * @param int|float $lightness L from 0 to 100 * * @return array */ @@ -6796,19 +7491,87 @@ class Compiler return $out; } + /** + * Convert HWB to RGB + * https://www.w3.org/TR/css-color-4/#hwb-to-rgb + * + * @api + * + * @param integer $hue H from 0 to 360 + * @param integer $whiteness W from 0 to 100 + * @param integer $blackness B from 0 to 100 + * + * @return array + */ + private function HWBtoRGB($hue, $whiteness, $blackness) + { + $w = min(100, max(0, $whiteness)) / 100; + $b = min(100, max(0, $blackness)) / 100; + + $sum = $w + $b; + if ($sum > 1.0) { + $w = $w / $sum; + $b = $b / $sum; + } + $b = min(1.0 - $w, $b); + + $rgb = $this->toRGB($hue, 100, 50); + for($i = 1; $i < 4; $i++) { + $rgb[$i] *= (1.0 - $w - $b); + $rgb[$i] = round($rgb[$i] + 255 * $w + 0.0001); + } + + return $rgb; + } + + /** + * Convert RGB to HWB + * + * @api + * + * @param integer $red + * @param integer $green + * @param integer $blue + * + * @return array + */ + private function RGBtoHWB($red, $green, $blue) + { + $min = min($red, $green, $blue); + $max = max($red, $green, $blue); + + $d = $max - $min; + + if ((int) $d === 0) { + $h = 0; + } else { + + if ($red == $max) { + $h = 60 * ($green - $blue) / $d; + } elseif ($green == $max) { + $h = 60 * ($blue - $red) / $d + 120; + } elseif ($blue == $max) { + $h = 60 * ($red - $green) / $d + 240; + } + } + + return [Type::T_HWB, fmod($h, 360), $min / 255 * 100, 100 - $max / 255 *100]; + } + + // Built in functions protected static $libCall = ['function', 'args...']; - protected function libCall($args, $kwargs) + protected function libCall($args) { - $functionReference = array_shift($args); + $functionReference = $args[0]; if (in_array($functionReference[0], [Type::T_STRING, Type::T_KEYWORD])) { $name = $this->compileStringContent($this->coerceString($functionReference)); - $warning = "DEPRECATION WARNING: Passing a string to call() is deprecated and will be illegal\n" + $warning = "Passing a string to call() is deprecated and will be illegal\n" . "in Sass 4.0. Use call(function-reference($name)) instead."; - fwrite($this->stderr, "$warning\n\n"); - $functionReference = $this->libGetFunction([$functionReference]); + Warn::deprecation($warning); + $functionReference = $this->libGetFunction([$this->assertString($functionReference, 'function')]); } if ($functionReference === static::$null) { @@ -6819,18 +7582,9 @@ class Compiler throw $this->error('Function reference expected, got ' . $functionReference[0]); } - $callArgs = []; - - // $kwargs['args'] is [Type::T_LIST, ',', [..]] - foreach ($kwargs['args'][2] as $varname => $arg) { - if (is_numeric($varname)) { - $varname = null; - } else { - $varname = [ 'var', $varname]; - } - - $callArgs[] = [$varname, $arg, false]; - } + $callArgs = [ + [null, $args[1], true] + ]; return $this->reduce([Type::T_FUNCTION_CALL, $functionReference, $callArgs]); } @@ -6842,7 +7596,7 @@ class Compiler ]; protected function libGetFunction($args) { - $name = $this->compileStringContent($this->coerceString(array_shift($args))); + $name = $this->compileStringContent($this->assertString(array_shift($args), 'name')); $isCss = false; if (count($args)) { @@ -6912,7 +7666,7 @@ class Compiler $key = array_search($this->normalizeValue($value), $values); - return false === $key ? static::$null : $key + 1; + return false === $key ? static::$null : new Number($key + 1, ''); } protected static $libRgb = [ @@ -6983,36 +7737,130 @@ class Compiler * Helper function for adjust_color, change_color, and scale_color * * @param array $args + * @param string $operation * @param callable $fn * * @return array + * + * @phpstan-param callable(float|int, float|int|null, float|int): (float|int) $fn */ - protected function alterColor($args, $fn) + protected function alterColor(array $args, $operation, $fn) { - $color = $this->assertColor($args[0]); + $color = $this->assertColor($args[0], 'color'); - foreach ([1 => 1, 2 => 2, 3 => 3, 7 => 4] as $iarg => $irgba) { - if (isset($args[$iarg])) { - $val = $this->assertNumber($args[$iarg])->getDimension(); - - if (! isset($color[$irgba])) { - $color[$irgba] = (($irgba < 4) ? 0 : 1); - } - - $color[$irgba] = \call_user_func($fn, $color[$irgba], $val, $iarg); - } + if ($args[1][2]) { + throw new SassScriptException('Only one positional argument is allowed. All other arguments must be passed by name.'); } - if (! empty($args[4]) || ! empty($args[5]) || ! empty($args[6])) { - $hsl = $this->toHSL($color[1], $color[2], $color[3]); + $kwargs = $this->getArgumentListKeywords($args[1]); - foreach ([4 => 1, 5 => 2, 6 => 3] as $iarg => $ihsl) { - if (! empty($args[$iarg])) { - $val = $this->assertNumber($args[$iarg])->getDimension(); - $hsl[$ihsl] = \call_user_func($fn, $hsl[$ihsl], $val, $iarg); + $scale = $operation === 'scale'; + $change = $operation === 'change'; + + /** + * @param string $name + * @param float|int $max + * @param bool $checkPercent + * @param bool $assertPercent + * + * @return float|int|null + */ + $getParam = function ($name, $max, $checkPercent = false, $assertPercent = false) use (&$kwargs, $scale, $change) { + if (!isset($kwargs[$name])) { + return null; + } + + $number = $this->assertNumber($kwargs[$name], $name); + unset($kwargs[$name]); + + if (!$scale && $checkPercent) { + if (!$number->hasUnit('%')) { + $warning = $this->error("{$name} Passing a number `$number` without unit % is deprecated."); + $this->logger->warn($warning->getMessage(), true); } } + if ($scale || $assertPercent) { + $number->assertUnit('%', $name); + } + + if ($scale) { + $max = 100; + } + + return $number->valueInRange($change ? 0 : -$max, $max, $name); + }; + + $alpha = $getParam('alpha', 1); + $red = $getParam('red', 255); + $green = $getParam('green', 255); + $blue = $getParam('blue', 255); + + if ($scale || !isset($kwargs['hue'])) { + $hue = null; + } else { + $hueNumber = $this->assertNumber($kwargs['hue'], 'hue'); + unset($kwargs['hue']); + $hue = $hueNumber->getDimension(); + } + $saturation = $getParam('saturation', 100, true); + $lightness = $getParam('lightness', 100, true); + $whiteness = $getParam('whiteness', 100, false, true); + $blackness = $getParam('blackness', 100, false, true); + + if (!empty($kwargs)) { + $unknownNames = array_keys($kwargs); + $lastName = array_pop($unknownNames); + $message = sprintf( + 'No argument%s named $%s%s.', + $unknownNames ? 's' : '', + $unknownNames ? implode(', $', $unknownNames) . ' or $' : '', + $lastName + ); + throw new SassScriptException($message); + } + + $hasRgb = $red !== null || $green !== null || $blue !== null; + $hasSL = $saturation !== null || $lightness !== null; + $hasWB = $whiteness !== null || $blackness !== null; + $found = false; + + if ($hasRgb && ($hasSL || $hasWB || $hue !== null)) { + throw new SassScriptException(sprintf('RGB parameters may not be passed along with %s parameters.', $hasWB ? 'HWB' : 'HSL')); + } + + if ($hasWB && $hasSL) { + throw new SassScriptException('HSL parameters may not be passed along with HWB parameters.'); + } + + if ($hasRgb) { + $color[1] = round($fn($color[1], $red, 255)); + $color[2] = round($fn($color[2], $green, 255)); + $color[3] = round($fn($color[3], $blue, 255)); + } elseif ($hasWB) { + $hwb = $this->RGBtoHWB($color[1], $color[2], $color[3]); + if ($hue !== null) { + $hwb[1] = $change ? $hue : $hwb[1] + $hue; + } + $hwb[2] = $fn($hwb[2], $whiteness, 100); + $hwb[3] = $fn($hwb[3], $blackness, 100); + + $rgb = $this->HWBtoRGB($hwb[1], $hwb[2], $hwb[3]); + + if (isset($color[4])) { + $rgb[4] = $color[4]; + } + + $color = $rgb; + } elseif ($hue !== null || $hasSL) { + $hsl = $this->toHSL($color[1], $color[2], $color[3]); + + if ($hue !== null) { + $hsl[1] = $change ? $hue : $hsl[1] + $hue; + } + $hsl[2] = $fn($hsl[2], $saturation, 100); + $hsl[3] = $fn($hsl[3], $lightness, 100); + $rgb = $this->toRGB($hsl[1], $hsl[2], $hsl[3]); if (isset($color[4])) { @@ -7022,58 +7870,54 @@ class Compiler $color = $rgb; } + if ($alpha !== null) { + $existingAlpha = isset($color[4]) ? $color[4] : 1; + $color[4] = $fn($existingAlpha, $alpha, 1); + } + return $color; } - protected static $libAdjustColor = [ - 'color', 'red:null', 'green:null', 'blue:null', - 'hue:null', 'saturation:null', 'lightness:null', 'alpha:null' - ]; + protected static $libAdjustColor = ['color', 'kwargs...']; protected function libAdjustColor($args) { - return $this->alterColor($args, function ($base, $alter, $i) { - return $base + $alter; + return $this->alterColor($args, 'adjust', function ($base, $alter, $max) { + if ($alter === null) { + return $base; + } + + $new = $base + $alter; + + if ($new < 0) { + return 0; + } + + if ($new > $max) { + return $max; + } + + return $new; }); } - protected static $libChangeColor = [ - 'color', 'red:null', 'green:null', 'blue:null', - 'hue:null', 'saturation:null', 'lightness:null', 'alpha:null' - ]; + protected static $libChangeColor = ['color', 'kwargs...']; protected function libChangeColor($args) { - return $this->alterColor($args, function ($base, $alter, $i) { + return $this->alterColor($args,'change', function ($base, $alter, $max) { + if ($alter === null) { + return $base; + } + return $alter; }); } - protected static $libScaleColor = [ - 'color', 'red:null', 'green:null', 'blue:null', - 'hue:null', 'saturation:null', 'lightness:null', 'alpha:null' - ]; + protected static $libScaleColor = ['color', 'kwargs...']; protected function libScaleColor($args) { - return $this->alterColor($args, function ($base, $scale, $i) { - // 1, 2, 3 - rgb - // 4, 5, 6 - hsl - // 7 - a - switch ($i) { - case 1: - case 2: - case 3: - $max = 255; - break; - - case 4: - $max = 360; - break; - - case 7: - $max = 1; - break; - - default: - $max = 100; + return $this->alterColor($args, 'scale', function ($base, $scale, $max) { + if ($scale === null) { + return $base; } $scale = $scale / 100; @@ -7109,7 +7953,7 @@ class Compiler throw $this->error('Error: argument `$color` of `red($color)` must be a color'); } - return $color[1]; + return new Number((int) $color[1], ''); } protected static $libGreen = ['color']; @@ -7121,7 +7965,7 @@ class Compiler throw $this->error('Error: argument `$color` of `green($color)` must be a color'); } - return $color[2]; + return new Number((int) $color[2], ''); } protected static $libBlue = ['color']; @@ -7133,14 +7977,14 @@ class Compiler throw $this->error('Error: argument `$color` of `blue($color)` must be a color'); } - return $color[3]; + return new Number((int) $color[3], ''); } protected static $libAlpha = ['color']; protected function libAlpha($args) { if ($color = $this->coerceColor($args[0])) { - return isset($color[4]) ? $color[4] : 1; + return new Number(isset($color[4]) ? $color[4] : 1, ''); } // this might be the IE function, so return value unchanged @@ -7168,14 +8012,9 @@ class Compiler { list($first, $second, $weight) = $args; - $first = $this->assertColor($first); - $second = $this->assertColor($second); - - if (! isset($weight)) { - $weight = 0.5; - } else { - $weight = $this->coercePercent($weight); - } + $first = $this->assertColor($first, 'color1'); + $second = $this->assertColor($second, 'color2'); + $weight = $this->coercePercent($this->assertNumber($weight, 'weight')); $firstAlpha = isset($first[4]) ? $first[4] : 1; $secondAlpha = isset($second[4]) ? $second[4] : 1; @@ -7201,6 +8040,7 @@ class Compiler protected static $libHsl = [ ['channels'], + ['hue', 'saturation'], ['hue', 'saturation', 'lightness'], ['hue', 'saturation', 'lightness', 'alpha'] ]; protected function libHsl($args, $kwargs, $funcName = 'hsl') @@ -7216,14 +8056,25 @@ class Compiler $args_to_check = $kwargs['channels'][2]; } + if (\count($args) === 2) { + // if var() is used as an argument, return as a css function + foreach ($args as $arg) { + if ($arg[0] === Type::T_FUNCTION && in_array($arg[1], ['var'])) { + return null; + } + } + + throw new SassScriptException('Missing argument $lightness.'); + } + foreach ($kwargs as $k => $arg) { - if (in_array($arg[0], [Type::T_FUNCTION_CALL]) && in_array($arg[1], ['min', 'max'])) { + if (in_array($arg[0], [Type::T_FUNCTION_CALL, Type::T_FUNCTION]) && in_array($arg[1], ['min', 'max'])) { return null; } } foreach ($args_to_check as $k => $arg) { - if (in_array($arg[0], [Type::T_FUNCTION_CALL]) && in_array($arg[1], ['min', 'max'])) { + if (in_array($arg[0], [Type::T_FUNCTION_CALL, Type::T_FUNCTION]) && in_array($arg[1], ['min', 'max'])) { if (count($kwargs) > 1 || ($k >= 2 && count($args) === 4)) { return null; } @@ -7275,6 +8126,7 @@ class Compiler protected static $libHsla = [ ['channels'], + ['hue', 'saturation'], ['hue', 'saturation', 'lightness'], ['hue', 'saturation', 'lightness', 'alpha']]; protected function libHsla($args, $kwargs) @@ -7285,7 +8137,7 @@ class Compiler protected static $libHue = ['color']; protected function libHue($args) { - $color = $this->assertColor($args[0]); + $color = $this->assertColor($args[0], 'color'); $hsl = $this->toHSL($color[1], $color[2], $color[3]); return new Number($hsl[1], 'deg'); @@ -7294,7 +8146,7 @@ class Compiler protected static $libSaturation = ['color']; protected function libSaturation($args) { - $color = $this->assertColor($args[0]); + $color = $this->assertColor($args[0], 'color'); $hsl = $this->toHSL($color[1], $color[2], $color[3]); return new Number($hsl[2], '%'); @@ -7303,12 +8155,138 @@ class Compiler protected static $libLightness = ['color']; protected function libLightness($args) { - $color = $this->assertColor($args[0]); + $color = $this->assertColor($args[0], 'color'); $hsl = $this->toHSL($color[1], $color[2], $color[3]); return new Number($hsl[3], '%'); } + /* + * Todo : a integrer dans le futur module color + protected static $libHwb = [ + ['channels'], + ['hue', 'whiteness', 'blackness'], + ['hue', 'whiteness', 'blackness', 'alpha'] ]; + protected function libHwb($args, $kwargs, $funcName = 'hwb') + { + $args_to_check = $args; + + if (\count($args) == 1) { + if ($args[0][0] !== Type::T_LIST) { + throw $this->error("Missing elements \$whiteness and \$blackness"); + } + + if (\trim($args[0][1])) { + throw $this->error("\$channels must be a space-separated list."); + } + + if (! empty($args[0]['enclosing'])) { + throw $this->error("\$channels must be an unbracketed list."); + } + + $args = $args[0][2]; + if (\count($args) > 3) { + throw $this->error("hwb() : Only 3 elements are allowed but ". \count($args) . "were passed"); + } + + $args_to_check = $this->extractSlashAlphaInColorFunction($kwargs['channels'][2]); + if (\count($args_to_check) !== \count($kwargs['channels'][2])) { + $args = $args_to_check; + } + } + + if (\count($args_to_check) < 2) { + throw $this->error("Missing elements \$whiteness and \$blackness"); + } + if (\count($args_to_check) < 3) { + throw $this->error("Missing element \$blackness"); + } + if (\count($args_to_check) > 4) { + throw $this->error("hwb() : Only 4 elements are allowed but ". \count($args) . "were passed"); + } + + foreach ($kwargs as $k => $arg) { + if (in_array($arg[0], [Type::T_FUNCTION_CALL]) && in_array($arg[1], ['min', 'max'])) { + return null; + } + } + + foreach ($args_to_check as $k => $arg) { + if (in_array($arg[0], [Type::T_FUNCTION_CALL]) && in_array($arg[1], ['min', 'max'])) { + if (count($kwargs) > 1 || ($k >= 2 && count($args) === 4)) { + return null; + } + + $args[$k] = $this->stringifyFncallArgs($arg); + } + + if ( + $k >= 2 && count($args) === 4 && + in_array($arg[0], [Type::T_FUNCTION_CALL, Type::T_FUNCTION]) && + in_array($arg[1], ['calc','env']) + ) { + return null; + } + } + + $hue = $this->reduce($args[0]); + $whiteness = $this->reduce($args[1]); + $blackness = $this->reduce($args[2]); + $alpha = null; + + if (\count($args) === 4) { + $alpha = $this->compileColorPartValue($args[3], 0, 1, false); + + if (! \is_numeric($alpha)) { + $val = $this->compileValue($args[3]); + throw $this->error("\$alpha: $val is not a number"); + } + } + + $this->assertNumber($hue, 'hue'); + $this->assertUnit($whiteness, ['%'], 'whiteness'); + $this->assertUnit($blackness, ['%'], 'blackness'); + + $this->assertRange($whiteness, 0, 100, "0% and 100%", "whiteness"); + $this->assertRange($blackness, 0, 100, "0% and 100%", "blackness"); + + $w = $whiteness->getDimension(); + $b = $blackness->getDimension(); + + $hueValue = $hue->getDimension() % 360; + + while ($hueValue < 0) { + $hueValue += 360; + } + + $color = $this->HWBtoRGB($hueValue, $w, $b); + + if (! \is_null($alpha)) { + $color[4] = $alpha; + } + + return $color; + } + + protected static $libWhiteness = ['color']; + protected function libWhiteness($args, $kwargs, $funcName = 'whiteness') { + + $color = $this->assertColor($args[0]); + $hwb = $this->RGBtoHWB($color[1], $color[2], $color[3]); + + return new Number($hwb[2], '%'); + } + + protected static $libBlackness = ['color']; + protected function libBlackness($args, $kwargs, $funcName = 'blackness') { + + $color = $this->assertColor($args[0]); + $hwb = $this->RGBtoHWB($color[1], $color[2], $color[3]); + + return new Number($hwb[3], '%'); + } + */ + protected function adjustHsl($color, $idx, $amount) { $hsl = $this->toHSL($color[1], $color[2], $color[3]); @@ -7325,8 +8303,8 @@ class Compiler protected static $libAdjustHue = ['color', 'degrees']; protected function libAdjustHue($args) { - $color = $this->assertColor($args[0]); - $degrees = $this->assertNumber($args[1])->getDimension(); + $color = $this->assertColor($args[0], 'color'); + $degrees = $this->assertNumber($args[1], 'degrees')->getDimension(); return $this->adjustHsl($color, 1, $degrees); } @@ -7334,7 +8312,7 @@ class Compiler protected static $libLighten = ['color', 'amount']; protected function libLighten($args) { - $color = $this->assertColor($args[0]); + $color = $this->assertColor($args[0], 'color'); $amount = Util::checkRange('amount', new Range(0, 100), $args[1], '%'); return $this->adjustHsl($color, 3, $amount); @@ -7343,7 +8321,7 @@ class Compiler protected static $libDarken = ['color', 'amount']; protected function libDarken($args) { - $color = $this->assertColor($args[0]); + $color = $this->assertColor($args[0], 'color'); $amount = Util::checkRange('amount', new Range(0, 100), $args[1], '%'); return $this->adjustHsl($color, 3, -$amount); @@ -7354,17 +8332,14 @@ class Compiler { $value = $args[0]; - if ($value instanceof Number) { + if (count($args) === 1) { + $this->assertNumber($args[0], 'amount'); + return null; } - if (count($args) === 1) { - $val = $this->compileValue($value); - throw $this->error("\$amount: $val is not a number"); - } - - $color = $this->assertColor($value); - $amount = 100 * $this->coercePercent($args[1]); + $color = $this->assertColor($value, 'color'); + $amount = 100 * $this->coercePercent($this->assertNumber($args[1], 'amount')); return $this->adjustHsl($color, 2, $amount); } @@ -7372,8 +8347,8 @@ class Compiler protected static $libDesaturate = ['color', 'amount']; protected function libDesaturate($args) { - $color = $this->assertColor($args[0]); - $amount = 100 * $this->coercePercent($args[1]); + $color = $this->assertColor($args[0], 'color'); + $amount = 100 * $this->coercePercent($this->assertNumber($args[1], 'amount')); return $this->adjustHsl($color, 2, -$amount); } @@ -7387,31 +8362,27 @@ class Compiler return null; } - return $this->adjustHsl($this->assertColor($value), 2, -100); + return $this->adjustHsl($this->assertColor($value, 'color'), 2, -100); } protected static $libComplement = ['color']; protected function libComplement($args) { - return $this->adjustHsl($this->assertColor($args[0]), 1, 180); + return $this->adjustHsl($this->assertColor($args[0], 'color'), 1, 180); } protected static $libInvert = ['color', 'weight:1']; protected function libInvert($args) { - list($value, $weight) = $args; - - if (! isset($weight)) { - $weight = 1; - } else { - $weight = $this->coercePercent($weight); - } + $value = $args[0]; if ($value instanceof Number) { return null; } - $color = $this->assertColor($value); + $weight = $this->coercePercent($this->assertNumber($args[1], 'weight')); + + $color = $this->assertColor($value, 'color'); $inverted = $color; $inverted[1] = 255 - $inverted[1]; $inverted[2] = 255 - $inverted[2]; @@ -7428,8 +8399,8 @@ class Compiler protected static $libOpacify = ['color', 'amount']; protected function libOpacify($args) { - $color = $this->assertColor($args[0]); - $amount = $this->coercePercent($args[1]); + $color = $this->assertColor($args[0], 'color'); + $amount = $this->coercePercent($this->assertNumber($args[1], 'amount')); $color[4] = (isset($color[4]) ? $color[4] : 1) + $amount; $color[4] = min(1, max(0, $color[4])); @@ -7447,8 +8418,8 @@ class Compiler protected static $libTransparentize = ['color', 'amount']; protected function libTransparentize($args) { - $color = $this->assertColor($args[0]); - $amount = $this->coercePercent($args[1]); + $color = $this->assertColor($args[0], 'color'); + $amount = $this->coercePercent($this->assertNumber($args[1], 'amount')); $color[4] = (isset($color[4]) ? $color[4] : 1) - $amount; $color[4] = min(1, max(0, $color[4])); @@ -7465,26 +8436,34 @@ class Compiler protected static $libUnquote = ['string']; protected function libUnquote($args) { - $str = $args[0]; + try { + $str = $this->assertString($args[0], 'string'); + } catch (SassScriptException $e) { + $value = $this->compileValue($args[0]); + $fname = $this->getPrettyPath($this->sourceNames[$this->sourceIndex]); + $line = $this->sourceLine; - if ($str[0] === Type::T_STRING) { - $str[1] = ''; + $message = "Passing $value, a non-string value, to unquote() +will be an error in future versions of Sass.\n on line $line of $fname"; + + $this->logger->warn($message, true); + + return $args[0]; } + $str[1] = ''; + return $str; } protected static $libQuote = ['string']; protected function libQuote($args) { - $value = $args[0]; + $value = $this->assertString($args[0], 'string'); - if ($value[0] === Type::T_STRING && ! empty($value[1])) { - $value[1] = '"'; - return $value; - } + $value[1] = '"'; - return [Type::T_STRING, '"', [$value]]; + return $value; } protected static $libPercentage = ['number']; @@ -7528,6 +8507,7 @@ class Compiler return new Number(abs($num->getDimension()), $num->getNumeratorUnits(), $num->getDenominatorUnits()); } + protected static $libMin = ['numbers...']; protected function libMin($args) { /** @@ -7535,7 +8515,7 @@ class Compiler */ $min = null; - foreach ($args as $arg) { + foreach ($args[0][2] as $arg) { $number = $this->assertNumber($arg); if (\is_null($min) || $min->greaterThan($number)) { @@ -7550,6 +8530,7 @@ class Compiler throw $this->error('At least one argument must be passed.'); } + protected static $libMax = ['numbers...']; protected function libMax($args) { /** @@ -7557,7 +8538,7 @@ class Compiler */ $max = null; - foreach ($args as $arg) { + foreach ($args[0][2] as $arg) { $number = $this->assertNumber($arg); if (\is_null($max) || $max->lessThan($number)) { @@ -7577,31 +8558,27 @@ class Compiler { $list = $this->coerceList($args[0], ',', true); - return \count($list[2]); + return new Number(\count($list[2]), ''); } - //protected static $libListSeparator = ['list...']; + protected static $libListSeparator = ['list']; protected function libListSeparator($args) { - if (\count($args) > 1) { - return 'comma'; - } - if (! \in_array($args[0][0], [Type::T_LIST, Type::T_MAP])) { - return 'space'; + return [Type::T_KEYWORD, 'space']; } $list = $this->coerceList($args[0]); if (\count($list[2]) <= 1 && empty($list['enclosing'])) { - return 'space'; + return [Type::T_KEYWORD, 'space']; } if ($list[1] === ',') { - return 'comma'; + return [Type::T_KEYWORD, 'comma']; } - return 'space'; + return [Type::T_KEYWORD, 'space']; } protected static $libNth = ['list', 'n']; @@ -7643,7 +8620,7 @@ class Compiler protected static $libMapGet = ['map', 'key']; protected function libMapGet($args) { - $map = $this->assertMap($args[0]); + $map = $this->assertMap($args[0], 'map'); $key = $args[1]; if (! \is_null($key)) { @@ -7662,7 +8639,7 @@ class Compiler protected static $libMapKeys = ['map']; protected function libMapKeys($args) { - $map = $this->assertMap($args[0]); + $map = $this->assertMap($args[0], 'map'); $keys = $map[1]; return [Type::T_LIST, ',', $keys]; @@ -7671,21 +8648,28 @@ class Compiler protected static $libMapValues = ['map']; protected function libMapValues($args) { - $map = $this->assertMap($args[0]); + $map = $this->assertMap($args[0], 'map'); $values = $map[2]; return [Type::T_LIST, ',', $values]; } - protected static $libMapRemove = ['map', 'key...']; + protected static $libMapRemove = [ + ['map'], + ['map', 'key', 'keys...'], + ]; protected function libMapRemove($args) { - $map = $this->assertMap($args[0]); - $keyList = $this->assertList($args[1]); + $map = $this->assertMap($args[0], 'map'); + + if (\count($args) === 1) { + return $map; + } $keys = []; + $keys[] = $this->compileStringContent($this->coerceString($args[1])); - foreach ($keyList[2] as $key) { + foreach ($args[2][2] as $key) { $keys[] = $this->compileStringContent($this->coerceString($key)); } @@ -7702,8 +8686,19 @@ class Compiler protected static $libMapHasKey = ['map', 'key']; protected function libMapHasKey($args) { - $map = $this->assertMap($args[0]); - $key = $this->compileStringContent($this->coerceString($args[1])); + $map = $this->assertMap($args[0], 'map'); + + return $this->toBool($this->mapHasKey($map, $args[1])); + } + + /** + * @param array|Number $keyValue + * + * @return bool + */ + private function mapHasKey(array $map, $keyValue) + { + $key = $this->compileStringContent($this->coerceString($keyValue)); for ($i = \count($map[1]) - 1; $i >= 0; $i--) { if ($key === $this->compileStringContent($this->coerceString($map[1][$i]))) { @@ -7720,8 +8715,8 @@ class Compiler ]; protected function libMapMerge($args) { - $map1 = $this->assertMap($args[0]); - $map2 = $this->assertMap($args[1]); + $map1 = $this->assertMap($args[0], 'map1'); + $map2 = $this->assertMap($args[1], 'map2'); foreach ($map2[1] as $i2 => $key2) { $key = $this->compileStringContent($this->coerceString($key2)); @@ -7743,12 +8738,18 @@ class Compiler protected static $libKeywords = ['args']; protected function libKeywords($args) { - $this->assertList($args[0]); + $value = $args[0]; + + if ($value[0] !== Type::T_LIST || !isset($value[3]) || !\is_array($value[3])) { + $compiledValue = $this->compileValue($value); + + throw SassScriptException::forArgument($compiledValue . ' is not an argument list.', 'args'); + } $keys = []; $values = []; - foreach ($args[0][2] as $name => $arg) { + foreach ($this->getArgumentListKeywords($value) as $name => $arg) { $keys[] = [Type::T_KEYWORD, $name]; $values[] = $arg; } @@ -7763,10 +8764,10 @@ class Compiler $this->coerceList($list, ' '); if (! empty($list['enclosing']) && $list['enclosing'] === 'bracket') { - return true; + return self::$true; } - return false; + return self::$false; } /** @@ -7857,21 +8858,23 @@ class Compiler return $res; } + protected static $libZip = ['lists...']; protected function libZip($args) { - foreach ($args as $key => $arg) { - $args[$key] = $this->coerceList($arg); + $argLists = []; + foreach ($args[0][2] as $arg) { + $argLists[] = $this->coerceList($arg); } $lists = []; - $firstList = array_shift($args); + $firstList = array_shift($argLists); $result = [Type::T_LIST, ',', $lists]; if (! \is_null($firstList)) { foreach ($firstList[2] as $key => $item) { $list = [Type::T_LIST, '', [$item]]; - foreach ($args as $arg) { + foreach ($argLists as $arg) { if (isset($arg[2][$key])) { $list[2][] = $arg[2][$key]; } else { @@ -7895,6 +8898,16 @@ class Compiler { $value = $args[0]; + return [Type::T_KEYWORD, $this->getTypeOf($value)]; + } + + /** + * @param array|Number $value + * + * @return string + */ + private function getTypeOf($value) + { switch ($value[0]) { case Type::T_KEYWORD: if ($value === static::$true || $value === static::$false) { @@ -7913,7 +8926,7 @@ class Compiler return 'function'; case Type::T_LIST: - if (isset($value[3]) && $value[3]) { + if (isset($value[3]) && \is_array($value[3])) { return 'arglist'; } @@ -7926,21 +8939,17 @@ class Compiler protected static $libUnit = ['number']; protected function libUnit($args) { - $num = $args[0]; + $num = $this->assertNumber($args[0], 'number'); - if ($num instanceof Number) { - return [Type::T_STRING, '"', [$num->unitStr()]]; - } - - return ''; + return [Type::T_STRING, '"', [$num->unitStr()]]; } protected static $libUnitless = ['number']; protected function libUnitless($args) { - $value = $args[0]; + $value = $this->assertNumber($args[0], 'number'); - return $value instanceof Number && $value->unitless(); + return $this->toBool($value->unitless()); } protected static $libComparable = [ @@ -7958,7 +8967,7 @@ class Compiler throw $this->error('Invalid argument(s) for "comparable"'); } - return $number1->isComparableTo($number2); + return $this->toBool($number1->isComparableTo($number2)); } protected static $libStrIndex = ['string', 'substring']; @@ -8017,25 +9026,37 @@ class Compiler protected static $libStrSlice = ['string', 'start-at', 'end-at:-1']; protected function libStrSlice($args) { - if (isset($args[2]) && ! $args[2][1]) { - return static::$nullString; - } - - $string = $this->coerceString($args[0]); + $string = $this->assertString($args[0], 'string'); $stringContent = $this->compileStringContent($string); - $start = (int) $args[1][1]; + $start = $this->assertNumber($args[1], 'start-at'); + $start->assertNoUnits('start-at'); + $startInt = $this->assertInteger($start, 'start-at'); + $end = $this->assertNumber($args[2], 'end-at'); + $end->assertNoUnits('end-at'); + $endInt = $this->assertInteger($end, 'end-at'); - if ($start > 0) { - $start--; + if ($endInt === 0) { + return [Type::T_STRING, $string[1], []]; } - $end = isset($args[2]) ? (int) $args[2][1] : -1; - $length = $end < 0 ? $end + 1 : ($end > 0 ? $end - $start : $end); + if ($startInt > 0) { + $startInt--; + } - $string[2] = $length - ? [substr($stringContent, $start, $length)] - : [substr($stringContent, $start)]; + if ($endInt < 0) { + $endInt = Util::mbStrlen($stringContent) + $endInt; + } else { + $endInt--; + } + + if ($endInt < $startInt) { + return [Type::T_STRING, $string[1], []]; + } + + $length = $endInt - $startInt + 1; // The end of the slice is inclusive + + $string[2] = [Util::mbSubstr($stringContent, $startInt, $length)]; return $string; } @@ -8043,7 +9064,7 @@ class Compiler protected static $libToLowerCase = ['string']; protected function libToLowerCase($args) { - $string = $this->coerceString($args[0]); + $string = $this->assertString($args[0], 'string'); $stringContent = $this->compileStringContent($string); $string[2] = [$this->stringTransformAsciiOnly($stringContent, 'strtolower')]; @@ -8054,7 +9075,7 @@ class Compiler protected static $libToUpperCase = ['string']; protected function libToUpperCase($args) { - $string = $this->coerceString($args[0]); + $string = $this->assertString($args[0], 'string'); $stringContent = $this->compileStringContent($string); $string[2] = [$this->stringTransformAsciiOnly($stringContent, 'strtoupper')]; @@ -8092,7 +9113,7 @@ class Compiler protected static $libFeatureExists = ['feature']; protected function libFeatureExists($args) { - $string = $this->coerceString($args[0]); + $string = $this->assertString($args[0], 'feature'); $name = $this->compileStringContent($string); return $this->toBool( @@ -8103,18 +9124,18 @@ class Compiler protected static $libFunctionExists = ['name']; protected function libFunctionExists($args) { - $string = $this->coerceString($args[0]); + $string = $this->assertString($args[0], 'name'); $name = $this->compileStringContent($string); // user defined functions if ($this->has(static::$namespaces['function'] . $name)) { - return true; + return self::$true; } $name = $this->normalizeName($name); if (isset($this->userFunctions[$name])) { - return true; + return self::$true; } // built-in functions @@ -8126,30 +9147,31 @@ class Compiler protected static $libGlobalVariableExists = ['name']; protected function libGlobalVariableExists($args) { - $string = $this->coerceString($args[0]); + $string = $this->assertString($args[0], 'name'); $name = $this->compileStringContent($string); - return $this->has($name, $this->rootEnv); + return $this->toBool($this->has($name, $this->rootEnv)); } protected static $libMixinExists = ['name']; protected function libMixinExists($args) { - $string = $this->coerceString($args[0]); + $string = $this->assertString($args[0], 'name'); $name = $this->compileStringContent($string); - return $this->has(static::$namespaces['mixin'] . $name); + return $this->toBool($this->has(static::$namespaces['mixin'] . $name)); } protected static $libVariableExists = ['name']; protected function libVariableExists($args) { - $string = $this->coerceString($args[0]); + $string = $this->assertString($args[0], 'name'); $name = $this->compileStringContent($string); - return $this->has($name); + return $this->toBool($this->has($name)); } + protected static $libCounter = ['args...']; /** * Workaround IE7's content counter bug. * @@ -8159,7 +9181,7 @@ class Compiler */ protected function libCounter($args) { - $list = array_map([$this, 'compileValue'], $args); + $list = array_map([$this, 'compileValue'], $args[0][2]); return [Type::T_STRING, '', ['counter(' . implode(',', $list) . ')']]; } @@ -8167,24 +9189,21 @@ class Compiler protected static $libRandom = ['limit:null']; protected function libRandom($args) { - if (isset($args[0]) & $args[0] !== static::$null) { - $n = $this->assertNumber($args[0])->getDimension(); + if (isset($args[0]) && $args[0] !== static::$null) { + $n = $this->assertInteger($args[0], 'limit'); if ($n < 1) { - throw $this->error("\$limit must be greater than or equal to 1"); + throw new SassScriptException("\$limit: Must be greater than 0, was $n."); } - if (round($n - \intval($n), Number::PRECISION) > 0) { - throw $this->error("Expected \$limit to be an integer but got $n for `random`"); - } - - return new Number(mt_rand(1, \intval($n)), ''); + return new Number(mt_rand(1, $n), ''); } $max = mt_getrandmax(); return new Number(mt_rand(0, $max - 1) / $max, ''); } + protected static $libUniqueId = []; protected function libUniqueId() { static $id; @@ -8200,6 +9219,12 @@ class Compiler return [Type::T_STRING, '', ['u' . str_pad(base_convert($id, 10, 36), 8, '0', STR_PAD_LEFT)]]; } + /** + * @param array|Number $value + * @param bool $force_enclosing_display + * + * @return array + */ protected function inspectFormatValue($value, $force_enclosing_display = false) { if ($value === static::$null) { @@ -8208,6 +9233,10 @@ class Compiler $stringValue = [$value]; + if ($value instanceof Number) { + return [Type::T_STRING, '', $stringValue]; + } + if ($value[0] === Type::T_LIST) { if (end($value[2]) === static::$null) { array_pop($value[2]); @@ -8246,9 +9275,11 @@ class Compiler /** * Preprocess selector args * - * @param array $arg + * @param array $arg + * @param string|null $varname + * @param bool $allowParent * - * @return array|boolean + * @return array */ protected function getSelectorArg($arg, $varname = null, $allowParent = false) { @@ -8259,13 +9290,14 @@ class Compiler } if (! $this->checkSelectorArgType($arg)) { - $var_display = ($varname ? ' $' . $varname . ':' : ''); $var_value = $this->compileValue($arg); - throw $this->error("Error:{$var_display} $var_value is not a valid selector: it must be a string," - . " a list of strings, or a list of lists of strings"); + throw SassScriptException::forArgument("$var_value is not a valid selector: it must be a string, a list of strings, or a list of lists of strings", $varname); } - $arg = $this->libUnquote([$arg]); + + if ($arg[0] === Type::T_STRING) { + $arg[1] = ''; + } $arg = $this->compileValue($arg); $parsedSelector = []; @@ -8278,8 +9310,7 @@ class Compiler foreach ($gluedSelector as $selector) { foreach ($selector as $s) { if (in_array(static::$selfSelector, $s)) { - $var_display = ($varname ? ' $' . $varname . ':' : ''); - throw $this->error("Error:{$var_display} Parent selectors aren't allowed here."); + throw SassScriptException::forArgument("Parent selectors aren't allowed here.", $varname); } } } @@ -8288,8 +9319,7 @@ class Compiler return $gluedSelector; } - $var_display = ($varname ? ' $' . $varname . ':' : ''); - throw $this->error("Error:{$var_display} expected more input, invalid selector."); + throw SassScriptException::forArgument("expected more input, invalid selector.", $varname); } /** @@ -8319,11 +9349,11 @@ class Compiler * * @param array $selectors * - * @return string + * @return array */ protected function formatOutputSelector($selectors) { - $selectors = $this->collapseSelectors($selectors, true); + $selectors = $this->collapseSelectorsAsList($selectors); return $selectors; } @@ -8336,7 +9366,7 @@ class Compiler $super = $this->getSelectorArg($super, 'super'); $sub = $this->getSelectorArg($sub, 'sub'); - return $this->isSuperSelector($super, $sub); + return $this->toBool($this->isSuperSelector($super, $sub)); } /** @@ -8579,6 +9609,10 @@ class Compiler $this->extendsMap = []; foreach ($extendee as $es) { + if (\count($es) !== 1) { + throw $this->error('Can\'t extend complex selector.'); + } + // only use the first one $this->pushExtends(reset($es), $extender, null); } @@ -8679,7 +9713,7 @@ class Compiler * @param array $compound1 * @param array $compound2 * - * @return array|mixed + * @return array */ protected function unifyCompoundSelectors($compound1, $compound2) { @@ -8913,9 +9947,9 @@ class Compiler /** * Find the html tag name in a selector parts list * - * @param array $parts + * @param string[] $parts * - * @return mixed|string + * @return string */ protected function findTagName($parts) { @@ -8952,7 +9986,11 @@ class Compiler protected static $libScssphpGlob = ['pattern']; protected function libScssphpGlob($args) { - $string = $this->coerceString($args[0]); + @trigger_error(sprintf('The "scssphp-glob" function is deprecated an will be removed in ScssPhp 2.0. Register your own alternative through "%s::registerFunction', __CLASS__), E_USER_DEPRECATED); + + $this->logger->warn('The "scssphp-glob" function is deprecated an will be removed in ScssPhp 2.0.', true); + + $string = $this->assertString($args[0], 'pattern'); $pattern = $this->compileStringContent($string); $matches = glob($pattern); $listParts = []; diff --git a/vendor/scssphp/scssphp/src/Compiler/CachedResult.php b/vendor/scssphp/scssphp/src/Compiler/CachedResult.php new file mode 100644 index 00000000..a6629199 --- /dev/null +++ b/vendor/scssphp/scssphp/src/Compiler/CachedResult.php @@ -0,0 +1,77 @@ + + */ + private $parsedFiles; + + /** + * @var array + * @phpstan-var list + */ + private $resolvedImports; + + /** + * @param CompilationResult $result + * @param array $parsedFiles + * @param array $resolvedImports + * + * @phpstan-param list $resolvedImports + */ + public function __construct(CompilationResult $result, array $parsedFiles, array $resolvedImports) + { + $this->result = $result; + $this->parsedFiles = $parsedFiles; + $this->resolvedImports = $resolvedImports; + } + + /** + * @return CompilationResult + */ + public function getResult() + { + return $this->result; + } + + /** + * @return array + */ + public function getParsedFiles() + { + return $this->parsedFiles; + } + + /** + * @return array + * + * @phpstan-return list + */ + public function getResolvedImports() + { + return $this->resolvedImports; + } +} diff --git a/vendor/scssphp/scssphp/src/Compiler/Environment.php b/vendor/scssphp/scssphp/src/Compiler/Environment.php index dc2f86c1..306b15a6 100644 --- a/vendor/scssphp/scssphp/src/Compiler/Environment.php +++ b/vendor/scssphp/scssphp/src/Compiler/Environment.php @@ -16,16 +16,18 @@ namespace ScssPhp\ScssPhp\Compiler; * Compiler environment * * @author Anthon Pang + * + * @internal */ class Environment { /** - * @var \ScssPhp\ScssPhp\Block + * @var \ScssPhp\ScssPhp\Block|null */ public $block; /** - * @var \ScssPhp\ScssPhp\Compiler\Environment + * @var \ScssPhp\ScssPhp\Compiler\Environment|null */ public $parent; diff --git a/vendor/scssphp/scssphp/src/Exception/CompilerException.php b/vendor/scssphp/scssphp/src/Exception/CompilerException.php index 343da4c7..0b00cf52 100644 --- a/vendor/scssphp/scssphp/src/Exception/CompilerException.php +++ b/vendor/scssphp/scssphp/src/Exception/CompilerException.php @@ -16,6 +16,8 @@ namespace ScssPhp\ScssPhp\Exception; * Compiler exception * * @author Oleksandr Savchenko + * + * @internal */ class CompilerException extends \Exception implements SassException { diff --git a/vendor/scssphp/scssphp/src/Exception/ParserException.php b/vendor/scssphp/scssphp/src/Exception/ParserException.php index 5237f307..00d77ec9 100644 --- a/vendor/scssphp/scssphp/src/Exception/ParserException.php +++ b/vendor/scssphp/scssphp/src/Exception/ParserException.php @@ -16,6 +16,8 @@ namespace ScssPhp\ScssPhp\Exception; * Parser Exception * * @author Oleksandr Savchenko + * + * @internal */ class ParserException extends \Exception implements SassException { diff --git a/vendor/scssphp/scssphp/src/Exception/RangeException.php b/vendor/scssphp/scssphp/src/Exception/RangeException.php index b18c32d6..4be4dee7 100644 --- a/vendor/scssphp/scssphp/src/Exception/RangeException.php +++ b/vendor/scssphp/scssphp/src/Exception/RangeException.php @@ -16,6 +16,8 @@ namespace ScssPhp\ScssPhp\Exception; * Range exception * * @author Anthon Pang + * + * @internal */ class RangeException extends \Exception implements SassException { diff --git a/vendor/scssphp/scssphp/src/Exception/ServerException.php b/vendor/scssphp/scssphp/src/Exception/ServerException.php index ad5b3799..e593c401 100644 --- a/vendor/scssphp/scssphp/src/Exception/ServerException.php +++ b/vendor/scssphp/scssphp/src/Exception/ServerException.php @@ -12,10 +12,14 @@ namespace ScssPhp\ScssPhp\Exception; +@trigger_error(sprintf('The "%s" class is deprecated.', ServerException::class), E_USER_DEPRECATED); + /** * Server Exception * * @author Anthon Pang + * + * @deprecated The Scssphp server should define its own exception instead. */ class ServerException extends \Exception implements SassException { diff --git a/vendor/scssphp/scssphp/src/Formatter.php b/vendor/scssphp/scssphp/src/Formatter.php index d52a6744..cc42ae80 100644 --- a/vendor/scssphp/scssphp/src/Formatter.php +++ b/vendor/scssphp/scssphp/src/Formatter.php @@ -19,6 +19,8 @@ use ScssPhp\ScssPhp\SourceMap\SourceMapGenerator; * Base formatter * * @author Leaf Corcoran + * + * @internal */ abstract class Formatter { @@ -78,7 +80,7 @@ abstract class Formatter protected $currentColumn; /** - * @var \ScssPhp\ScssPhp\SourceMap\SourceMapGenerator + * @var \ScssPhp\ScssPhp\SourceMap\SourceMapGenerator|null */ protected $sourceMapGenerator; @@ -139,6 +141,8 @@ abstract class Formatter * Output lines inside a block * * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $block + * + * @return void */ protected function blockLines(OutputBlock $block) { @@ -156,9 +160,13 @@ abstract class Formatter * Output block selectors * * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $block + * + * @return void */ protected function blockSelectors(OutputBlock $block) { + assert(! empty($block->selectors)); + $inner = $this->indentStr(); $this->write($inner @@ -170,6 +178,8 @@ abstract class Formatter * Output block children * * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $block + * + * @return void */ protected function blockChildren(OutputBlock $block) { @@ -182,6 +192,8 @@ abstract class Formatter * Output non-empty block * * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $block + * + * @return void */ protected function block(OutputBlock $block) { @@ -285,6 +297,8 @@ abstract class Formatter * Output content * * @param string $str + * + * @return void */ protected function write($str) { diff --git a/vendor/scssphp/scssphp/src/Formatter/Compact.php b/vendor/scssphp/scssphp/src/Formatter/Compact.php index 249920ef..22f22688 100644 --- a/vendor/scssphp/scssphp/src/Formatter/Compact.php +++ b/vendor/scssphp/scssphp/src/Formatter/Compact.php @@ -20,6 +20,8 @@ use ScssPhp\ScssPhp\Formatter; * @author Leaf Corcoran * * @deprecated since 1.4.0. Use the Compressed formatter instead. + * + * @internal */ class Compact extends Formatter { diff --git a/vendor/scssphp/scssphp/src/Formatter/Compressed.php b/vendor/scssphp/scssphp/src/Formatter/Compressed.php index d666a665..de13c188 100644 --- a/vendor/scssphp/scssphp/src/Formatter/Compressed.php +++ b/vendor/scssphp/scssphp/src/Formatter/Compressed.php @@ -18,6 +18,8 @@ use ScssPhp\ScssPhp\Formatter; * Compressed formatter * * @author Leaf Corcoran + * + * @internal */ class Compressed extends Formatter { @@ -67,6 +69,8 @@ class Compressed extends Formatter */ protected function blockSelectors(OutputBlock $block) { + assert(! empty($block->selectors)); + $inner = $this->indentStr(); $this->write( diff --git a/vendor/scssphp/scssphp/src/Formatter/Crunched.php b/vendor/scssphp/scssphp/src/Formatter/Crunched.php index 91c31443..2bc1e929 100644 --- a/vendor/scssphp/scssphp/src/Formatter/Crunched.php +++ b/vendor/scssphp/scssphp/src/Formatter/Crunched.php @@ -20,6 +20,8 @@ use ScssPhp\ScssPhp\Formatter; * @author Anthon Pang * * @deprecated since 1.4.0. Use the Compressed formatter instead. + * + * @internal */ class Crunched extends Formatter { @@ -69,6 +71,8 @@ class Crunched extends Formatter */ protected function blockSelectors(OutputBlock $block) { + assert(! empty($block->selectors)); + $inner = $this->indentStr(); $this->write( diff --git a/vendor/scssphp/scssphp/src/Formatter/Debug.php b/vendor/scssphp/scssphp/src/Formatter/Debug.php index c676601b..b3f44225 100644 --- a/vendor/scssphp/scssphp/src/Formatter/Debug.php +++ b/vendor/scssphp/scssphp/src/Formatter/Debug.php @@ -20,6 +20,8 @@ use ScssPhp\ScssPhp\Formatter; * @author Anthon Pang * * @deprecated since 1.4.0. + * + * @internal */ class Debug extends Formatter { diff --git a/vendor/scssphp/scssphp/src/Formatter/Expanded.php b/vendor/scssphp/scssphp/src/Formatter/Expanded.php index b7cbde18..a280416d 100644 --- a/vendor/scssphp/scssphp/src/Formatter/Expanded.php +++ b/vendor/scssphp/scssphp/src/Formatter/Expanded.php @@ -18,6 +18,8 @@ use ScssPhp\ScssPhp\Formatter; * Expanded formatter * * @author Leaf Corcoran + * + * @internal */ class Expanded extends Formatter { diff --git a/vendor/scssphp/scssphp/src/Formatter/Nested.php b/vendor/scssphp/scssphp/src/Formatter/Nested.php index 3249c182..9e729568 100644 --- a/vendor/scssphp/scssphp/src/Formatter/Nested.php +++ b/vendor/scssphp/scssphp/src/Formatter/Nested.php @@ -21,6 +21,8 @@ use ScssPhp\ScssPhp\Type; * @author Leaf Corcoran * * @deprecated since 1.4.0. Use the Expanded formatter instead. + * + * @internal */ class Nested extends Formatter { diff --git a/vendor/scssphp/scssphp/src/Formatter/OutputBlock.php b/vendor/scssphp/scssphp/src/Formatter/OutputBlock.php index fe0321bd..88deb2d3 100644 --- a/vendor/scssphp/scssphp/src/Formatter/OutputBlock.php +++ b/vendor/scssphp/scssphp/src/Formatter/OutputBlock.php @@ -16,6 +16,8 @@ namespace ScssPhp\ScssPhp\Formatter; * Output block * * @author Anthon Pang + * + * @internal */ class OutputBlock { @@ -30,17 +32,17 @@ class OutputBlock public $depth; /** - * @var array + * @var array|null */ public $selectors; /** - * @var array + * @var string[] */ public $lines; /** - * @var array + * @var OutputBlock[] */ public $children; @@ -50,17 +52,17 @@ class OutputBlock public $parent; /** - * @var string + * @var string|null */ public $sourceName; /** - * @var integer + * @var integer|null */ public $sourceLine; /** - * @var integer + * @var integer|null */ public $sourceColumn; } diff --git a/vendor/scssphp/scssphp/src/Logger/LoggerInterface.php b/vendor/scssphp/scssphp/src/Logger/LoggerInterface.php new file mode 100644 index 00000000..7c0a2f76 --- /dev/null +++ b/vendor/scssphp/scssphp/src/Logger/LoggerInterface.php @@ -0,0 +1,48 @@ +stream = $stream; + $this->closeOnDestruct = $closeOnDestruct; + } + + /** + * @internal + */ + public function __destruct() + { + if ($this->closeOnDestruct) { + fclose($this->stream); + } + } + + /** + * @inheritDoc + */ + public function warn($message, $deprecation = false) + { + $prefix = ($deprecation ? 'DEPRECATION ' : '') . 'WARNING: '; + + fwrite($this->stream, $prefix . $message . "\n\n"); + } + + /** + * @inheritDoc + */ + public function debug($message) + { + fwrite($this->stream, $message . "\n"); + } +} diff --git a/vendor/scssphp/scssphp/src/Node.php b/vendor/scssphp/scssphp/src/Node.php index 60d357e0..53019375 100644 --- a/vendor/scssphp/scssphp/src/Node.php +++ b/vendor/scssphp/scssphp/src/Node.php @@ -16,6 +16,8 @@ namespace ScssPhp\ScssPhp; * Base node * * @author Anthon Pang + * + * @internal */ abstract class Node { @@ -30,12 +32,12 @@ abstract class Node public $sourceIndex; /** - * @var integer + * @var int|null */ public $sourceLine; /** - * @var integer + * @var int|null */ public $sourceColumn; } diff --git a/vendor/scssphp/scssphp/src/Node/Number.php b/vendor/scssphp/scssphp/src/Node/Number.php index 166de50d..78f86bac 100644 --- a/vendor/scssphp/scssphp/src/Node/Number.php +++ b/vendor/scssphp/scssphp/src/Node/Number.php @@ -12,10 +12,13 @@ namespace ScssPhp\ScssPhp\Node; +use ScssPhp\ScssPhp\Base\Range; use ScssPhp\ScssPhp\Compiler; +use ScssPhp\ScssPhp\Exception\RangeException; use ScssPhp\ScssPhp\Exception\SassScriptException; use ScssPhp\ScssPhp\Node; use ScssPhp\ScssPhp\Type; +use ScssPhp\ScssPhp\Util; /** * Dimension + optional units @@ -27,6 +30,8 @@ use ScssPhp\ScssPhp\Type; * }} * * @author Anthon Pang + * + * @template-implements \ArrayAccess */ class Number extends Node implements \ArrayAccess { @@ -42,6 +47,7 @@ class Number extends Node implements \ArrayAccess * @see http://www.w3.org/TR/2012/WD-css3-values-20120308/ * * @var array + * @phpstan-var array> */ protected static $unitTable = [ 'in' => [ @@ -243,6 +249,23 @@ class Number extends Node implements \ArrayAccess return self::getUnitString($this->numeratorUnits, $this->denominatorUnits); } + /** + * @param float|int $min + * @param float|int $max + * @param string|null $name + * + * @return float|int + * @throws SassScriptException + */ + public function valueInRange($min, $max, $name = null) + { + try { + return Util::checkRange('', new Range($min, $max), $this); + } catch (RangeException $e) { + throw SassScriptException::forArgument(sprintf('Expected %s to be within %s%s and %s%3$s', $this, $min, $this->unitStr(), $max), $name); + } + } + /** * @param string|null $varName * @@ -254,7 +277,22 @@ class Number extends Node implements \ArrayAccess return; } - throw SassScriptException::forArgument(sprintf('Expected %s to have no units', $this), $varName); + throw SassScriptException::forArgument(sprintf('Expected %s to have no units.', $this), $varName); + } + + /** + * @param string $unit + * @param string|null $varName + * + * @return void + */ + public function assertUnit($unit, $varName = null) + { + if ($this->hasUnit($unit)) { + return; + } + + throw SassScriptException::forArgument(sprintf('Expected %s to have unit "%s".', $this, $unit), $varName); } /** @@ -279,6 +317,29 @@ class Number extends Node implements \ArrayAccess )); } + /** + * Returns a copy of this number, converted to the units represented by $newNumeratorUnits and $newDenominatorUnits. + * + * This does not throw an error if this number is unitless and + * $newNumeratorUnits/$newDenominatorUnits are not empty, or vice versa. Instead, + * it treats all unitless numbers as convertible to and from all units without + * changing the value. + * + * @param string[] $newNumeratorUnits + * @param string[] $newDenominatorUnits + * + * @return Number + * + * @phpstan-param list $newNumeratorUnits + * @phpstan-param list $newDenominatorUnits + * + * @throws SassScriptException if this number's units are not compatible with $newNumeratorUnits and $newDenominatorUnits + */ + public function coerce(array $newNumeratorUnits, array $newDenominatorUnits) + { + return new Number($this->valueInUnits($newNumeratorUnits, $newDenominatorUnits), $newNumeratorUnits, $newDenominatorUnits); + } + /** * @param Number $other * @@ -560,6 +621,8 @@ class Number extends Node implements \ArrayAccess * * @phpstan-param list $numeratorUnits * @phpstan-param list $denominatorUnits + * + * @throws SassScriptException if this number's units are not compatible with $numeratorUnits and $denominatorUnits */ private function valueInUnits(array $numeratorUnits, array $denominatorUnits) { diff --git a/vendor/scssphp/scssphp/src/Parser.php b/vendor/scssphp/scssphp/src/Parser.php index 1faa82f8..bb5d6b30 100644 --- a/vendor/scssphp/scssphp/src/Parser.php +++ b/vendor/scssphp/scssphp/src/Parser.php @@ -13,11 +13,15 @@ namespace ScssPhp\ScssPhp; use ScssPhp\ScssPhp\Exception\ParserException; +use ScssPhp\ScssPhp\Logger\LoggerInterface; +use ScssPhp\ScssPhp\Logger\QuietLogger; /** * Parser * * @author Leaf Corcoran + * + * @internal */ class Parser { @@ -80,7 +84,7 @@ class Parser */ private $count; /** - * @var Block + * @var Block|null */ private $env; /** @@ -110,18 +114,24 @@ class Parser private $cssOnly; + /** + * @var LoggerInterface + */ + private $logger; + /** * Constructor * * @api * - * @param string $sourceName - * @param integer $sourceIndex - * @param string|null $encoding - * @param Cache|null $cache - * @param bool $cssOnly + * @param string|null $sourceName + * @param integer $sourceIndex + * @param string|null $encoding + * @param Cache|null $cache + * @param bool $cssOnly + * @param LoggerInterface|null $logger */ - public function __construct($sourceName, $sourceIndex = 0, $encoding = 'utf-8', Cache $cache = null, $cssOnly = false) + public function __construct($sourceName, $sourceIndex = 0, $encoding = 'utf-8', Cache $cache = null, $cssOnly = false, LoggerInterface $logger = null) { $this->sourceName = $sourceName ?: '(stdin)'; $this->sourceIndex = $sourceIndex; @@ -132,6 +142,7 @@ class Parser $this->commentsSeen = []; $this->allowVars = true; $this->cssOnly = $cssOnly; + $this->logger = $logger ?: new QuietLogger(); if (empty(static::$operatorPattern)) { static::$operatorPattern = '([*\/%+-]|[!=]\=|\>\=?|\<\=?|and|or)'; @@ -168,6 +179,8 @@ class Parser * * @param string $msg * + * @phpstan-return never-return + * * @throws ParserException * * @deprecated use "parseError" and throw the exception in the caller instead. @@ -514,6 +527,10 @@ class Parser ) { ! $this->cssOnly || $this->assertPlainCssValid(false, $s); + list($line, $column) = $this->getSourcePosition($s); + $file = $this->sourceName; + $this->logger->warn("The \"@scssphp-import-once\" directive is deprecated and will be removed in ScssPhp 2.0, in \"$file\", line $line, column $column.", true); + $this->append([Type::T_SCSSPHP_IMPORT_ONCE, $importPath], $s); return true; @@ -973,11 +990,6 @@ class Parser $this->seek($s); - // misc - if ($this->literal('-->', 3)) { - return true; - } - // opening css block if ( $this->selectors($selectors) && @@ -1065,10 +1077,7 @@ class Parser } // extra stuff - if ( - $this->matchChar(';') || - $this->literal('