diff --git a/composer.json b/composer.json index b13d87295..474e6000f 100644 --- a/composer.json +++ b/composer.json @@ -38,7 +38,7 @@ "psr/container": "^1.0", "punic/punic": "^1.6", "sabre/dav": "^4.1.3", - "scssphp/scssphp": "1.0.3", + "scssphp/scssphp": "^1.4.0", "stecman/symfony-console-completion": "^0.11.0", "swiftmailer/swiftmailer": "^6.0", "symfony/console": "4.4.18", diff --git a/composer.lock b/composer.lock index 59ea4db95..5bfc8f316 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "54aa8c3315eec1c1cf4b20ac730f5a82", + "content-hash": "ac3490f1a9096ec27f6057eebc73e851", "packages": [ { "name": "aws/aws-sdk-php", @@ -3798,24 +3798,28 @@ }, { "name": "scssphp/scssphp", - "version": "1.0.3", + "version": "v1.4.1", "source": { "type": "git", "url": "https://github.com/scssphp/scssphp.git", - "reference": "616c518333c656eaa23182ac6cfc01453f1e7c78" + "reference": "ba86c963b94ec7ebd6e19d90cdab90d89667dbf7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/scssphp/scssphp/zipball/616c518333c656eaa23182ac6cfc01453f1e7c78", - "reference": "616c518333c656eaa23182ac6cfc01453f1e7c78", + "url": "https://api.github.com/repos/scssphp/scssphp/zipball/ba86c963b94ec7ebd6e19d90cdab90d89667dbf7", + "reference": "ba86c963b94ec7ebd6e19d90cdab90d89667dbf7", "shasum": "" }, "require": { - "php": "^5.6.0 || ^7" + "ext-ctype": "*", + "ext-json": "*", + "php": ">=5.6.0" }, "require-dev": { - "phpunit/phpunit": "~4.6", - "squizlabs/php_codesniffer": "~2.5", + "phpunit/phpunit": "^5.7 || ^6.5 || ^7.5 || ^8.3 || ^9.4", + "sass/sass-spec": "2020.12.29", + "squizlabs/php_codesniffer": "~3.5", + "symfony/phpunit-bridge": "^5.1", "twbs/bootstrap": "~4.3", "zurb/foundation": "~6.5" }, @@ -3853,7 +3857,11 @@ "scss", "stylesheet" ], - "time": "2019-08-07T20:16:04+00:00" + "support": { + "issues": "https://github.com/scssphp/scssphp/issues", + "source": "https://github.com/scssphp/scssphp/tree/v1.4.1" + }, + "time": "2021-01-04T13:23:23+00:00" }, { "name": "spomky-labs/base64url", diff --git a/composer/InstalledVersions.php b/composer/InstalledVersions.php index 4f023de23..39f24b91a 100644 --- a/composer/InstalledVersions.php +++ b/composer/InstalledVersions.php @@ -24,12 +24,12 @@ class InstalledVersions private static $installed = array ( 'root' => array ( - 'pretty_version' => 'dev-master', - 'version' => 'dev-master', + 'pretty_version' => 'v21.0.0beta5', + 'version' => '21.0.0.0-beta5', 'aliases' => array ( ), - 'reference' => '65a8a4553327c818931876ac1aa80a2143e02733', + 'reference' => '09596e43fba86a3643879595a8fb6fece4af6a78', 'name' => 'nextcloud/3rdparty', ), 'versions' => @@ -297,12 +297,12 @@ class InstalledVersions ), 'nextcloud/3rdparty' => array ( - 'pretty_version' => 'dev-master', - 'version' => 'dev-master', + 'pretty_version' => 'v21.0.0beta5', + 'version' => '21.0.0.0-beta5', 'aliases' => array ( ), - 'reference' => '65a8a4553327c818931876ac1aa80a2143e02733', + 'reference' => '09596e43fba86a3643879595a8fb6fece4af6a78', ), 'nextcloud/lognormalizer' => array ( @@ -653,12 +653,12 @@ class InstalledVersions ), 'scssphp/scssphp' => array ( - 'pretty_version' => '1.0.3', - 'version' => '1.0.3.0', + 'pretty_version' => 'v1.4.1', + 'version' => '1.4.1.0', 'aliases' => array ( ), - 'reference' => '616c518333c656eaa23182ac6cfc01453f1e7c78', + 'reference' => 'ba86c963b94ec7ebd6e19d90cdab90d89667dbf7', ), 'spomky-labs/base64url' => array ( diff --git a/composer/autoload_classmap.php b/composer/autoload_classmap.php index b85b80888..6b8d843bb 100644 --- a/composer/autoload_classmap.php +++ b/composer/autoload_classmap.php @@ -2411,6 +2411,8 @@ 'ScssPhp\\ScssPhp\\Exception\\CompilerException' => $vendorDir . '/scssphp/scssphp/src/Exception/CompilerException.php', 'ScssPhp\\ScssPhp\\Exception\\ParserException' => $vendorDir . '/scssphp/scssphp/src/Exception/ParserException.php', 'ScssPhp\\ScssPhp\\Exception\\RangeException' => $vendorDir . '/scssphp/scssphp/src/Exception/RangeException.php', + 'ScssPhp\\ScssPhp\\Exception\\SassException' => $vendorDir . '/scssphp/scssphp/src/Exception/SassException.php', + 'ScssPhp\\ScssPhp\\Exception\\SassScriptException' => $vendorDir . '/scssphp/scssphp/src/Exception/SassScriptException.php', 'ScssPhp\\ScssPhp\\Exception\\ServerException' => $vendorDir . '/scssphp/scssphp/src/Exception/ServerException.php', 'ScssPhp\\ScssPhp\\Formatter' => $vendorDir . '/scssphp/scssphp/src/Formatter.php', 'ScssPhp\\ScssPhp\\Formatter\\Compact' => $vendorDir . '/scssphp/scssphp/src/Formatter/Compact.php', @@ -2422,13 +2424,14 @@ 'ScssPhp\\ScssPhp\\Formatter\\OutputBlock' => $vendorDir . '/scssphp/scssphp/src/Formatter/OutputBlock.php', 'ScssPhp\\ScssPhp\\Node' => $vendorDir . '/scssphp/scssphp/src/Node.php', 'ScssPhp\\ScssPhp\\Node\\Number' => $vendorDir . '/scssphp/scssphp/src/Node/Number.php', + 'ScssPhp\\ScssPhp\\OutputStyle' => $vendorDir . '/scssphp/scssphp/src/OutputStyle.php', 'ScssPhp\\ScssPhp\\Parser' => $vendorDir . '/scssphp/scssphp/src/Parser.php', 'ScssPhp\\ScssPhp\\SourceMap\\Base64' => $vendorDir . '/scssphp/scssphp/src/SourceMap/Base64.php', 'ScssPhp\\ScssPhp\\SourceMap\\Base64VLQ' => $vendorDir . '/scssphp/scssphp/src/SourceMap/Base64VLQ.php', - 'ScssPhp\\ScssPhp\\SourceMap\\Base64VLQEncoder' => $vendorDir . '/scssphp/scssphp/src/SourceMap/Base64VLQEncoder.php', 'ScssPhp\\ScssPhp\\SourceMap\\SourceMapGenerator' => $vendorDir . '/scssphp/scssphp/src/SourceMap/SourceMapGenerator.php', 'ScssPhp\\ScssPhp\\Type' => $vendorDir . '/scssphp/scssphp/src/Type.php', 'ScssPhp\\ScssPhp\\Util' => $vendorDir . '/scssphp/scssphp/src/Util.php', + 'ScssPhp\\ScssPhp\\Util\\Path' => $vendorDir . '/scssphp/scssphp/src/Util/Path.php', 'ScssPhp\\ScssPhp\\Version' => $vendorDir . '/scssphp/scssphp/src/Version.php', 'SearchDAV\\Backend\\ISearchBackend' => $vendorDir . '/icewind/searchdav/src/Backend/ISearchBackend.php', 'SearchDAV\\Backend\\SearchPropertyDefinition' => $vendorDir . '/icewind/searchdav/src/Backend/SearchPropertyDefinition.php', diff --git a/composer/autoload_static.php b/composer/autoload_static.php index 62e993ed4..3876ed6a8 100644 --- a/composer/autoload_static.php +++ b/composer/autoload_static.php @@ -2928,6 +2928,8 @@ class ComposerStaticInit2f23f73bc0cc116b4b1eee1521aa8652 'ScssPhp\\ScssPhp\\Exception\\CompilerException' => __DIR__ . '/..' . '/scssphp/scssphp/src/Exception/CompilerException.php', 'ScssPhp\\ScssPhp\\Exception\\ParserException' => __DIR__ . '/..' . '/scssphp/scssphp/src/Exception/ParserException.php', 'ScssPhp\\ScssPhp\\Exception\\RangeException' => __DIR__ . '/..' . '/scssphp/scssphp/src/Exception/RangeException.php', + 'ScssPhp\\ScssPhp\\Exception\\SassException' => __DIR__ . '/..' . '/scssphp/scssphp/src/Exception/SassException.php', + 'ScssPhp\\ScssPhp\\Exception\\SassScriptException' => __DIR__ . '/..' . '/scssphp/scssphp/src/Exception/SassScriptException.php', 'ScssPhp\\ScssPhp\\Exception\\ServerException' => __DIR__ . '/..' . '/scssphp/scssphp/src/Exception/ServerException.php', 'ScssPhp\\ScssPhp\\Formatter' => __DIR__ . '/..' . '/scssphp/scssphp/src/Formatter.php', 'ScssPhp\\ScssPhp\\Formatter\\Compact' => __DIR__ . '/..' . '/scssphp/scssphp/src/Formatter/Compact.php', @@ -2939,13 +2941,14 @@ class ComposerStaticInit2f23f73bc0cc116b4b1eee1521aa8652 'ScssPhp\\ScssPhp\\Formatter\\OutputBlock' => __DIR__ . '/..' . '/scssphp/scssphp/src/Formatter/OutputBlock.php', 'ScssPhp\\ScssPhp\\Node' => __DIR__ . '/..' . '/scssphp/scssphp/src/Node.php', 'ScssPhp\\ScssPhp\\Node\\Number' => __DIR__ . '/..' . '/scssphp/scssphp/src/Node/Number.php', + 'ScssPhp\\ScssPhp\\OutputStyle' => __DIR__ . '/..' . '/scssphp/scssphp/src/OutputStyle.php', 'ScssPhp\\ScssPhp\\Parser' => __DIR__ . '/..' . '/scssphp/scssphp/src/Parser.php', 'ScssPhp\\ScssPhp\\SourceMap\\Base64' => __DIR__ . '/..' . '/scssphp/scssphp/src/SourceMap/Base64.php', 'ScssPhp\\ScssPhp\\SourceMap\\Base64VLQ' => __DIR__ . '/..' . '/scssphp/scssphp/src/SourceMap/Base64VLQ.php', - 'ScssPhp\\ScssPhp\\SourceMap\\Base64VLQEncoder' => __DIR__ . '/..' . '/scssphp/scssphp/src/SourceMap/Base64VLQEncoder.php', 'ScssPhp\\ScssPhp\\SourceMap\\SourceMapGenerator' => __DIR__ . '/..' . '/scssphp/scssphp/src/SourceMap/SourceMapGenerator.php', 'ScssPhp\\ScssPhp\\Type' => __DIR__ . '/..' . '/scssphp/scssphp/src/Type.php', 'ScssPhp\\ScssPhp\\Util' => __DIR__ . '/..' . '/scssphp/scssphp/src/Util.php', + 'ScssPhp\\ScssPhp\\Util\\Path' => __DIR__ . '/..' . '/scssphp/scssphp/src/Util/Path.php', 'ScssPhp\\ScssPhp\\Version' => __DIR__ . '/..' . '/scssphp/scssphp/src/Version.php', 'SearchDAV\\Backend\\ISearchBackend' => __DIR__ . '/..' . '/icewind/searchdav/src/Backend/ISearchBackend.php', 'SearchDAV\\Backend\\SearchPropertyDefinition' => __DIR__ . '/..' . '/icewind/searchdav/src/Backend/SearchPropertyDefinition.php', diff --git a/composer/installed.json b/composer/installed.json index 7806fbb62..43946be97 100644 --- a/composer/installed.json +++ b/composer/installed.json @@ -3938,29 +3938,33 @@ }, { "name": "scssphp/scssphp", - "version": "1.0.3", - "version_normalized": "1.0.3.0", + "version": "v1.4.1", + "version_normalized": "1.4.1.0", "source": { "type": "git", "url": "https://github.com/scssphp/scssphp.git", - "reference": "616c518333c656eaa23182ac6cfc01453f1e7c78" + "reference": "ba86c963b94ec7ebd6e19d90cdab90d89667dbf7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/scssphp/scssphp/zipball/616c518333c656eaa23182ac6cfc01453f1e7c78", - "reference": "616c518333c656eaa23182ac6cfc01453f1e7c78", + "url": "https://api.github.com/repos/scssphp/scssphp/zipball/ba86c963b94ec7ebd6e19d90cdab90d89667dbf7", + "reference": "ba86c963b94ec7ebd6e19d90cdab90d89667dbf7", "shasum": "" }, "require": { - "php": "^5.6.0 || ^7" + "ext-ctype": "*", + "ext-json": "*", + "php": ">=5.6.0" }, "require-dev": { - "phpunit/phpunit": "~4.6", - "squizlabs/php_codesniffer": "~2.5", + "phpunit/phpunit": "^5.7 || ^6.5 || ^7.5 || ^8.3 || ^9.4", + "sass/sass-spec": "2020.12.29", + "squizlabs/php_codesniffer": "~3.5", + "symfony/phpunit-bridge": "^5.1", "twbs/bootstrap": "~4.3", "zurb/foundation": "~6.5" }, - "time": "2019-08-07T20:16:04+00:00", + "time": "2021-01-04T13:23:23+00:00", "bin": [ "bin/pscss" ], @@ -3996,6 +4000,10 @@ "scss", "stylesheet" ], + "support": { + "issues": "https://github.com/scssphp/scssphp/issues", + "source": "https://github.com/scssphp/scssphp/tree/v1.4.1" + }, "install-path": "../scssphp/scssphp" }, { diff --git a/composer/installed.php b/composer/installed.php index b44b96e2e..5fca74171 100644 --- a/composer/installed.php +++ b/composer/installed.php @@ -1,12 +1,12 @@ array ( - 'pretty_version' => 'dev-master', - 'version' => 'dev-master', + 'pretty_version' => 'v21.0.0beta5', + 'version' => '21.0.0.0-beta5', 'aliases' => array ( ), - 'reference' => '65a8a4553327c818931876ac1aa80a2143e02733', + 'reference' => '09596e43fba86a3643879595a8fb6fece4af6a78', 'name' => 'nextcloud/3rdparty', ), 'versions' => @@ -274,12 +274,12 @@ ), 'nextcloud/3rdparty' => array ( - 'pretty_version' => 'dev-master', - 'version' => 'dev-master', + 'pretty_version' => 'v21.0.0beta5', + 'version' => '21.0.0.0-beta5', 'aliases' => array ( ), - 'reference' => '65a8a4553327c818931876ac1aa80a2143e02733', + 'reference' => '09596e43fba86a3643879595a8fb6fece4af6a78', ), 'nextcloud/lognormalizer' => array ( @@ -630,12 +630,12 @@ ), 'scssphp/scssphp' => array ( - 'pretty_version' => '1.0.3', - 'version' => '1.0.3.0', + 'pretty_version' => 'v1.4.1', + 'version' => '1.4.1.0', 'aliases' => array ( ), - 'reference' => '616c518333c656eaa23182ac6cfc01453f1e7c78', + 'reference' => 'ba86c963b94ec7ebd6e19d90cdab90d89667dbf7', ), 'spomky-labs/base64url' => array ( diff --git a/composer/package-versions-deprecated/src/PackageVersions/Versions.php b/composer/package-versions-deprecated/src/PackageVersions/Versions.php index b7bed02c5..f030e0f8a 100644 --- a/composer/package-versions-deprecated/src/PackageVersions/Versions.php +++ b/composer/package-versions-deprecated/src/PackageVersions/Versions.php @@ -92,7 +92,7 @@ final class Versions 'sabre/uri' => '2.2.1@f502edffafea8d746825bd5f0b923a60fd2715ff', 'sabre/vobject' => '4.3.3@58f9f9b46a1080c0130bd86f4df9a568aacb9c79', 'sabre/xml' => '2.2.3@c3b959f821c19b36952ec4a595edd695c216bfc6', - 'scssphp/scssphp' => '1.0.3@616c518333c656eaa23182ac6cfc01453f1e7c78', + 'scssphp/scssphp' => 'v1.4.1@ba86c963b94ec7ebd6e19d90cdab90d89667dbf7', 'spomky-labs/base64url' => 'v2.0.1@3eb46a1de803f0078962d910e3a2759224a68c61', 'spomky-labs/cbor-php' => 'v1.0.8@575a66dc406575b030e3ba541b33447842f93185', 'stecman/symfony-console-completion' => '0.11.0@a9502dab59405e275a9f264536c4e1cb61fc3518', @@ -117,7 +117,7 @@ final class Versions 'web-auth/cose-lib' => 'v3.1.1@bc28b39608b0674546d89318e55fb37eaec1a9e9', 'web-auth/metadata-service' => 'v3.1.1@0e9d9b85590a0c5155b99d9a4b5bdaa01b1748e3', 'web-auth/webauthn-lib' => 'v3.1.1@a8a11bc30480e98768987fe0fc266aef08ec99da', - 'nextcloud/3rdparty' => 'dev-master@65a8a4553327c818931876ac1aa80a2143e02733', + 'nextcloud/3rdparty' => 'v21.0.0beta5@09596e43fba86a3643879595a8fb6fece4af6a78', ); private function __construct() diff --git a/scssphp/scssphp/README.md b/scssphp/scssphp/README.md index f541e4888..54557344d 100644 --- a/scssphp/scssphp/README.md +++ b/scssphp/scssphp/README.md @@ -1,12 +1,12 @@ # scssphp -### +### -[![Build](https://travis-ci.org/scssphp/scssphp.svg?branch=master)](http://travis-ci.org/scssphp/scssphp) +![Build](https://github.com/scssphp/scssphp/workflows/CI/badge.svg) [![License](https://poser.pugx.org/scssphp/scssphp/license)](https://packagist.org/packages/scssphp/scssphp) `scssphp` is a compiler for SCSS written in PHP. -Checkout the homepage, , for directions on how to use. +Checkout the homepage, , for directions on how to use. ## Running Tests @@ -23,8 +23,7 @@ There are several tests in the `tests/` directory: * `FailingTest.php` contains tests reported in Github issues that demonstrate compatibility bugs. * `InputTest.php` compiles every `.scss` file in the `tests/inputs` directory then compares to the respective `.css` file in the `tests/outputs` directory. -* `ScssTest.php` extracts (ruby) `scss` tests from the `tests/scss_test.rb` file. -* `ServerTest.php` contains functional tests for the `Server` class. +* `SassSpecTest.php` extracts tests from the `sass/sass-spec` repository. When changing any of the tests in `tests/inputs`, the tests will most likely fail because the output has changed. Once you verify that the output is correct @@ -32,16 +31,17 @@ you can run the following command to rebuild all the tests: BUILD=1 vendor/bin/phpunit tests -This will compile all the tests, and save results into `tests/outputs`. +This will compile all the tests, and save results into `tests/outputs`. It also +updates the list of excluded specs from sass-spec. -To enable the `scss` compatibility tests: +To enable the full `sass-spec` compatibility tests: - TEST_SCSS_COMPAT=1 vendor/bin/phpunit tests + TEST_SASS_SPEC=1 vendor/bin/phpunit tests ## Coding Standard -`scssphp` source conforms to [PSR2](http://www.php-fig.org/psr/psr-2/). +`scssphp` source conforms to [PSR12](https://www.php-fig.org/psr/psr-12/). Run the following command from the root directory to check the code for "sniffs". - vendor/bin/phpcs --standard=PSR2 bin src tests + vendor/bin/phpcs --standard=PSR12 --extensions=php bin src tests *.php diff --git a/scssphp/scssphp/composer.json b/scssphp/scssphp/composer.json index cbeb96578..e4c47d347 100644 --- a/scssphp/scssphp/composer.json +++ b/scssphp/scssphp/composer.json @@ -23,26 +23,43 @@ "psr-4": { "ScssPhp\\ScssPhp\\": "src/" } }, "autoload-dev": { - "psr-4": { "ScssPhp\\ScssPhp\\Test\\": "tests/" } + "psr-4": { "ScssPhp\\ScssPhp\\Tests\\": "tests/" } }, "require": { - "php": "^5.6.0 || ^7" + "php": ">=5.6.0", + "ext-json": "*", + "ext-ctype": "*" }, "require-dev": { - "squizlabs/php_codesniffer": "~2.5", - "phpunit/phpunit": "~4.6", + "phpunit/phpunit": "^5.7 || ^6.5 || ^7.5 || ^8.3 || ^9.4", + "sass/sass-spec": "2020.12.29", + "squizlabs/php_codesniffer": "~3.5", + "symfony/phpunit-bridge": "^5.1", "twbs/bootstrap": "~4.3", "zurb/foundation": "~6.5" }, + "repositories": [ + { + "type": "package", + "package": { + "name": "sass/sass-spec", + "version": "2020.12.29", + "source": { + "type": "git", + "url": "https://github.com/sass/sass-spec.git", + "reference": "d975d33146fb679a6b359ceca329012f02e4a794" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sass/sass-spec/zipball/d975d33146fb679a6b359ceca329012f02e4a794", + "reference": "d975d33146fb679a6b359ceca329012f02e4a794", + "shasum": "" + } + } + } + ], "bin": ["bin/pscss"], - "archive": { - "exclude": [ - "/Makefile", - "/.gitattributes", - "/.gitignore", - "/.travis.yml", - "/phpunit.xml.dist", - "/tests" - ] + "config": { + "sort-packages": true } } diff --git a/scssphp/scssphp/phpcs.xml.dist b/scssphp/scssphp/phpcs.xml.dist new file mode 100644 index 000000000..b162dbd6b --- /dev/null +++ b/scssphp/scssphp/phpcs.xml.dist @@ -0,0 +1,12 @@ + + + + + + + + + + diff --git a/scssphp/scssphp/scss.inc.php b/scssphp/scssphp/scss.inc.php index e4ec7f181..6b39d320d 100644 --- a/scssphp/scssphp/scss.inc.php +++ b/scssphp/scssphp/scss.inc.php @@ -1,15 +1,19 @@ */ class Cache { @@ -57,12 +58,12 @@ class Cache public function __construct($options) { // check $cacheDir - if (isset($options['cache_dir'])) { - self::$cacheDir = $options['cache_dir']; + if (isset($options['cacheDir'])) { + self::$cacheDir = $options['cacheDir']; } if (empty(self::$cacheDir)) { - throw new Exception('cache_dir not set'); + throw new Exception('cacheDir not set'); } if (isset($options['prefix'])) { @@ -74,7 +75,7 @@ public function __construct($options) } if (isset($options['forceRefresh'])) { - self::$forceRefresh = $options['force_refresh']; + self::$forceRefresh = $options['forceRefresh']; } self::checkCacheDir(); @@ -97,18 +98,20 @@ public function getCache($operation, $what, $options = [], $lastModified = null) { $fileCache = self::$cacheDir . self::cacheName($operation, $what, $options); - if ((! self::$forceRefresh || (self::$forceRefresh === 'once' && + if ( + ((self::$forceRefresh === false) || (self::$forceRefresh === 'once' && isset(self::$refreshed[$fileCache]))) && file_exists($fileCache) ) { $cacheTime = filemtime($fileCache); - if ((is_null($lastModified) || $cacheTime > $lastModified) && + if ( + (\is_null($lastModified) || $cacheTime > $lastModified) && $cacheTime + self::$gcLifetime > time() ) { $c = file_get_contents($fileCache); $c = unserialize($c); - if (is_array($c) && isset($c['value'])) { + if (\is_array($c) && isset($c['value'])) { return $c['value']; } } @@ -132,6 +135,7 @@ public function setCache($operation, $what, $value, $options = []) $c = ['value' => $value]; $c = serialize($c); + file_put_contents($fileCache, $c); if (self::$forceRefresh === 'once') { @@ -153,6 +157,7 @@ private static function cacheName($operation, $what, $options = []) { $t = [ 'version' => self::CACHE_VERSION, + 'scssphpVersion' => Version::VERSION, 'operation' => $operation, 'what' => $what, 'options' => $options @@ -176,13 +181,11 @@ public static function checkCacheDir() self::$cacheDir = str_replace('\\', '/', self::$cacheDir); self::$cacheDir = rtrim(self::$cacheDir, '/') . '/'; - if (! file_exists(self::$cacheDir)) { - if (! mkdir(self::$cacheDir)) { - throw new Exception('Cache directory couldn\'t be created: ' . self::$cacheDir); - } - } elseif (! is_dir(self::$cacheDir)) { + if (! is_dir(self::$cacheDir)) { throw new Exception('Cache directory doesn\'t exist: ' . self::$cacheDir); - } elseif (! is_writable(self::$cacheDir)) { + } + + if (! is_writable(self::$cacheDir)) { throw new Exception('Cache directory isn\'t writable: ' . self::$cacheDir); } } diff --git a/scssphp/scssphp/src/Colors.php b/scssphp/scssphp/src/Colors.php index ad459246a..4b62c361c 100644 --- a/scssphp/scssphp/src/Colors.php +++ b/scssphp/scssphp/src/Colors.php @@ -1,8 +1,9 @@ '240,248,255', 'antiquewhite' => '250,235,215', + 'cyan' => '0,255,255', 'aqua' => '0,255,255', 'aquamarine' => '127,255,212', 'azure' => '240,255,255', @@ -46,13 +48,12 @@ class Colors 'cornflowerblue' => '100,149,237', 'cornsilk' => '255,248,220', 'crimson' => '220,20,60', - 'cyan' => '0,255,255', 'darkblue' => '0,0,139', 'darkcyan' => '0,139,139', 'darkgoldenrod' => '184,134,11', 'darkgray' => '169,169,169', - 'darkgreen' => '0,100,0', 'darkgrey' => '169,169,169', + 'darkgreen' => '0,100,0', 'darkkhaki' => '189,183,107', 'darkmagenta' => '139,0,139', 'darkolivegreen' => '85,107,47', @@ -74,15 +75,16 @@ class Colors 'firebrick' => '178,34,34', 'floralwhite' => '255,250,240', 'forestgreen' => '34,139,34', + 'magenta' => '255,0,255', 'fuchsia' => '255,0,255', 'gainsboro' => '220,220,220', 'ghostwhite' => '248,248,255', 'gold' => '255,215,0', 'goldenrod' => '218,165,32', 'gray' => '128,128,128', + 'grey' => '128,128,128', 'green' => '0,128,0', 'greenyellow' => '173,255,47', - 'grey' => '128,128,128', 'honeydew' => '240,255,240', 'hotpink' => '255,105,180', 'indianred' => '205,92,92', @@ -98,8 +100,8 @@ class Colors 'lightcyan' => '224,255,255', 'lightgoldenrodyellow' => '250,250,210', 'lightgray' => '211,211,211', - 'lightgreen' => '144,238,144', 'lightgrey' => '211,211,211', + 'lightgreen' => '144,238,144', 'lightpink' => '255,182,193', 'lightsalmon' => '255,160,122', 'lightseagreen' => '32,178,170', @@ -111,7 +113,6 @@ class Colors 'lime' => '0,255,0', 'limegreen' => '50,205,50', 'linen' => '250,240,230', - 'magenta' => '255,0,255', 'maroon' => '128,0,0', 'mediumaquamarine' => '102,205,170', 'mediumblue' => '0,0,205', @@ -145,7 +146,6 @@ class Colors 'plum' => '221,160,221', 'powderblue' => '176,224,230', 'purple' => '128,0,128', - 'rebeccapurple' => '102,51,153', 'red' => '255,0,0', 'rosybrown' => '188,143,143', 'royalblue' => '65,105,225', @@ -167,7 +167,6 @@ class Colors 'teal' => '0,128,128', 'thistle' => '216,191,216', 'tomato' => '255,99,71', - 'transparent' => '0,0,0,0', 'turquoise' => '64,224,208', 'violet' => '238,130,238', 'wheat' => '245,222,179', @@ -175,5 +174,72 @@ class Colors 'whitesmoke' => '245,245,245', 'yellow' => '255,255,0', 'yellowgreen' => '154,205,50', + 'rebeccapurple' => '102,51,153', + 'transparent' => '0,0,0,0', ]; + + /** + * Convert named color in a [r,g,b[,a]] array + * + * @param string $colorName + * + * @return array|null + */ + public static function colorNameToRGBa($colorName) + { + if (\is_string($colorName) && isset(static::$cssColors[$colorName])) { + $rgba = explode(',', static::$cssColors[$colorName]); + + // only case with opacity is transparent, with opacity=0, so we can intval on opacity also + $rgba = array_map('intval', $rgba); + + return $rgba; + } + + return null; + } + + /** + * Reverse conversion : from RGBA to a color name if possible + * + * @param integer $r + * @param integer $g + * @param integer $b + * @param integer $a + * + * @return string|null + */ + public static function RGBaToColorName($r, $g, $b, $a = 1) + { + static $reverseColorTable = null; + + if (! is_numeric($r) || ! is_numeric($g) || ! is_numeric($b) || ! is_numeric($a)) { + return null; + } + + if ($a < 1) { + return null; + } + + if (\is_null($reverseColorTable)) { + $reverseColorTable = []; + + foreach (static::$cssColors as $name => $rgb_str) { + $rgb_str = explode(',', $rgb_str); + + if ( + \count($rgb_str) == 3 && + ! isset($reverseColorTable[\intval($rgb_str[0])][\intval($rgb_str[1])][\intval($rgb_str[2])]) + ) { + $reverseColorTable[\intval($rgb_str[0])][\intval($rgb_str[1])][\intval($rgb_str[2])] = $name; + } + } + } + + if (isset($reverseColorTable[\intval($r)][\intval($g)][\intval($b)])) { + return $reverseColorTable[\intval($r)][\intval($g)][\intval($b)]; + } + + return null; + } } diff --git a/scssphp/scssphp/src/Compiler.php b/scssphp/scssphp/src/Compiler.php index 6e4b729ec..0997814ee 100644 --- a/scssphp/scssphp/src/Compiler.php +++ b/scssphp/scssphp/src/Compiler.php @@ -1,8 +1,9 @@ */ - static protected $operatorNames = [ + protected static $operatorNames = [ '+' => 'add', '-' => 'sub', '*' => 'mul', @@ -87,83 +105,188 @@ class Compiler '<=' => 'lte', '>=' => 'gte', - '<=>' => 'cmp', ]; /** - * @var array + * @var array */ - static protected $namespaces = [ + protected static $namespaces = [ 'special' => '%', 'mixin' => '@', 'function' => '^', ]; - static public $true = [Type::T_KEYWORD, 'true']; - static public $false = [Type::T_KEYWORD, 'false']; - static public $null = [Type::T_NULL]; - static public $nullString = [Type::T_STRING, '', []]; - static public $defaultValue = [Type::T_KEYWORD, '']; - static public $selfSelector = [Type::T_SELF]; - static public $emptyList = [Type::T_LIST, '', []]; - static public $emptyMap = [Type::T_MAP, [], []]; - static public $emptyString = [Type::T_STRING, '"', []]; - static public $with = [Type::T_KEYWORD, 'with']; - static public $without = [Type::T_KEYWORD, 'without']; - - protected $importPaths = ['']; + public static $true = [Type::T_KEYWORD, 'true']; + public static $false = [Type::T_KEYWORD, 'false']; + /** @deprecated */ + public static $NaN = [Type::T_KEYWORD, 'NaN']; + /** @deprecated */ + public static $Infinity = [Type::T_KEYWORD, 'Infinity']; + public static $null = [Type::T_NULL]; + public static $nullString = [Type::T_STRING, '', []]; + public static $defaultValue = [Type::T_KEYWORD, '']; + public static $selfSelector = [Type::T_SELF]; + public static $emptyList = [Type::T_LIST, '', []]; + public static $emptyMap = [Type::T_MAP, [], []]; + public static $emptyString = [Type::T_STRING, '"', []]; + public static $with = [Type::T_KEYWORD, 'with']; + public static $without = [Type::T_KEYWORD, 'without']; + + /** + * @var array + */ + protected $importPaths = []; + /** + * @var array + */ protected $importCache = []; + /** + * @var string[] + */ protected $importedFiles = []; protected $userFunctions = []; protected $registeredVars = []; + /** + * @var array + */ protected $registeredFeatures = [ 'extend-selector-pseudoclass' => false, 'at-error' => true, - 'units-level-3' => false, + 'units-level-3' => true, 'global-variable-shadowing' => false, ]; + /** + * @var string|null + */ protected $encoding = null; + /** + * @deprecated + */ protected $lineNumberStyle = null; + /** + * @var int|SourceMapGenerator + * @phpstan-var self::SOURCE_MAP_*|SourceMapGenerator + */ protected $sourceMap = self::SOURCE_MAP_NONE; protected $sourceMapOptions = []; /** * @var string|\ScssPhp\ScssPhp\Formatter */ - protected $formatter = 'ScssPhp\ScssPhp\Formatter\Nested'; + protected $formatter = Expanded::class; + /** + * @var Environment + */ protected $rootEnv; + /** + * @var OutputBlock|null + */ protected $rootBlock; /** * @var \ScssPhp\ScssPhp\Compiler\Environment */ protected $env; + /** + * @var OutputBlock|null + */ protected $scope; + /** + * @var Environment|null + */ protected $storeEnv; + /** + * @var bool|null + */ protected $charsetSeen; + /** + * @var array + */ protected $sourceNames; + /** + * @var Cache|null + */ protected $cache; + /** + * @var int + */ protected $indentLevel; + /** + * @var array[] + */ protected $extends; + /** + * @var array + */ protected $extendsMap; + /** + * @var array + */ protected $parsedFiles; + /** + * @var Parser|null + */ protected $parser; + /** + * @var int|null + */ protected $sourceIndex; + /** + * @var int|null + */ protected $sourceLine; + /** + * @var int|null + */ protected $sourceColumn; + /** + * @var resource + */ protected $stderr; + /** + * @var bool|null + */ protected $shouldEvaluate; + /** + * @var null + * @deprecated + */ protected $ignoreErrors; + /** + * @var bool + */ + protected $ignoreCallStackMessage = false; + /** + * @var array[] + */ protected $callStack = []; + /** + * The directory of the currently processed file + * + * @var string|null + */ + private $currentDirectory; + + /** + * The directory of the input file + * + * @var string + */ + private $rootDirectory; + + private $legacyCwdImportPath = true; + /** * Constructor + * + * @param array|null $cacheOptions */ public function __construct($cacheOptions = null) { @@ -173,8 +296,15 @@ public function __construct($cacheOptions = null) if ($cacheOptions) { $this->cache = new Cache($cacheOptions); } + + $this->stderr = fopen('php://stderr', 'w'); } + /** + * Get compiler options + * + * @return array + */ public function getCompileOptions() { $options = [ @@ -185,11 +315,24 @@ public function getCompileOptions() 'sourceMap' => serialize($this->sourceMap), 'sourceMapOptions' => $this->sourceMapOptions, 'formatter' => $this->formatter, + 'legacyImportPath' => $this->legacyCwdImportPath, ]; return $options; } + /** + * Set an alternative error output stream, for testing purpose only + * + * @param resource $handle + * + * @return void + */ + public function setErrorOuput($handle) + { + $this->stderr = $handle; + } + /** * Compile scss * @@ -203,14 +346,14 @@ public function getCompileOptions() public function compile($code, $path = null) { if ($this->cache) { - $cacheKey = ($path ? $path : "(stdin)") . ":" . md5($code); + $cacheKey = ($path ? $path : '(stdin)') . ':' . md5($code); $compileOptions = $this->getCompileOptions(); - $cache = $this->cache->getCache("compile", $cacheKey, $compileOptions); + $cache = $this->cache->getCache('compile', $cacheKey, $compileOptions); - if (is_array($cache) && isset($cache['dependencies']) && isset($cache['out'])) { + 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 (! file_exists($file) || filemtime($file) !== $mtime) { + if (! is_file($file) || filemtime($file) !== $mtime) { unset($cache); break; } @@ -234,48 +377,70 @@ public function compile($code, $path = null) $this->storeEnv = null; $this->charsetSeen = null; $this->shouldEvaluate = null; - $this->stderr = fopen('php://stderr', 'w'); + $this->ignoreCallStackMessage = false; - $this->parser = $this->parserFactory($path); - $tree = $this->parser->parse($code); - $this->parser = null; + if (!\is_null($path) && is_file($path)) { + $path = realpath($path) ?: $path; + $this->currentDirectory = dirname($path); + $this->rootDirectory = $this->currentDirectory; + } else { + $this->currentDirectory = null; + $this->rootDirectory = getcwd(); + } - $this->formatter = new $this->formatter(); - $this->rootBlock = null; - $this->rootEnv = $this->pushEnv($tree); + try { + $this->parser = $this->parserFactory($path); + $tree = $this->parser->parse($code); + $this->parser = null; - $this->injectVariables($this->registeredVars); - $this->compileRoot($tree); - $this->popEnv(); + $this->formatter = new $this->formatter(); + $this->rootBlock = null; + $this->rootEnv = $this->pushEnv($tree); - $sourceMapGenerator = null; + $this->injectVariables($this->registeredVars); + $this->compileRoot($tree); + $this->popEnv(); - if ($this->sourceMap) { - if (is_object($this->sourceMap) && $this->sourceMap instanceof SourceMapGenerator) { - $sourceMapGenerator = $this->sourceMap; - $this->sourceMap = self::SOURCE_MAP_FILE; - } elseif ($this->sourceMap !== self::SOURCE_MAP_NONE) { - $sourceMapGenerator = new SourceMapGenerator($this->sourceMapOptions); - } - } + $sourceMapGenerator = null; - $out = $this->formatter->format($this->scope, $sourceMapGenerator); + if ($this->sourceMap) { + if (\is_object($this->sourceMap) && $this->sourceMap instanceof SourceMapGenerator) { + $sourceMapGenerator = $this->sourceMap; + $this->sourceMap = self::SOURCE_MAP_FILE; + } elseif ($this->sourceMap !== self::SOURCE_MAP_NONE) { + $sourceMapGenerator = new SourceMapGenerator($this->sourceMapOptions); + } + } - if (! empty($out) && $this->sourceMap && $this->sourceMap !== self::SOURCE_MAP_NONE) { - $sourceMap = $sourceMapGenerator->generateJson(); - $sourceMapUrl = null; + $out = $this->formatter->format($this->scope, $sourceMapGenerator); - switch ($this->sourceMap) { - case self::SOURCE_MAP_INLINE: - $sourceMapUrl = sprintf('data:application/json,%s', Util::encodeURIComponent($sourceMap)); - break; + $prefix = ''; - case self::SOURCE_MAP_FILE: - $sourceMapUrl = $sourceMapGenerator->saveMap($sourceMap); - break; + if (!$this->charsetSeen) { + if (strlen($out) !== Util::mbStrlen($out)) { + $prefix = '@charset "UTF-8";' . "\n"; + $out = $prefix . $out; + } } - $out .= sprintf('/*# sourceMappingURL=%s */', $sourceMapUrl); + if (! empty($out) && $this->sourceMap && $this->sourceMap !== self::SOURCE_MAP_NONE) { + $sourceMap = $sourceMapGenerator->generateJson($prefix); + $sourceMapUrl = null; + + switch ($this->sourceMap) { + case self::SOURCE_MAP_INLINE: + $sourceMapUrl = sprintf('data:application/json,%s', Util::encodeURIComponent($sourceMap)); + break; + + case self::SOURCE_MAP_FILE: + $sourceMapUrl = $sourceMapGenerator->saveMap($sourceMap); + break; + } + + $out .= sprintf('/*# sourceMappingURL=%s */', $sourceMapUrl); + } + } catch (SassScriptException $e) { + throw $this->error($e->getMessage()); } if ($this->cache && isset($cacheKey) && isset($compileOptions)) { @@ -284,7 +449,7 @@ public function compile($code, $path = null) 'out' => &$out, ]; - $this->cache->setCache("compile", $cacheKey, $v, $compileOptions); + $this->cache->setCache('compile', $cacheKey, $v, $compileOptions); } return $out; @@ -299,7 +464,18 @@ public function compile($code, $path = null) */ protected function parserFactory($path) { - $parser = new Parser($path, count($this->sourceNames), $this->encoding, $this->cache); + // https://sass-lang.com/documentation/at-rules/import + // CSS files imported by Sass don’t allow any special Sass features. + // In order to make sure authors don’t accidentally write Sass in their CSS, + // all Sass features that aren’t also valid CSS will produce errors. + // Otherwise, the CSS will be rendered as-is. It can even be extended! + $cssOnly = false; + + if (substr($path, '-4') === '.css') { + $cssOnly = true; + } + + $parser = new Parser($path, \count($this->sourceNames), $this->encoding, $this->cache, $cssOnly); $this->sourceNames[] = $path; $this->addParsedFile($path); @@ -318,7 +494,7 @@ protected function parserFactory($path) protected function isSelfExtend($target, $origin) { foreach ($origin as $sel) { - if (in_array($target, $sel)) { + if (\in_array($target, $sel)) { return true; } } @@ -329,17 +505,15 @@ protected function isSelfExtend($target, $origin) /** * Push extends * - * @param array $target - * @param array $origin - * @param \stdClass $block + * @param array $target + * @param array $origin + * @param array|null $block + * + * @return void */ protected function pushExtends($target, $origin, $block) { - if ($this->isSelfExtend($target, $origin)) { - return; - } - - $i = count($this->extends); + $i = \count($this->extends); $this->extends[] = [$target, $origin, $block]; foreach ($target as $part) { @@ -361,13 +535,13 @@ protected function pushExtends($target, $origin, $block) */ protected function makeOutputBlock($type, $selectors = null) { - $out = new OutputBlock; - $out->type = $type; - $out->lines = []; - $out->children = []; - $out->parent = $this->scope; - $out->selectors = $selectors; - $out->depth = $this->env->depth; + $out = new OutputBlock(); + $out->type = $type; + $out->lines = []; + $out->children = []; + $out->parent = $this->scope; + $out->selectors = $selectors; + $out->depth = $this->env->depth; if ($this->env->block instanceof Block) { $out->sourceName = $this->env->block->sourceName; @@ -386,6 +560,8 @@ protected function makeOutputBlock($type, $selectors = null) * Compile root * * @param \ScssPhp\ScssPhp\Block $rootBlock + * + * @return void */ protected function compileRoot(Block $rootBlock) { @@ -398,6 +574,8 @@ protected function compileRoot(Block $rootBlock) /** * Report missing selectors + * + * @return void */ protected function missingSelectors() { @@ -417,7 +595,7 @@ protected function missingSelectors() $origin = $this->collapseSelectors($origin); $this->sourceLine = $block[Parser::SOURCE_LINE]; - $this->throwError("\"$origin\" failed to @extend \"$target\". The selector \"$target\" was not found."); + throw $this->error("\"$origin\" failed to @extend \"$target\". The selector \"$target\" was not found."); } } @@ -426,6 +604,8 @@ protected function missingSelectors() * * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $block * @param string $parentKey + * + * @return void */ protected function flattenSelectors(OutputBlock $block, $parentKey = null) { @@ -435,7 +615,7 @@ protected function flattenSelectors(OutputBlock $block, $parentKey = null) foreach ($block->selectors as $s) { $selectors[] = $s; - if (! is_array($s)) { + if (! \is_array($s)) { continue; } @@ -468,7 +648,7 @@ protected function flattenSelectors(OutputBlock $block, $parentKey = null) $block->selectors[] = $this->compileSelector($selector); } - if ($placeholderSelector && 0 === count($block->selectors) && null !== $parentKey) { + if ($placeholderSelector && 0 === \count($block->selectors) && null !== $parentKey) { unset($block->parent->children[$parentKey]); return; @@ -492,16 +672,20 @@ protected function glueFunctionSelectors($parts) $new = []; foreach ($parts as $part) { - if (is_array($part)) { + if (\is_array($part)) { $part = $this->glueFunctionSelectors($part); $new[] = $part; } else { // a selector part finishing with a ) is the last part of a :not( or :nth-child( // and need to be joined to this - if (count($new) && is_string($new[count($new) - 1]) && - strlen($part) && substr($part, -1) === ')' && strpos($part, '(') === false + if ( + \count($new) && \is_string($new[\count($new) - 1]) && + \strlen($part) && substr($part, -1) === ')' && strpos($part, '(') === false ) { - $new[count($new) - 1] .= $part; + while (\count($new) > 1 && substr($new[\count($new) - 1], -1) !== '(') { + $part = array_pop($new) . $part; + } + $new[\count($new) - 1] .= $part; } else { $new[] = $part; } @@ -518,17 +702,20 @@ protected function glueFunctionSelectors($parts) * @param array $out * @param integer $from * @param boolean $initial + * + * @return void */ protected function matchExtends($selector, &$out, $from = 0, $initial = true) { static $partsPile = []; - $selector = $this->glueFunctionSelectors($selector); - if (count($selector) == 1 && in_array(reset($selector), $partsPile)) { + if (\count($selector) == 1 && \in_array(reset($selector), $partsPile)) { return; } + $outRecurs = []; + foreach ($selector as $i => $part) { if ($i < $from) { continue; @@ -536,39 +723,43 @@ protected function matchExtends($selector, &$out, $from = 0, $initial = true) // check that we are not building an infinite loop of extensions // if the new part is just including a previous part don't try to extend anymore - if (count($part) > 1) { + if (\count($part) > 1) { foreach ($partsPile as $previousPart) { - if (! count(array_diff($previousPart, $part))) { + if (! \count(array_diff($previousPart, $part))) { continue 2; } } } - if ($this->matchExtendsSingle($part, $origin)) { - $partsPile[] = $part; - $after = array_slice($selector, $i + 1); - $before = array_slice($selector, 0, $i); + $partsPile[] = $part; + if ($this->matchExtendsSingle($part, $origin, $initial)) { + $after = \array_slice($selector, $i + 1); + $before = \array_slice($selector, 0, $i); list($before, $nonBreakableBefore) = $this->extractRelationshipFromFragment($before); foreach ($origin as $new) { $k = 0; // remove shared parts - if (count($new) > 1) { + if (\count($new) > 1) { while ($k < $i && isset($new[$k]) && $selector[$k] === $new[$k]) { $k++; } } + if (\count($nonBreakableBefore) && $k === \count($new)) { + $k--; + } + $replacement = []; - $tempReplacement = $k > 0 ? array_slice($new, $k) : $new; + $tempReplacement = $k > 0 ? \array_slice($new, $k) : $new; - for ($l = count($tempReplacement) - 1; $l >= 0; $l--) { + for ($l = \count($tempReplacement) - 1; $l >= 0; $l--) { $slice = []; foreach ($tempReplacement[$l] as $chunk) { - if (! in_array($chunk, $slice)) { + if (! \in_array($chunk, $slice)) { $slice[] = $chunk; } } @@ -580,7 +771,7 @@ protected function matchExtends($selector, &$out, $from = 0, $initial = true) } } - $afterBefore = $l != 0 ? array_slice($tempReplacement, 0, $l) : []; + $afterBefore = $l != 0 ? \array_slice($tempReplacement, 0, $l) : []; // Merge shared direct relationships. $mergedBefore = $this->mergeDirectRelationships($afterBefore, $nonBreakableBefore); @@ -596,64 +787,139 @@ protected function matchExtends($selector, &$out, $from = 0, $initial = true) continue; } - $out[] = $result; + $this->pushOrMergeExtentedSelector($out, $result); // recursively check for more matches - $startRecurseFrom = count($before) + min(count($nonBreakableBefore), count($mergedBefore)); - $this->matchExtends($result, $out, $startRecurseFrom, false); + $startRecurseFrom = \count($before) + min(\count($nonBreakableBefore), \count($mergedBefore)); + + if (\count($origin) > 1) { + $this->matchExtends($result, $out, $startRecurseFrom, false); + } else { + $this->matchExtends($result, $outRecurs, $startRecurseFrom, false); + } // selector sequence merging - if (! empty($before) && count($new) > 1) { - $preSharedParts = $k > 0 ? array_slice($before, 0, $k) : []; - $postSharedParts = $k > 0 ? array_slice($before, $k) : $before; + if (! empty($before) && \count($new) > 1) { + $preSharedParts = $k > 0 ? \array_slice($before, 0, $k) : []; + $postSharedParts = $k > 0 ? \array_slice($before, $k) : $before; - list($betweenSharedParts, $nonBreakable2) = $this->extractRelationshipFromFragment($afterBefore); + list($betweenSharedParts, $nonBreakabl2) = $this->extractRelationshipFromFragment($afterBefore); $result2 = array_merge( $preSharedParts, $betweenSharedParts, $postSharedParts, - $nonBreakable2, + $nonBreakabl2, $nonBreakableBefore, $replacement, $after ); - $out[] = $result2; + $this->pushOrMergeExtentedSelector($out, $result2); } } + } + array_pop($partsPile); + } + + while (\count($outRecurs)) { + $result = array_shift($outRecurs); + $this->pushOrMergeExtentedSelector($out, $result); + } + } + + /** + * Test a part for being a pseudo selector + * + * @param string $part + * @param array $matches + * + * @return boolean + */ + protected function isPseudoSelector($part, &$matches) + { + if ( + strpos($part, ':') === 0 && + preg_match(",^::?([\w-]+)\((.+)\)$,", $part, $matches) + ) { + return true; + } + + return false; + } + + /** + * Push extended selector except if + * - this is a pseudo selector + * - same as previous + * - in a white list + * in this case we merge the pseudo selector content + * + * @param array $out + * @param array $extended + * + * @return void + */ + protected function pushOrMergeExtentedSelector(&$out, $extended) + { + if (\count($out) && \count($extended) === 1 && \count(reset($extended)) === 1) { + $single = reset($extended); + $part = reset($single); + + if ( + $this->isPseudoSelector($part, $matchesExtended) && + \in_array($matchesExtended[1], [ 'slotted' ]) + ) { + $prev = end($out); + $prev = $this->glueFunctionSelectors($prev); + + if (\count($prev) === 1 && \count(reset($prev)) === 1) { + $single = reset($prev); + $part = reset($single); - array_pop($partsPile); + if ( + $this->isPseudoSelector($part, $matchesPrev) && + $matchesPrev[1] === $matchesExtended[1] + ) { + $extended = explode($matchesExtended[1] . '(', $matchesExtended[0], 2); + $extended[1] = $matchesPrev[2] . ', ' . $extended[1]; + $extended = implode($matchesExtended[1] . '(', $extended); + $extended = [ [ $extended ]]; + array_pop($out); + } + } } } + $out[] = $extended; } /** * Match extends single * - * @param array $rawSingle - * @param array $outOrigin + * @param array $rawSingle + * @param array $outOrigin + * @param boolean $initial * * @return boolean */ - protected function matchExtendsSingle($rawSingle, &$outOrigin) + protected function matchExtendsSingle($rawSingle, &$outOrigin, $initial = true) { $counts = []; $single = []; // simple usual cases, no need to do the whole trick - if (in_array($rawSingle, [['>'],['+'],['~']])) { + if (\in_array($rawSingle, [['>'],['+'],['~']])) { return false; } foreach ($rawSingle as $part) { // matches Number - if (! is_string($part)) { + if (! \is_string($part)) { return false; } - if (! preg_match('/^[\[.:#%]/', $part) && count($single)) { - $single[count($single) - 1] .= $part; + if (! preg_match('/^[\[.:#%]/', $part) && \count($single)) { + $single[\count($single) - 1] .= $part; } else { $single[] = $part; } @@ -661,21 +927,52 @@ protected function matchExtendsSingle($rawSingle, &$outOrigin) $extendingDecoratedTag = false; - if (count($single) > 1) { + if (\count($single) > 1) { $matches = null; $extendingDecoratedTag = preg_match('/^[a-z0-9]+$/i', $single[0], $matches) ? $matches[0] : false; } - foreach ($single as $part) { + $outOrigin = []; + $found = false; + + foreach ($single as $k => $part) { if (isset($this->extendsMap[$part])) { foreach ($this->extendsMap[$part] as $idx) { $counts[$idx] = isset($counts[$idx]) ? $counts[$idx] + 1 : 1; } } - } - $outOrigin = []; - $found = false; + if ( + $initial && + $this->isPseudoSelector($part, $matches) && + ! \in_array($matches[1], [ 'not' ]) + ) { + $buffer = $matches[2]; + $parser = $this->parserFactory(__METHOD__); + + if ($parser->parseSelector($buffer, $subSelectors, false)) { + foreach ($subSelectors as $ksub => $subSelector) { + $subExtended = []; + $this->matchExtends($subSelector, $subExtended, 0, false); + + if ($subExtended) { + $subSelectorsExtended = $subSelectors; + $subSelectorsExtended[$ksub] = $subExtended; + + foreach ($subSelectorsExtended as $ksse => $sse) { + $subSelectorsExtended[$ksse] = $this->collapseSelectors($sse); + } + + $subSelectorsExtended = implode(', ', $subSelectorsExtended); + $singleExtended = $single; + $singleExtended[$k] = str_replace('(' . $buffer . ')', "($subSelectorsExtended)", $part); + $outOrigin[] = [ $singleExtended ]; + $found = true; + } + } + } + } + } foreach ($counts as $idx => $count) { list($target, $origin, /* $block */) = $this->extends[$idx]; @@ -683,7 +980,7 @@ protected function matchExtendsSingle($rawSingle, &$outOrigin) $origin = $this->glueFunctionSelectors($origin); // check count - if ($count !== count($target)) { + if ($count !== \count($target)) { continue; } @@ -693,14 +990,15 @@ protected function matchExtendsSingle($rawSingle, &$outOrigin) foreach ($origin as $j => $new) { // prevent infinite loop when target extends itself - if ($this->isSelfExtend($single, $origin)) { + if ($this->isSelfExtend($single, $origin) && ! $initial) { return false; } $replacement = end($new); // Extending a decorated tag with another tag is not possible. - if ($extendingDecoratedTag && $replacement[0] != $extendingDecoratedTag && + if ( + $extendingDecoratedTag && $replacement[0] != $extendingDecoratedTag && preg_match('/^[a-z0-9]+$/i', $replacement[0]) ) { unset($origin[$j]); @@ -709,8 +1007,8 @@ protected function matchExtendsSingle($rawSingle, &$outOrigin) $combined = $this->combineSelectorSingle($replacement, $rem); - if (count(array_diff($combined, $origin[$j][count($origin[$j]) - 1]))) { - $origin[$j][count($origin[$j]) - 1] = $combined; + if (\count(array_diff($combined, $origin[$j][\count($origin[$j]) - 1]))) { + $origin[$j][\count($origin[$j]) - 1] = $combined; } } @@ -738,12 +1036,13 @@ protected function extractRelationshipFromFragment(array $fragment) { $parents = []; $children = []; - $j = $i = count($fragment); + + $j = $i = \count($fragment); for (;;) { - $children = $j != $i ? array_slice($fragment, $j, $i - $j) : []; - $parents = array_slice($fragment, 0, $j); - $slice = end($parents); + $children = $j != $i ? \array_slice($fragment, $j, $i - $j) : []; + $parents = \array_slice($fragment, 0, $j); + $slice = end($parents); if (empty($slice) || ! $this->isImmediateRelationshipCombinator($slice[0])) { break; @@ -765,30 +1064,45 @@ protected function extractRelationshipFromFragment(array $fragment) */ protected function combineSelectorSingle($base, $other) { - $tag = []; - $out = []; - $wasTag = true; + $tag = []; + $out = []; + $wasTag = false; + $pseudo = []; + + while (\count($other) && strpos(end($other), ':') === 0) { + array_unshift($pseudo, array_pop($other)); + } + + foreach ([array_reverse($base), array_reverse($other)] as $single) { + $rang = count($single); - foreach ([$base, $other] as $single) { foreach ($single as $part) { - if (preg_match('/^[\[.:#]/', $part)) { + if (preg_match('/^[\[:]/', $part)) { $out[] = $part; $wasTag = false; - } elseif (preg_match('/^[^_-]/', $part)) { + } elseif (preg_match('/^[\.#]/', $part)) { + array_unshift($out, $part); + $wasTag = false; + } elseif (preg_match('/^[^_-]/', $part) && $rang === 1) { $tag[] = $part; $wasTag = true; } elseif ($wasTag) { - $tag[count($tag) - 1] .= $part; + $tag[\count($tag) - 1] .= $part; } else { - $out[count($out) - 1] .= $part; + array_unshift($out, $part); } + $rang--; } } - if (count($tag)) { + if (\count($tag)) { array_unshift($out, $tag[0]); } + while (\count($pseudo)) { + $out[] = array_shift($pseudo); + } + return $out; } @@ -796,6 +1110,8 @@ protected function combineSelectorSingle($base, $other) * Compile media * * @param \ScssPhp\ScssPhp\Block $media + * + * @return void */ protected function compileMedia(Block $media) { @@ -820,7 +1136,8 @@ protected function compileMedia(Block $media) foreach ($media->children as $child) { $type = $child[0]; - if ($type !== Type::T_BLOCK && + if ( + $type !== Type::T_BLOCK && $type !== Type::T_MEDIA && $type !== Type::T_DIRECTIVE && $type !== Type::T_IMPORT @@ -831,40 +1148,17 @@ protected function compileMedia(Block $media) } if ($needsWrap) { - $wrapped = new Block; - $wrapped->sourceName = $media->sourceName; - $wrapped->sourceIndex = $media->sourceIndex; - $wrapped->sourceLine = $media->sourceLine; + $wrapped = new Block(); + $wrapped->sourceName = $media->sourceName; + $wrapped->sourceIndex = $media->sourceIndex; + $wrapped->sourceLine = $media->sourceLine; $wrapped->sourceColumn = $media->sourceColumn; - $wrapped->selectors = []; - $wrapped->comments = []; - $wrapped->parent = $media; - $wrapped->children = $media->children; + $wrapped->selectors = []; + $wrapped->comments = []; + $wrapped->parent = $media; + $wrapped->children = $media->children; $media->children = [[Type::T_BLOCK, $wrapped]]; - if (isset($this->lineNumberStyle)) { - $annotation = $this->makeOutputBlock(Type::T_COMMENT); - $annotation->depth = 0; - - $file = $this->sourceNames[$media->sourceIndex]; - $line = $media->sourceLine; - - switch ($this->lineNumberStyle) { - case static::LINE_COMMENTS: - $annotation->lines[] = '/* line ' . $line - . ($file ? ', ' . $file : '') - . ' */'; - break; - - case static::DEBUG_INFO: - $annotation->lines[] = '@media -sass-debug-info{' - . ($file ? 'filename{font-family:"' . $file . '"}' : '') - . 'line{font-family:' . $line . '}}'; - break; - } - - $this->scope->children[] = $annotation; - } } $this->compileChildrenNoReturn($media->children, $this->scope); @@ -898,27 +1192,69 @@ protected function mediaParent(OutputBlock $scope) /** * Compile directive * - * @param \ScssPhp\ScssPhp\Block $block + * @param \ScssPhp\ScssPhp\Block|array $block + * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $out + * + * @return void */ - protected function compileDirective(Block $block) + protected function compileDirective($directive, OutputBlock $out) { - $s = '@' . $block->name; + if (\is_array($directive)) { + $directiveName = $this->compileDirectiveName($directive[0]); + $s = '@' . $directiveName; - if (! empty($block->value)) { - $s .= ' ' . $this->compileValue($block->value); - } + if (! empty($directive[1])) { + $s .= ' ' . $this->compileValue($directive[1]); + } + // sass-spec compliance on newline after directives, a bit tricky :/ + $appendNewLine = (! empty($directive[2]) || strpos($s, "\n")) ? "\n" : ""; + if (\is_array($directive[0]) && empty($directive[1])) { + $appendNewLine = "\n"; + } - if ($block->name === 'keyframes' || substr($block->name, -10) === '-keyframes') { - $this->compileKeyframeBlock($block, [$s]); + if (empty($directive[3])) { + $this->appendRootDirective($s . ';' . $appendNewLine, $out, [Type::T_COMMENT, Type::T_DIRECTIVE]); + } else { + $this->appendOutputLine($out, Type::T_DIRECTIVE, $s . ';'); + } } else { - $this->compileNestedBlock($block, [$s]); + $directive->name = $this->compileDirectiveName($directive->name); + $s = '@' . $directive->name; + + if (! empty($directive->value)) { + $s .= ' ' . $this->compileValue($directive->value); + } + + if ($directive->name === 'keyframes' || substr($directive->name, -10) === '-keyframes') { + $this->compileKeyframeBlock($directive, [$s]); + } else { + $this->compileNestedBlock($directive, [$s]); + } + } + } + + /** + * directive names can include some interpolation + * + * @param string|array $directiveName + * @return array|string + * @throws CompilerException + */ + protected function compileDirectiveName($directiveName) + { + if (is_string($directiveName)) { + return $directiveName; } + + return $this->compileValue($directiveName); } /** * Compile at-root * * @param \ScssPhp\ScssPhp\Block $block + * + * @return void */ protected function compileAtRoot(Block $block) { @@ -928,7 +1264,7 @@ protected function compileAtRoot(Block $block) // wrap inline selector if ($block->selector) { - $wrapped = new Block; + $wrapped = new Block(); $wrapped->sourceName = $block->sourceName; $wrapped->sourceIndex = $block->sourceIndex; $wrapped->sourceLine = $block->sourceLine; @@ -945,7 +1281,9 @@ protected function compileAtRoot(Block $block) $selfParent = $block->selfParent; - if (! $block->selfParent->selectors && isset($block->parent) && $block->parent && + if ( + ! $block->selfParent->selectors && + isset($block->parent) && $block->parent && isset($block->parent->selectors) && $block->parent->selectors ) { $selfParent = $block->parent; @@ -973,7 +1311,7 @@ protected function compileAtRoot(Block $block) * @param array $with * @param array $without * - * @return mixed + * @return OutputBlock */ protected function filterScopeWithWithout($scope, $with, $without) { @@ -998,8 +1336,8 @@ protected function filterScopeWithWithout($scope, $with, $without) if ($this->isWith($scope, $with, $without)) { $s = clone $scope; $s->children = []; - $s->lines = []; - $s->parent = null; + $s->lines = []; + $s->parent = null; if ($s->type !== Type::T_MEDIA && $s->type !== Type::T_DIRECTIVE) { $s->selectors = []; @@ -1008,7 +1346,7 @@ protected function filterScopeWithWithout($scope, $with, $without) $filteredScopes[] = $s; } - if (count($childStash)) { + if (\count($childStash)) { $scope = array_shift($childStash); } elseif ($scope->children) { $scope = end($scope->children); @@ -1017,7 +1355,7 @@ protected function filterScopeWithWithout($scope, $with, $without) } } - if (! count($filteredScopes)) { + if (! \count($filteredScopes)) { return $this->rootBlock; } @@ -1028,7 +1366,7 @@ protected function filterScopeWithWithout($scope, $with, $without) $p = &$newScope; - while (count($filteredScopes)) { + while (\count($filteredScopes)) { $s = array_shift($filteredScopes); $s->parent = $p; $p->children[] = $s; @@ -1046,11 +1384,11 @@ protected function filterScopeWithWithout($scope, $with, $without) * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $scope * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $previousScope * - * @return mixed + * @return OutputBlock */ protected function completeScope($scope, $previousScope) { - if (! $scope->type && (! $scope->selectors || ! count($scope->selectors)) && count($scope->lines)) { + if (! $scope->type && (! $scope->selectors || ! \count($scope->selectors)) && \count($scope->lines)) { $scope->selectors = $this->findScopeSelectors($previousScope, $scope->depth); } @@ -1102,6 +1440,17 @@ protected function compileWith($withCondition) $without = ['rule' => true]; if ($withCondition) { + if ($withCondition[0] === Type::T_INTERPOLATE) { + $w = $this->compileValue($withCondition); + + $buffer = "($w)"; + $parser = $this->parserFactory(__METHOD__); + + if ($parser->parseValue($buffer, $reParsedWith)) { + $withCondition = $reParsedWith; + } + } + if ($this->libMapHasKey([$withCondition, static::$with])) { $without = []; // cancel the default $list = $this->coerceList($this->libMapGet([$withCondition, static::$with])); @@ -1131,11 +1480,13 @@ protected function compileWith($withCondition) /** * Filter env stack * - * @param array $envs + * @param Environment[] $envs * @param array $with * @param array $without * - * @return \ScssPhp\ScssPhp\Compiler\Environment + * @return Environment + * + * @phpstan-param non-empty-array $envs */ protected function filterWithWithout($envs, $with, $without) { @@ -1144,8 +1495,9 @@ protected function filterWithWithout($envs, $with, $without) foreach ($envs as $e) { if ($e->block && ! $this->isWith($e->block, $with, $without)) { $ec = clone $e; - $ec->block = null; + $ec->block = null; $ec->selectors = []; + $filtered[] = $ec; } else { $filtered[] = $e; @@ -1173,7 +1525,7 @@ protected function isWith($block, $with, $without) if ($block->type === Type::T_DIRECTIVE) { if (isset($block->name)) { - return $this->testWithWithout($block->name, $with, $without); + return $this->testWithWithout($this->compileDirectiveName($block->name), $with, $without); } elseif (isset($block->selectors) && preg_match(',@(\w+),ims', json_encode($block->selectors), $m)) { return $this->testWithWithout($m[1], $with, $without); } else { @@ -1182,15 +1534,18 @@ protected function isWith($block, $with, $without) } } elseif (isset($block->selectors)) { // a selector starting with number is a keyframe rule - if (count($block->selectors)) { + if (\count($block->selectors)) { $s = reset($block->selectors); - while (is_array($s)) { + + while (\is_array($s)) { $s = reset($s); } - if (is_object($s) && get_class($s) === 'ScssPhp\ScssPhp\Node\Number') { + + if (\is_object($s) && $s instanceof Number) { return $this->testWithWithout('keyframes', $with, $without); } } + return $this->testWithWithout('rule', $with, $without); } @@ -1203,14 +1558,14 @@ protected function isWith($block, $with, $without) * @param string $what * @param array $with * @param array $without - * @return bool + * + * @return boolean * true if the block should be kept, false to reject */ protected function testWithWithout($what, $with, $without) { - // if without, reject only if in the list (or 'all' is in the list) - if (count($without)) { + if (\count($without)) { return (isset($without[$what]) || isset($without['all'])) ? false : true; } @@ -1224,6 +1579,8 @@ protected function testWithWithout($what, $with, $without) * * @param \ScssPhp\ScssPhp\Block $block * @param array $selectors + * + * @return void */ protected function compileKeyframeBlock(Block $block, $selectors) { @@ -1250,8 +1607,10 @@ protected function compileKeyframeBlock(Block $block, $selectors) /** * Compile nested properties lines * - * @param \ScssPhp\ScssPhp\Block $block - * @param OutputBlock $out + * @param \ScssPhp\ScssPhp\Block $block + * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $out + * + * @return void */ protected function compileNestedPropertiesBlock(Block $block, OutputBlock $out) { @@ -1276,6 +1635,7 @@ protected function compileNestedPropertiesBlock(Block $block, OutputBlock $out) array_unshift($child[1]->prefix[2], $prefix); break; } + $this->compileChild($child, $nested); } } @@ -1285,6 +1645,8 @@ protected function compileNestedPropertiesBlock(Block $block, OutputBlock $out) * * @param \ScssPhp\ScssPhp\Block $block * @param array $selectors + * + * @return void */ protected function compileNestedBlock(Block $block, $selectors) { @@ -1295,7 +1657,7 @@ protected function compileNestedBlock(Block $block, $selectors) // wrap assign children in a block // except for @font-face - if ($block->type !== Type::T_DIRECTIVE || $block->name !== "font-face") { + if ($block->type !== Type::T_DIRECTIVE || $this->compileDirectiveName($block->name) !== 'font-face') { // need wrapping? $needWrapping = false; @@ -1307,16 +1669,16 @@ protected function compileNestedBlock(Block $block, $selectors) } if ($needWrapping) { - $wrapped = new Block; - $wrapped->sourceName = $block->sourceName; - $wrapped->sourceIndex = $block->sourceIndex; - $wrapped->sourceLine = $block->sourceLine; + $wrapped = new Block(); + $wrapped->sourceName = $block->sourceName; + $wrapped->sourceIndex = $block->sourceIndex; + $wrapped->sourceLine = $block->sourceLine; $wrapped->sourceColumn = $block->sourceColumn; - $wrapped->selectors = []; - $wrapped->comments = []; - $wrapped->parent = $block; - $wrapped->children = $block->children; - $wrapped->selfParent = $block->selfParent; + $wrapped->selectors = []; + $wrapped->comments = []; + $wrapped->parent = $block; + $wrapped->children = $block->children; + $wrapped->selfParent = $block->selfParent; $block->children = [[Type::T_BLOCK, $wrapped]]; } @@ -1346,6 +1708,8 @@ protected function compileNestedBlock(Block $block, $selectors) * @see Compiler::compileChild() * * @param \ScssPhp\ScssPhp\Block $block + * + * @return void */ protected function compileBlock(Block $block) { @@ -1354,33 +1718,9 @@ protected function compileBlock(Block $block) $out = $this->makeOutputBlock(null); - if (isset($this->lineNumberStyle) && count($env->selectors) && count($block->children)) { - $annotation = $this->makeOutputBlock(Type::T_COMMENT); - $annotation->depth = 0; - - $file = $this->sourceNames[$block->sourceIndex]; - $line = $block->sourceLine; - - switch ($this->lineNumberStyle) { - case static::LINE_COMMENTS: - $annotation->lines[] = '/* line ' . $line - . ($file ? ', ' . $file : '') - . ' */'; - break; - - case static::DEBUG_INFO: - $annotation->lines[] = '@media -sass-debug-info{' - . ($file ? 'filename{font-family:"' . $file . '"}' : '') - . 'line{font-family:' . $line . '}}'; - break; - } - - $this->scope->children[] = $annotation; - } - $this->scope->children[] = $out; - if (count($block->children)) { + if (\count($block->children)) { $out->selectors = $this->multiplySelectors($env, $block->selfParent); // propagate selfParent to the children where they still can be useful @@ -1393,31 +1733,68 @@ protected function compileBlock(Block $block) $this->compileChildrenNoReturn($block->children, $out, $block->selfParent); - // and revert for the following childs of the same block + // and revert for the following children of the same block if ($selfParentSelectors) { $block->selfParent->selectors = $selfParentSelectors; } } - $this->formatter->stripSemicolon($out->lines); - $this->popEnv(); } + /** - * Compile root level comment + * Compile the value of a comment that can have interpolation * - * @param array $block + * @param array $value + * @param boolean $pushEnv + * + * @return string */ - protected function compileComment($block) + protected function compileCommentValue($value, $pushEnv = false) { - $out = $this->makeOutputBlock(Type::T_COMMENT); - $out->lines[] = is_string($block[1]) ? $block[1] : $this->compileValue($block[1]); + $c = $value[1]; - $this->scope->children[] = $out; - } + if (isset($value[2])) { + if ($pushEnv) { + $this->pushEnv(); + } - /** + $ignoreCallStackMessage = $this->ignoreCallStackMessage; + $this->ignoreCallStackMessage = true; + + try { + $c = $this->compileValue($value[2]); + } catch (\Exception $e) { + // ignore error in comment compilation which are only interpolation + } + + $this->ignoreCallStackMessage = $ignoreCallStackMessage; + + if ($pushEnv) { + $this->popEnv(); + } + } + + return $c; + } + + /** + * Compile root level comment + * + * @param array $block + * + * @return void + */ + protected function compileComment($block) + { + $out = $this->makeOutputBlock(Type::T_COMMENT); + $out->lines[] = $this->compileCommentValue($block, true); + + $this->scope->children[] = $out; + } + + /** * Evaluate selectors * * @param array $selectors @@ -1432,11 +1809,17 @@ protected function evalSelectors($selectors) // after evaluating interpolates, we might need a second pass if ($this->shouldEvaluate) { - $selectors = $this->revertSelfSelector($selectors); - $buffer = $this->collapseSelectors($selectors); - $parser = $this->parserFactory(__METHOD__); + $selectors = $this->replaceSelfSelector($selectors, '&'); + $buffer = $this->collapseSelectors($selectors); + $parser = $this->parserFactory(__METHOD__); + + try { + $isValid = $parser->parseSelector($buffer, $newSelectors, true); + } catch (ParserException $e) { + throw $this->error($e->getMessage()); + } - if ($parser->parseSelector($buffer, $newSelectors)) { + if ($isValid) { $selectors = array_map([$this, 'evalSelector'], $newSelectors); } } @@ -1466,14 +1849,15 @@ protected function evalSelector($selector) protected function evalSelectorPart($part) { foreach ($part as &$p) { - if (is_array($p) && ($p[0] === Type::T_INTERPOLATE || $p[0] === Type::T_STRING)) { + if (\is_array($p) && ($p[0] === Type::T_INTERPOLATE || $p[0] === Type::T_STRING)) { $p = $this->compileValue($p); - // force re-evaluation - if (strpos($p, '&') !== false || strpos($p, ',') !== false) { + // force re-evaluation if self char or non standard char + if (preg_match(',[^\w-],', $p)) { $this->shouldEvaluate = true; } - } elseif (is_string($p) && strlen($p) >= 2 && + } elseif ( + \is_string($p) && \strlen($p) >= 2 && ($first = $p[0]) && ($first === '"' || $first === "'") && substr($p, -1) === $first ) { @@ -1513,14 +1897,15 @@ function ($value, $key) use (&$compound) { ); if ($selectorFormat && $this->isImmediateRelationshipCombinator($compound)) { - if (count($output)) { - $output[count($output) - 1] .= ' ' . $compound; + if (\count($output)) { + $output[\count($output) - 1] .= ' ' . $compound; } else { $output[] = $compound; } + $glueNext = true; } elseif ($glueNext) { - $output[count($output) - 1] .= ' ' . $compound; + $output[\count($output) - 1] .= ' ' . $compound; $glueNext = false; } else { $output[] = $compound; @@ -1531,6 +1916,7 @@ function ($value, $key) use (&$compound) { foreach ($output as &$o) { $o = [Type::T_STRING, '', [$o]]; } + $output = [Type::T_LIST, ' ', $output]; } else { $output = implode(' ', $output); @@ -1555,14 +1941,18 @@ function ($value, $key) use (&$compound) { * * @return array */ - protected function revertSelfSelector($selectors) + protected function replaceSelfSelector($selectors, $replace = null) { foreach ($selectors as &$part) { - if (is_array($part)) { + if (\is_array($part)) { if ($part === [Type::T_SELF]) { - $part = '&'; + if (\is_null($replace)) { + $replace = $this->reduce([Type::T_SELF]); + $replace = $this->compileValue($replace); + } + $part = $replace; } else { - $part = $this->revertSelfSelector($part); + $part = $this->replaceSelfSelector($part, $replace); } } } @@ -1582,18 +1972,19 @@ protected function flattenSelectorSingle($single) $joined = []; foreach ($single as $part) { - if (empty($joined) || - ! is_string($part) || + if ( + empty($joined) || + ! \is_string($part) || preg_match('/[\[.:#%]/', $part) ) { $joined[] = $part; continue; } - if (is_array(end($joined))) { + if (\is_array(end($joined))) { $joined[] = $part; } else { - $joined[count($joined) - 1] .= $part; + $joined[\count($joined) - 1] .= $part; } } @@ -1609,7 +2000,7 @@ protected function flattenSelectorSingle($single) */ protected function compileSelector($selector) { - if (! is_array($selector)) { + if (! \is_array($selector)) { return $selector; // media and the like } @@ -1632,7 +2023,7 @@ protected function compileSelector($selector) protected function compileSelectorPart($piece) { foreach ($piece as &$p) { - if (! is_array($p)) { + if (! \is_array($p)) { continue; } @@ -1659,13 +2050,13 @@ protected function compileSelectorPart($piece) */ protected function hasSelectorPlaceholder($selector) { - if (! is_array($selector)) { + if (! \is_array($selector)) { return false; } foreach ($selector as $parts) { foreach ($parts as $part) { - if (strlen($part) && '%' === $part[0]) { + if (\strlen($part) && '%' === $part[0]) { return true; } } @@ -1674,6 +2065,11 @@ protected function hasSelectorPlaceholder($selector) return false; } + /** + * @param string $name + * + * @return void + */ protected function pushCallStack($name = '') { $this->callStack[] = [ @@ -1684,14 +2080,18 @@ protected function pushCallStack($name = '') ]; // infinite calling loop - if (count($this->callStack) > 25000) { + if (\count($this->callStack) > 25000) { // not displayed but you can var_dump it to deep debug $msg = $this->callStackMessage(true, 100); - $msg = "Infinite calling loop"; - $this->throwError($msg); + $msg = 'Infinite calling loop'; + + throw $this->error($msg); } } + /** + * @return void + */ protected function popCallStack() { array_pop($this->callStack); @@ -1714,6 +2114,8 @@ protected function compileChildren($stms, OutputBlock $out, $traceName = '') $ret = $this->compileChild($stm, $out); if (isset($ret)) { + $this->popCallStack(); + return $ret; } } @@ -1724,13 +2126,15 @@ protected function compileChildren($stms, OutputBlock $out, $traceName = '') } /** - * Compile children and throw exception if unexpected @return + * Compile children and throw exception if unexpected `@return` * * @param array $stms * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $out * @param \ScssPhp\ScssPhp\Block $selfParent * @param string $traceName * + * @return void + * * @throws \Exception */ protected function compileChildrenNoReturn($stms, OutputBlock $out, $selfParent = null, $traceName = '') @@ -1738,11 +2142,11 @@ protected function compileChildrenNoReturn($stms, OutputBlock $out, $selfParent $this->pushCallStack($traceName); foreach ($stms as $stm) { - if ($selfParent && isset($stm[1]) && is_object($stm[1]) && $stm[1] instanceof Block) { + if ($selfParent && isset($stm[1]) && \is_object($stm[1]) && $stm[1] instanceof Block) { $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']); @@ -1751,9 +2155,7 @@ protected function compileChildrenNoReturn($stms, OutputBlock $out, $selfParent } if (isset($ret)) { - $this->throwError('@return may only be used within a function'); - - return; + throw $this->error('@return may only be used within a function'); } } @@ -1762,7 +2164,7 @@ protected function compileChildrenNoReturn($stms, OutputBlock $out, $selfParent /** - * evaluate media query : compile internal value keeping the structure inchanged + * evaluate media query : compile internal value keeping the structure unchanged * * @param array $queryList * @@ -1771,16 +2173,20 @@ protected function compileChildrenNoReturn($stms, OutputBlock $out, $selfParent protected function evaluateMediaQuery($queryList) { static $parser = null; + $outQueryList = []; + foreach ($queryList as $kql => $query) { $shouldReparse = false; + foreach ($query as $kq => $q) { - for ($i = 1; $i < count($q); $i++) { + for ($i = 1; $i < \count($q); $i++) { $value = $this->compileValue($q[$i]); // the parser had no mean to know if media type or expression if it was an interpolation // so you need to reparse if the T_MEDIA_TYPE looks like anything else a media type - if ($q[0] == Type::T_MEDIA_TYPE && + if ( + $q[0] == Type::T_MEDIA_TYPE && (strpos($value, '(') !== false || strpos($value, ')') !== false || strpos($value, ':') !== false || @@ -1792,24 +2198,31 @@ protected function evaluateMediaQuery($queryList) $queryList[$kql][$kq][$i] = [Type::T_KEYWORD, $value]; } } + if ($shouldReparse) { - if (is_null($parser)) { + if (\is_null($parser)) { $parser = $this->parserFactory(__METHOD__); } + $queryString = $this->compileMediaQuery([$queryList[$kql]]); $queryString = reset($queryString); + if (strpos($queryString, '@media ') === 0) { $queryString = substr($queryString, 7); $queries = []; + if ($parser->parseMediaQueryList($queryString, $queries)) { $queries = $this->evaluateMediaQuery($queries[2]); - while (count($queries)) { + + while (\count($queries)) { $outQueryList[] = array_shift($queries); } + continue; } } } + $outQueryList[] = $queryList[$kql]; } @@ -1825,10 +2238,10 @@ protected function evaluateMediaQuery($queryList) */ protected function compileMediaQuery($queryList) { - $start = '@media '; + $start = '@media '; $default = trim($start); - $out = []; - $current = ""; + $out = []; + $current = ''; foreach ($queryList as $query) { $type = null; @@ -1846,16 +2259,17 @@ protected function compileMediaQuery($queryList) foreach ($query as $q) { switch ($q[0]) { case Type::T_MEDIA_TYPE: - $newType = array_map([$this, 'compileValue'], array_slice($q, 1)); + $newType = array_map([$this, 'compileValue'], \array_slice($q, 1)); + // combining not and anything else than media type is too risky and should be avoided if (! $mediaTypeOnly) { - if (in_array(Type::T_NOT, $newType) || ($type && in_array(Type::T_NOT, $type) )) { + if (\in_array(Type::T_NOT, $newType) || ($type && \in_array(Type::T_NOT, $type) )) { if ($type) { array_unshift($parts, implode(' ', array_filter($type))); } if (! empty($parts)) { - if (strlen($current)) { + if (\strlen($current)) { $current .= $this->formatter->tagSeparator; } @@ -1866,9 +2280,9 @@ protected function compileMediaQuery($queryList) $out[] = $start . $current; } - $current = ""; - $type = null; - $parts = []; + $current = ''; + $type = null; + $parts = []; } } @@ -1918,7 +2332,7 @@ protected function compileMediaQuery($queryList) } if (! empty($parts)) { - if (strlen($current)) { + if (\strlen($current)) { $current .= $this->formatter->tagSeparator; } @@ -2000,23 +2414,19 @@ protected function mergeMediaTypes($type1, $type2) return $type1; } - $m1 = ''; - $t1 = ''; - - if (count($type1) > 1) { - $m1= strtolower($type1[0]); - $t1= strtolower($type1[1]); + if (\count($type1) > 1) { + $m1 = strtolower($type1[0]); + $t1 = strtolower($type1[1]); } else { + $m1 = ''; $t1 = strtolower($type1[0]); } - $m2 = ''; - $t2 = ''; - - if (count($type2) > 1) { + if (\count($type2) > 1) { $m2 = strtolower($type2[0]); $t2 = strtolower($type2[1]); } else { + $m2 = ''; $t2 = strtolower($type2[0]); } @@ -2045,7 +2455,7 @@ protected function mergeMediaTypes($type1, $type2) } // t1 == t2, neither m1 nor m2 are "not" - return [empty($m1)? $m2 : $m1, $t1]; + return [empty($m1) ? $m2 : $m1, $t1]; } /** @@ -2062,8 +2472,8 @@ protected function compileImport($rawPath, OutputBlock $out, $once = false) if ($rawPath[0] === Type::T_STRING) { $path = $this->compileStringContent($rawPath); - if ($path = $this->findImport($path)) { - if (! $once || ! in_array($path, $this->importedFiles)) { + if (strpos($path, 'url(') !== 0 && $path = $this->findImport($path)) { + if (! $once || ! \in_array($path, $this->importedFiles)) { $this->importFile($path, $out); $this->importedFiles[] = $path; } @@ -2071,20 +2481,21 @@ protected function compileImport($rawPath, OutputBlock $out, $once = false) return true; } - $this->appendRootDirective('@import ' . $this->compileValue($rawPath). ';', $out); + $this->appendRootDirective('@import ' . $this->compileImportPath($rawPath) . ';', $out); return false; } if ($rawPath[0] === Type::T_LIST) { // handle a list of strings - if (count($rawPath[2]) === 0) { + if (\count($rawPath[2]) === 0) { return false; } foreach ($rawPath[2] as $path) { if ($path[0] !== Type::T_STRING) { - $this->appendRootDirective('@import ' . $this->compileValue($rawPath) . ';', $out); + $this->appendRootDirective('@import ' . $this->compileImportPath($rawPath) . ';', $out); + return false; } } @@ -2096,19 +2507,68 @@ protected function compileImport($rawPath, OutputBlock $out, $once = false) return true; } - $this->appendRootDirective('@import ' . $this->compileValue($rawPath) . ';', $out); + $this->appendRootDirective('@import ' . $this->compileImportPath($rawPath) . ';', $out); return false; } + /** + * @param array $rawPath + * @return string + * @throws CompilerException + */ + protected function compileImportPath($rawPath) + { + $path = $this->compileValue($rawPath); + + // case url() without quotes : suppress \r \n remaining in the path + // if this is a real string there can not be CR or LF char + if (strpos($path, 'url(') === 0) { + $path = str_replace(array("\r", "\n"), array('', ' '), $path); + } else { + // if this is a file name in a string, spaces should be escaped + $path = $this->reduce($rawPath); + $path = $this->escapeImportPathString($path); + $path = $this->compileValue($path); + } + + return $path; + } + + /** + * @param array $path + * @return array + * @throws CompilerException + */ + protected function escapeImportPathString($path) + { + switch ($path[0]) { + case Type::T_LIST: + foreach ($path[2] as $k => $v) { + $path[2][$k] = $this->escapeImportPathString($v); + } + break; + case Type::T_STRING: + if ($path[1]) { + $path = $this->compileValue($path); + $path = str_replace(' ', '\\ ', $path); + $path = [Type::T_KEYWORD, $path]; + } + break; + } + + return $path; + } /** * Append a root directive like @import or @charset as near as the possible from the source code * (keeping before comments, @import and @charset coming before in the source code) * - * @param string $line - * @param @param \ScssPhp\ScssPhp\Formatter\OutputBlock $out - * @param array $allowed + * @param string $line + * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $out + * @param array $allowed + * + * @return void */ protected function appendRootDirective($line, $out, $allowed = [Type::T_COMMENT]) { @@ -2120,8 +2580,8 @@ protected function appendRootDirective($line, $out, $allowed = [Type::T_COMMENT] $i = 0; - while ($i < count($root->children)) { - if (! isset($root->children[$i]->type) || ! in_array($root->children[$i]->type, $allowed)) { + while ($i < \count($root->children)) { + if (! isset($root->children[$i]->type) || ! \in_array($root->children[$i]->type, $allowed)) { break; } @@ -2131,59 +2591,53 @@ protected function appendRootDirective($line, $out, $allowed = [Type::T_COMMENT] // remove incompatible children from the bottom of the list $saveChildren = []; - while ($i < count($root->children)) { + while ($i < \count($root->children)) { $saveChildren[] = array_pop($root->children); } // insert the directive as a comment $child = $this->makeOutputBlock(Type::T_COMMENT); - $child->lines[] = $line; - $child->sourceName = $this->sourceNames[$this->sourceIndex]; - $child->sourceLine = $this->sourceLine; + $child->lines[] = $line; + $child->sourceName = $this->sourceNames[$this->sourceIndex]; + $child->sourceLine = $this->sourceLine; $child->sourceColumn = $this->sourceColumn; $root->children[] = $child; // repush children - while (count($saveChildren)) { + while (\count($saveChildren)) { $root->children[] = array_pop($saveChildren); } } /** - * Append lines to the courrent output block: + * Append lines to the current output block: * directly to the block or through a child if necessary * * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $out * @param string $type - * @param string $line + * @param string|mixed $line + * + * @return void */ protected function appendOutputLine(OutputBlock $out, $type, $line) { $outWrite = &$out; - if ($type === Type::T_COMMENT) { - $parent = $out->parent; - - if (end($parent->children) !== $out) { - $outWrite = &$parent->children[count($parent->children)-1]; - } - - if (!is_string($line)) { - $line = $this->compileValue($line); - } - } - // check if it's a flat output or not - if (count($out->children)) { - $lastChild = &$out->children[count($out->children) -1]; + if (\count($out->children)) { + $lastChild = &$out->children[\count($out->children) - 1]; - if ($lastChild->depth === $out->depth && is_null($lastChild->selectors) && ! count($lastChild->children)) { + if ( + $lastChild->depth === $out->depth && + \is_null($lastChild->selectors) && + ! \count($lastChild->children) + ) { $outWrite = $lastChild; } else { $nextLines = $this->makeOutputBlock($type); $nextLines->parent = $out; - $nextLines->depth = $out->depth; + $nextLines->depth = $out->depth; $out->children[] = $nextLines; $outWrite = &$nextLines; @@ -2199,21 +2653,22 @@ protected function appendOutputLine(OutputBlock $out, $type, $line) * @param array $child * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $out * - * @return array + * @return array|Number|null */ protected function compileChild($child, OutputBlock $out) { if (isset($child[Parser::SOURCE_LINE])) { - $this->sourceIndex = isset($child[Parser::SOURCE_INDEX]) ? $child[Parser::SOURCE_INDEX] : null; - $this->sourceLine = isset($child[Parser::SOURCE_LINE]) ? $child[Parser::SOURCE_LINE] : -1; + $this->sourceIndex = isset($child[Parser::SOURCE_INDEX]) ? $child[Parser::SOURCE_INDEX] : null; + $this->sourceLine = isset($child[Parser::SOURCE_LINE]) ? $child[Parser::SOURCE_LINE] : -1; $this->sourceColumn = isset($child[Parser::SOURCE_COLUMN]) ? $child[Parser::SOURCE_COLUMN] : -1; - } elseif (is_array($child) && isset($child[1]->sourceLine)) { - $this->sourceIndex = $child[1]->sourceIndex; - $this->sourceLine = $child[1]->sourceLine; + } elseif (\is_array($child) && isset($child[1]->sourceLine)) { + $this->sourceIndex = $child[1]->sourceIndex; + $this->sourceLine = $child[1]->sourceLine; $this->sourceColumn = $child[1]->sourceColumn; } elseif (! empty($out->sourceLine) && ! empty($out->sourceName)) { - $this->sourceLine = $out->sourceLine; - $this->sourceIndex = array_search($out->sourceName, $this->sourceNames); + $this->sourceLine = $out->sourceLine; + $this->sourceIndex = array_search($out->sourceName, $this->sourceNames); + $this->sourceColumn = $out->sourceColumn; if ($this->sourceIndex === false) { $this->sourceIndex = null; @@ -2234,7 +2689,7 @@ protected function compileChild($child, OutputBlock $out) break; case Type::T_DIRECTIVE: - $this->compileDirective($child[1]); + $this->compileDirective($child[1], $out); break; case Type::T_AT_ROOT: @@ -2256,13 +2711,37 @@ protected function compileChild($child, OutputBlock $out) } break; + case Type::T_CUSTOM_PROPERTY: + list(, $name, $value) = $child; + $compiledName = $this->compileValue($name); + + // if the value reduces to null from something else then + // the property should be discarded + if ($value[0] !== Type::T_NULL) { + $value = $this->reduce($value); + + if ($value[0] === Type::T_NULL || $value === static::$nullString) { + break; + } + } + + $compiledValue = $this->compileValue($value); + + $line = $this->formatter->customProperty( + $compiledName, + $compiledValue + ); + + $this->appendOutputLine($out, Type::T_ASSIGN, $line); + break; + case Type::T_ASSIGN: list(, $name, $value) = $child; if ($name[0] === Type::T_VARIABLE) { - $flags = isset($child[3]) ? $child[3] : []; - $isDefault = in_array('!default', $flags); - $isGlobal = in_array('!global', $flags); + $flags = isset($child[3]) ? $child[3] : []; + $isDefault = \in_array('!default', $flags); + $isGlobal = \in_array('!global', $flags); if ($isGlobal) { $this->set($name[1], $this->reduce($value), false, $this->rootEnv, $value); @@ -2270,7 +2749,7 @@ protected function compileChild($child, OutputBlock $out) } $shouldSet = $isDefault && - (($result = $this->get($name[1], false)) === null || + (\is_null($result = $this->get($name[1], false)) || $result === static::$null); if (! $isDefault || $shouldSet) { @@ -2281,28 +2760,78 @@ protected function compileChild($child, OutputBlock $out) $compiledName = $this->compileValue($name); - // handle shorthand syntax: size / line-height - if ($compiledName === 'font' || $compiledName === 'grid-row' || $compiledName === 'grid-column') { + // handle shorthand syntaxes : size / line-height... + if (\in_array($compiledName, ['font', 'grid-row', 'grid-column', 'border-radius'])) { if ($value[0] === Type::T_VARIABLE) { // if the font value comes from variable, the content is already reduced // (i.e., formulas were already calculated), so we need the original unreduced value $value = $this->get($value[1], true, null, true); } - $fontValue=&$value; + $shorthandValue=&$value; + + $shorthandDividerNeedsUnit = false; + $maxListElements = null; + $maxShorthandDividers = 1; + + switch ($compiledName) { + case 'border-radius': + $maxListElements = 4; + $shorthandDividerNeedsUnit = true; + break; + } - if ($value[0] === Type::T_LIST && $value[1]==',') { + if ($compiledName === 'font' && $value[0] === Type::T_LIST && $value[1] === ',') { // this is the case if more than one font is given: example: "font: 400 1em/1.3 arial,helvetica" // we need to handle the first list element - $fontValue=&$value[2][0]; + $shorthandValue=&$value[2][0]; } - if ($fontValue[0] === Type::T_EXPRESSION && $fontValue[1] === '/') { - $fontValue = $this->expToString($fontValue); - } elseif ($fontValue[0] === Type::T_LIST) { - foreach ($fontValue[2] as &$item) { + if ($shorthandValue[0] === Type::T_EXPRESSION && $shorthandValue[1] === '/') { + $revert = true; + + if ($shorthandDividerNeedsUnit) { + $divider = $shorthandValue[3]; + + if (\is_array($divider)) { + $divider = $this->reduce($divider, true); + } + + if ($divider instanceof Number && \intval($divider->getDimension()) && $divider->unitless()) { + $revert = false; + } + } + + if ($revert) { + $shorthandValue = $this->expToString($shorthandValue); + } + } elseif ($shorthandValue[0] === Type::T_LIST) { + foreach ($shorthandValue[2] as &$item) { if ($item[0] === Type::T_EXPRESSION && $item[1] === '/') { - $item = $this->expToString($item); + if ($maxShorthandDividers > 0) { + $revert = true; + + // if the list of values is too long, this has to be a shorthand, + // otherwise it could be a real division + if (\is_null($maxListElements) || \count($shorthandValue[2]) <= $maxListElements) { + if ($shorthandDividerNeedsUnit) { + $divider = $item[3]; + + if (\is_array($divider)) { + $divider = $this->reduce($divider, true); + } + + if ($divider instanceof Number && \intval($divider->getDimension()) && $divider->unitless()) { + $revert = false; + } + } + } + + if ($revert) { + $item = $this->expToString($item); + $maxShorthandDividers--; + } + } } } } @@ -2320,11 +2849,14 @@ protected function compileChild($child, OutputBlock $out) $compiledValue = $this->compileValue($value); - $line = $this->formatter->property( - $compiledName, - $compiledValue - ); - $this->appendOutputLine($out, Type::T_ASSIGN, $line); + // ignore empty value + if (\strlen($compiledValue)) { + $line = $this->formatter->property( + $compiledName, + $compiledValue + ); + $this->appendOutputLine($out, Type::T_ASSIGN, $line); + } break; case Type::T_COMMENT: @@ -2333,27 +2865,32 @@ protected function compileChild($child, OutputBlock $out) break; } - $this->appendOutputLine($out, Type::T_COMMENT, $child[1]); + $line = $this->compileCommentValue($child, true); + $this->appendOutputLine($out, Type::T_COMMENT, $line); break; case Type::T_MIXIN: case Type::T_FUNCTION: list(, $block) = $child; - + // the block need to be able to go up to it's parent env to resolve vars + $block->parentEnv = $this->getStoreEnv(); $this->set(static::$namespaces[$block->type] . $block->name, $block, true); break; case Type::T_EXTEND: foreach ($child[1] as $sel) { + $sel = $this->replaceSelfSelector($sel); $results = $this->evalSelectors([$sel]); foreach ($results as $result) { // only use the first one $result = current($result); $selectors = $out->selectors; - if (!$selectors && isset($child['selfParent'])) { + + if (! $selectors && isset($child['selfParent'])) { $selectors = $this->multiplySelectors($this->env, $child['selfParent']); } + $this->pushExtends($result, $selectors, $child); } } @@ -2367,7 +2904,8 @@ protected function compileChild($child, OutputBlock $out) } foreach ($if->cases as $case) { - if ($case->type === Type::T_ELSE || + if ( + $case->type === Type::T_ELSE || $case->type === Type::T_ELSEIF && $this->isTruthy($this->reduce($case->cond)) ) { return $this->compileChildren($case->children, $out); @@ -2378,12 +2916,12 @@ protected function compileChild($child, OutputBlock $out) case Type::T_EACH: list(, $each) = $child; - $list = $this->coerceList($this->reduce($each->list)); + $list = $this->coerceList($this->reduce($each->list), ',', true); $this->pushEnv(); foreach ($list[2] as $item) { - if (count($each->vars) === 1) { + if (\count($each->vars) === 1) { $this->set($each->vars[0], $item, true); } else { list(,, $values) = $this->coerceList($item); @@ -2396,19 +2934,17 @@ protected function compileChild($child, OutputBlock $out) $ret = $this->compileChildren($each->children, $out); if ($ret) { - if ($ret[0] !== Type::T_CONTROL) { - $this->popEnv(); - - return $ret; - } + $store = $this->env->store; + $this->popEnv(); + $this->backPropagateEnv($store, $each->vars); - if ($ret[1]) { - break; - } + return $ret; } } - + $store = $this->env->store; $this->popEnv(); + $this->backPropagateEnv($store, $each->vars); + break; case Type::T_WHILE: @@ -2418,13 +2954,7 @@ protected function compileChild($child, OutputBlock $out) $ret = $this->compileChildren($while->children, $out); if ($ret) { - if ($ret[0] !== Type::T_CONTROL) { - return $ret; - } - - if ($ret[1]) { - break; - } + return $ret; } } break; @@ -2435,47 +2965,53 @@ protected function compileChild($child, OutputBlock $out) $start = $this->reduce($for->start, true); $end = $this->reduce($for->end, true); - if (! ($start[2] == $end[2] || $end->unitless())) { - $this->throwError('Incompatible units: "%s" and "%s".', $start->unitStr(), $end->unitStr()); + if (! $start instanceof Number) { + throw $this->error('%s is not a number', $start[0]); + } - break; + if (! $end instanceof Number) { + throw $this->error('%s is not a number', $end[0]); } - $unit = $start[2]; - $start = $start[1]; - $end = $end[1]; + $start->assertSameUnitOrUnitless($end); + + $numeratorUnits = $start->getNumeratorUnits(); + $denominatorUnits = $start->getDenominatorUnits(); + + $start = $start->getDimension(); + $end = $end->getDimension(); $d = $start < $end ? 1 : -1; + $this->pushEnv(); + for (;;) { - if ((! $for->until && $start - $d == $end) || + if ( + (! $for->until && $start - $d == $end) || ($for->until && $start == $end) ) { break; } - $this->set($for->var, new Node\Number($start, $unit)); + $this->set($for->var, new Number($start, $numeratorUnits, $denominatorUnits)); $start += $d; $ret = $this->compileChildren($for->children, $out); if ($ret) { - if ($ret[0] !== Type::T_CONTROL) { - return $ret; - } + $store = $this->env->store; + $this->popEnv(); + $this->backPropagateEnv($store, [$for->var]); - if ($ret[1]) { - break; - } + return $ret; } } - break; - case Type::T_BREAK: - return [Type::T_CONTROL, true]; + $store = $this->env->store; + $this->popEnv(); + $this->backPropagateEnv($store, [$for->var]); - case Type::T_CONTINUE: - return [Type::T_CONTROL, false]; + break; case Type::T_RETURN: return $this->reduce($child[1], true); @@ -2491,8 +3027,7 @@ protected function compileChild($child, OutputBlock $out) $mixin = $this->get(static::$namespaces['mixin'] . $name, false); if (! $mixin) { - $this->throwError("Undefined mixin $name"); - break; + throw $this->error("Undefined mixin $name"); } $callingScope = $this->getStoreEnv(); @@ -2501,9 +3036,6 @@ protected function compileChild($child, OutputBlock $out) $this->pushEnv(); $this->env->depth--; - $storeEnv = $this->storeEnv; - $this->storeEnv = $this->env; - // Find the parent selectors in the env to be able to know what '&' refers to in the mixin // and assign this fake parent to childs $selfParent = null; @@ -2518,7 +3050,7 @@ protected function compileChild($child, OutputBlock $out) $parent->selectors = $parentSelectors; foreach ($mixin->children as $k => $child) { - if (isset($child[1]) && is_object($child[1]) && $child[1] instanceof Block) { + if (isset($child[1]) && \is_object($child[1]) && $child[1] instanceof Block) { $mixin->children[$k][1]->parent = $parent; } } @@ -2549,37 +3081,39 @@ protected function compileChild($child, OutputBlock $out) $this->env->marker = 'mixin'; - $this->compileChildrenNoReturn($mixin->children, $out, $selfParent, $this->env->marker . " " . $name); + if (! empty($mixin->parentEnv)) { + $this->env->declarationScopeParent = $mixin->parentEnv; + } else { + throw $this->error("@mixin $name() without parentEnv"); + } - $this->storeEnv = $storeEnv; + $this->compileChildrenNoReturn($mixin->children, $out, $selfParent, $this->env->marker . ' ' . $name); $this->popEnv(); break; case Type::T_MIXIN_CONTENT: - $env = isset($this->storeEnv) ? $this->storeEnv : $this->env; - $content = $this->get(static::$namespaces['special'] . 'content', false, $env); - $argUsing = $this->get(static::$namespaces['special'] . 'using', false, $env); + $env = isset($this->storeEnv) ? $this->storeEnv : $this->env; + $content = $this->get(static::$namespaces['special'] . 'content', false, $env); + $argUsing = $this->get(static::$namespaces['special'] . 'using', false, $env); $argContent = $child[1]; if (! $content) { - $content = new \stdClass(); - $content->scope = new \stdClass(); - $content->children = $env->parent->block->children; break; } $storeEnv = $this->storeEnv; - $varsUsing = []; + if (isset($argUsing) && isset($argContent)) { // Get the arguments provided for the content with the names provided in the "using" argument list - $this->storeEnv = $this->env; + $this->storeEnv = null; $varsUsing = $this->applyArguments($argUsing, $argContent, false); } // restore the scope from the @content $this->storeEnv = $content->scope; + // append the vars from using if any foreach ($varsUsing as $name => $val) { $this->set($name, $val, true, $this->storeEnv); @@ -2593,36 +3127,34 @@ protected function compileChild($child, OutputBlock $out) case Type::T_DEBUG: list(, $value) = $child; - $fname = $this->sourceNames[$this->sourceIndex]; - $line = $this->sourceLine; - $value = $this->compileValue($this->reduce($value, true)); - fwrite($this->stderr, "File $fname on line $line DEBUG: $value\n"); + $fname = $this->getPrettyPath($this->sourceNames[$this->sourceIndex]); + $line = $this->sourceLine; + $value = $this->compileDebugValue($value); + + fwrite($this->stderr, "$fname:$line DEBUG: $value\n"); break; case Type::T_WARN: list(, $value) = $child; - $fname = $this->sourceNames[$this->sourceIndex]; - $line = $this->sourceLine; - $value = $this->compileValue($this->reduce($value, true)); - fwrite($this->stderr, "File $fname on line $line WARN: $value\n"); + $fname = $this->getPrettyPath($this->sourceNames[$this->sourceIndex]); + $line = $this->sourceLine; + $value = $this->compileDebugValue($value); + + fwrite($this->stderr, "WARNING: $value\n on line $line of $fname\n\n"); break; case Type::T_ERROR: list(, $value) = $child; - $fname = $this->sourceNames[$this->sourceIndex]; - $line = $this->sourceLine; + $fname = $this->getPrettyPath($this->sourceNames[$this->sourceIndex]); + $line = $this->sourceLine; $value = $this->compileValue($this->reduce($value, true)); - $this->throwError("File $fname on line $line ERROR: $value\n"); - break; - case Type::T_CONTROL: - $this->throwError('@break/@continue not permitted in this scope'); - break; + throw $this->error("File $fname on line $line ERROR: $value\n"); default: - $this->throwError("unknown child type: $child[0]"); + throw $this->error("unknown child type: $child[0]"); } } @@ -2630,14 +3162,21 @@ protected function compileChild($child, OutputBlock $out) * Reduce expression to string * * @param array $exp + * @param bool $keepParens * * @return array */ - protected function expToString($exp) + protected function expToString($exp, $keepParens = false) { - list(, $op, $left, $right, /* $inParens */, $whiteLeft, $whiteRight) = $exp; + list(, $op, $left, $right, $inParens, $whiteLeft, $whiteRight) = $exp; + + $content = []; - $content = [$this->reduce($left)]; + if ($keepParens && $inParens) { + $content[] = '('; + } + + $content[] = $this->reduce($left); if ($whiteLeft) { $content[] = ' '; @@ -2651,13 +3190,17 @@ protected function expToString($exp) $content[] = $this->reduce($right); + if ($keepParens && $inParens) { + $content[] = ')'; + } + return [Type::T_STRING, '', $content]; } /** * Is truthy? * - * @param array $value + * @param array|Number $value * * @return boolean */ @@ -2705,13 +3248,17 @@ protected function shouldEval($value) /** * Reduce value * - * @param array $value + * @param array|Number $value * @param boolean $inExp * - * @return array|\ScssPhp\ScssPhp\Node\Number + * @return null|string|array|Number */ protected function reduce($value, $inExp = false) { + if (\is_null($value)) { + return null; + } + switch ($value[0]) { case Type::T_EXPRESSION: list(, $op, $left, $right, $inParens) = $value; @@ -2726,16 +3273,16 @@ protected function reduce($value, $inExp = false) } // special case: looks like css shorthand - if ($opName == 'div' && ! $inParens && ! $inExp && isset($right[2]) && - (($right[0] !== Type::T_NUMBER && $right[2] != '') || + if ( + $opName == 'div' && ! $inParens && ! $inExp && + (($right[0] !== Type::T_NUMBER && isset($right[2]) && $right[2] != '') || ($right[0] === Type::T_NUMBER && ! $right->unitless())) ) { return $this->expToString($value); } - $left = $this->coerceForExpression($left); + $left = $this->coerceForExpression($left); $right = $this->coerceForExpression($right); - $ltype = $left[0]; $rtype = $right[0]; @@ -2749,52 +3296,15 @@ protected function reduce($value, $inExp = false) // 3. op[op name] $fn = "op${ucOpName}${ucLType}${ucRType}"; - if (is_callable([$this, $fn]) || + if ( + \is_callable([$this, $fn]) || (($fn = "op${ucLType}${ucRType}") && - is_callable([$this, $fn]) && + \is_callable([$this, $fn]) && $passOp = true) || (($fn = "op${ucOpName}") && - is_callable([$this, $fn]) && + \is_callable([$this, $fn]) && $genOp = true) ) { - $coerceUnit = false; - - if (! isset($genOp) && - $left[0] === Type::T_NUMBER && $right[0] === Type::T_NUMBER - ) { - $coerceUnit = true; - - switch ($opName) { - case 'mul': - $targetUnit = $left[2]; - - foreach ($right[2] as $unit => $exp) { - $targetUnit[$unit] = (isset($targetUnit[$unit]) ? $targetUnit[$unit] : 0) + $exp; - } - break; - - case 'div': - $targetUnit = $left[2]; - - foreach ($right[2] as $unit => $exp) { - $targetUnit[$unit] = (isset($targetUnit[$unit]) ? $targetUnit[$unit] : 0) - $exp; - } - break; - - case 'mod': - $targetUnit = $left[2]; - break; - - default: - $targetUnit = $left->unitless() ? $right[2] : $left[2]; - } - - if (! $left->unitless() && ! $right->unitless()) { - $left = $left->normalize(); - $right = $right->normalize(); - } - } - $shouldEval = $inParens || $inExp; if (isset($passOp)) { @@ -2804,10 +3314,6 @@ protected function reduce($value, $inExp = false) } if (isset($out)) { - if ($coerceUnit && $out[0] === Type::T_NUMBER) { - $out = $out->coerce($targetUnit); - } - return $out; } } @@ -2820,13 +3326,13 @@ protected function reduce($value, $inExp = false) $inExp = $inExp || $this->shouldEval($exp); $exp = $this->reduce($exp); - if ($exp[0] === Type::T_NUMBER) { + if ($exp instanceof Number) { switch ($op) { case '+': - return new Node\Number($exp[1], $exp[2]); + return $exp; case '-': - return new Node\Number(-$exp[1], $exp[2]); + return $exp->unaryMinus(); } } @@ -2867,7 +3373,7 @@ protected function reduce($value, $inExp = false) case Type::T_STRING: foreach ($value[2] as &$item) { - if (is_array($item) || $item instanceof \ArrayAccess) { + if (\is_array($item) || $item instanceof \ArrayAccess) { $item = $this->reduce($item); } } @@ -2876,6 +3382,7 @@ protected function reduce($value, $inExp = false) case Type::T_INTERPOLATE: $value[1] = $this->reduce($value[1]); + if ($inExp) { return $value[1]; } @@ -2886,8 +3393,10 @@ protected function reduce($value, $inExp = false) return $this->fncall($value[1], $value[2]); case Type::T_SELF: - $selfSelector = $this->multiplySelectors($this->env); + $selfParent = ! empty($this->env->block->selfParent) ? $this->env->block->selfParent : null; + $selfSelector = $this->multiplySelectors($this->env, $selfParent); $selfSelector = $this->collapseSelectors($selfSelector, true); + return $selfSelector; default: @@ -2901,74 +3410,280 @@ protected function reduce($value, $inExp = false) * @param string $name * @param array $argValues * - * @return array|null + * @return array|Number */ - protected function fncall($name, $argValues) + protected function fncall($functionReference, $argValues) { - // SCSS @function - if ($this->callScssFunction($name, $argValues, $returnValue)) { - return $returnValue; - } + // a string means this is a static hard reference coming from the parsing + if (is_string($functionReference)) { + $name = $functionReference; - // native PHP functions - if ($this->callNativeFunction($name, $argValues, $returnValue)) { - return $returnValue; + $functionReference = $this->getFunctionReference($name); + if ($functionReference === static::$null || $functionReference[0] !== Type::T_FUNCTION_REFERENCE) { + $functionReference = [Type::T_FUNCTION, $name, [Type::T_LIST, ',', []]]; + } } - // for CSS functions, simply flatten the arguments into a list - $listArgs = []; + // a function type means we just want a plain css function call + if ($functionReference[0] === Type::T_FUNCTION) { + // for CSS functions, simply flatten the arguments into a list + $listArgs = []; - foreach ((array) $argValues as $arg) { - if (empty($arg[0])) { - $listArgs[] = $this->reduce($arg[1]); + foreach ((array) $argValues as $arg) { + if (empty($arg[0]) || count($argValues) === 1) { + $listArgs[] = $this->reduce($this->stringifyFncallArgs($arg[1])); + } } + + return [Type::T_FUNCTION, $functionReference[1], [Type::T_LIST, ',', $listArgs]]; } - return [Type::T_FUNCTION, $name, [Type::T_LIST, ',', $listArgs]]; - } + if ($functionReference === static::$null || $functionReference[0] !== Type::T_FUNCTION_REFERENCE) { + return static::$defaultValue; + } - /** - * Normalize name - * - * @param string $name - * - * @return string - */ - protected function normalizeName($name) - { - return str_replace('-', '_', $name); - } - /** - * Normalize value - * - * @param array $value - * - * @return array - */ - public function normalizeValue($value) - { - $value = $this->coerceForExpression($this->reduce($value)); + switch ($functionReference[1]) { + // SCSS @function + case 'scss': + return $this->callScssFunction($functionReference[3], $argValues); - switch ($value[0]) { - case Type::T_LIST: - $value = $this->extractInterpolation($value); + // native PHP functions + case 'user': + case 'native': + list(,,$name, $fn, $prototype) = $functionReference; - if ($value[0] !== Type::T_LIST) { - return [Type::T_KEYWORD, $this->compileValue($value)]; + // special cases of css valid functions min/max + $name = strtolower($name); + if (\in_array($name, ['min', 'max'])) { + $cssFunction = $this->cssValidArg( + [Type::T_FUNCTION_CALL, $name, $argValues], + ['min', 'max', 'calc', 'env', 'var'] + ); + if ($cssFunction !== false) { + return $cssFunction; + } } + $returnValue = $this->callNativeFunction($name, $fn, $prototype, $argValues); - foreach ($value[2] as $key => $item) { - $value[2][$key] = $this->normalizeValue($item); + if (! isset($returnValue)) { + return $this->fncall([Type::T_FUNCTION, $name, [Type::T_LIST, ',', []]], $argValues); } - return $value; - - case Type::T_STRING: - return [$value[0], '"', [$this->compileStringContent($value)]]; + return $returnValue; + + default: + return static::$defaultValue; + } + } + + protected function cssValidArg($arg, $allowed_function = [], $inFunction = false) + { + switch ($arg[0]) { + case Type::T_INTERPOLATE: + return [Type::T_KEYWORD, $this->CompileValue($arg)]; + + case Type::T_FUNCTION: + if (! \in_array($arg[1], $allowed_function)) { + return false; + } + if ($arg[2][0] === Type::T_LIST) { + foreach ($arg[2][2] as $k => $subarg) { + $arg[2][2][$k] = $this->cssValidArg($subarg, $allowed_function, $arg[1]); + if ($arg[2][2][$k] === false) { + return false; + } + } + } + return $arg; + + case Type::T_FUNCTION_CALL: + if (! \in_array($arg[1], $allowed_function)) { + return false; + } + $cssArgs = []; + foreach ($arg[2] as $argValue) { + if ($argValue === static::$null) { + return false; + } + $cssArg = $this->cssValidArg($argValue[1], $allowed_function, $arg[1]); + if (empty($argValue[0]) && $cssArg !== false) { + $cssArgs[] = [$argValue[0], $cssArg]; + } else { + return false; + } + } + + return $this->fncall([Type::T_FUNCTION, $arg[1], [Type::T_LIST, ',', []]], $cssArgs); + + case Type::T_STRING: + case Type::T_KEYWORD: + if (!$inFunction or !\in_array($inFunction, ['calc', 'env', 'var'])) { + return false; + } + return $this->stringifyFncallArgs($arg); case Type::T_NUMBER: - return $value->normalize(); + return $this->stringifyFncallArgs($arg); + + case Type::T_LIST: + if (!$inFunction) { + return false; + } + if (empty($arg['enclosing']) and $arg[1] === '') { + foreach ($arg[2] as $k => $subarg) { + $arg[2][$k] = $this->cssValidArg($subarg, $allowed_function, $inFunction); + if ($arg[2][$k] === false) { + return false; + } + } + $arg[0] = Type::T_STRING; + return $arg; + } + return false; + + case Type::T_EXPRESSION: + if (! \in_array($arg[1], ['+', '-', '/', '*'])) { + return false; + } + $arg[2] = $this->cssValidArg($arg[2], $allowed_function, $inFunction); + $arg[3] = $this->cssValidArg($arg[3], $allowed_function, $inFunction); + if ($arg[2] === false || $arg[3] === false) { + return false; + } + return $this->expToString($arg, true); + + case Type::T_VARIABLE: + case Type::T_SELF: + default: + return false; + } + } + + + /** + * Reformat fncall arguments to proper css function output + * + * @param $arg + * + * @return array|\ArrayAccess|Number|string|null + */ + protected function stringifyFncallArgs($arg) + { + + switch ($arg[0]) { + case Type::T_LIST: + foreach ($arg[2] as $k => $v) { + $arg[2][$k] = $this->stringifyFncallArgs($v); + } + break; + + case Type::T_EXPRESSION: + if ($arg[1] === '/') { + $arg[2] = $this->stringifyFncallArgs($arg[2]); + $arg[3] = $this->stringifyFncallArgs($arg[3]); + $arg[5] = $arg[6] = false; // no space around / + $arg = $this->expToString($arg); + } + break; + + case Type::T_FUNCTION_CALL: + $name = strtolower($arg[1]); + + if (in_array($name, ['max', 'min', 'calc'])) { + $args = $arg[2]; + $arg = $this->fncall([Type::T_FUNCTION, $name, [Type::T_LIST, ',', []]], $args); + } + break; + } + + return $arg; + } + + /** + * Find a function reference + * @param string $name + * @param bool $safeCopy + * @return array + */ + protected function getFunctionReference($name, $safeCopy = false) + { + // SCSS @function + if ($func = $this->get(static::$namespaces['function'] . $name, false)) { + if ($safeCopy) { + $func = clone $func; + } + + return [Type::T_FUNCTION_REFERENCE, 'scss', $name, $func]; + } + + // native PHP functions + + // try to find a native lib function + $normalizedName = $this->normalizeName($name); + $libName = null; + + if (isset($this->userFunctions[$normalizedName])) { + // see if we can find a user function + list($f, $prototype) = $this->userFunctions[$normalizedName]; + + return [Type::T_FUNCTION_REFERENCE, 'user', $name, $f, $prototype]; + } + + if (($f = $this->getBuiltinFunction($normalizedName)) && \is_callable($f)) { + $libName = $f[1]; + $prototype = isset(static::$$libName) ? static::$$libName : null; + + return [Type::T_FUNCTION_REFERENCE, 'native', $name, $f, $prototype]; + } + + return static::$null; + } + + + /** + * Normalize name + * + * @param string $name + * + * @return string + */ + protected function normalizeName($name) + { + return str_replace('-', '_', $name); + } + + /** + * Normalize value + * + * @param array|Number $value + * + * @return array|Number + */ + public function normalizeValue($value) + { + $value = $this->coerceForExpression($this->reduce($value)); + + switch ($value[0]) { + case Type::T_LIST: + $value = $this->extractInterpolation($value); + + if ($value[0] !== Type::T_LIST) { + return [Type::T_KEYWORD, $this->compileValue($value)]; + } + + foreach ($value[2] as $key => $item) { + $value[2][$key] = $this->normalizeValue($item); + } + + if (! empty($value['enclosing'])) { + unset($value['enclosing']); + } + + return $value; + + case Type::T_STRING: + return [$value[0], '"', [$this->compileStringContent($value)]]; case Type::T_INTERPOLATE: return [Type::T_KEYWORD, $this->compileValue($value)]; @@ -2981,70 +3696,66 @@ public function normalizeValue($value) /** * Add numbers * - * @param array $left - * @param array $right + * @param Number $left + * @param Number $right * - * @return \ScssPhp\ScssPhp\Node\Number + * @return Number */ - protected function opAddNumberNumber($left, $right) + protected function opAddNumberNumber(Number $left, Number $right) { - return new Node\Number($left[1] + $right[1], $left[2]); + return $left->plus($right); } /** * Multiply numbers * - * @param array $left - * @param array $right + * @param Number $left + * @param Number $right * - * @return \ScssPhp\ScssPhp\Node\Number + * @return Number */ - protected function opMulNumberNumber($left, $right) + protected function opMulNumberNumber(Number $left, Number $right) { - return new Node\Number($left[1] * $right[1], $left[2]); + return $left->times($right); } /** * Subtract numbers * - * @param array $left - * @param array $right + * @param Number $left + * @param Number $right * - * @return \ScssPhp\ScssPhp\Node\Number + * @return Number */ - protected function opSubNumberNumber($left, $right) + protected function opSubNumberNumber(Number $left, Number $right) { - return new Node\Number($left[1] - $right[1], $left[2]); + return $left->minus($right); } /** * Divide numbers * - * @param array $left - * @param array $right + * @param Number $left + * @param Number $right * - * @return array|\ScssPhp\ScssPhp\Node\Number + * @return Number */ - protected function opDivNumberNumber($left, $right) + protected function opDivNumberNumber(Number $left, Number $right) { - if ($right[1] == 0) { - return [Type::T_STRING, '', [$left[1] . $left[2] . '/' . $right[1] . $right[2]]]; - } - - return new Node\Number($left[1] / $right[1], $left[2]); + return $left->dividedBy($right); } /** * Mod numbers * - * @param array $left - * @param array $right + * @param Number $left + * @param Number $right * - * @return \ScssPhp\ScssPhp\Node\Number + * @return Number */ - protected function opModNumberNumber($left, $right) + protected function opModNumberNumber(Number $left, Number $right) { - return new Node\Number($left[1] % $right[1], $left[2]); + return $left->modulo($right); } /** @@ -3083,11 +3794,11 @@ protected function opAdd($left, $right) /** * Boolean and * - * @param array $left - * @param array $right + * @param array|Number $left + * @param array|Number $right * @param boolean $shouldEval * - * @return array|null + * @return array|Number|null */ protected function opAnd($left, $right, $shouldEval) { @@ -3111,11 +3822,11 @@ protected function opAnd($left, $right, $shouldEval) /** * Boolean or * - * @param array $left - * @param array $right + * @param array|Number $left + * @param array|Number $right * @param boolean $shouldEval * - * @return array|null + * @return array|Number|null */ protected function opOr($left, $right, $shouldEval) { @@ -3147,6 +3858,15 @@ protected function opOr($left, $right, $shouldEval) */ protected function opColorColor($op, $left, $right) { + if ($op !== '==' && $op !== '!=') { + $warning = "Color arithmetic is deprecated and will be an error in future versions.\n" + . "Consider using Sass's color functions instead."; + $fname = $this->getPrettyPath($this->sourceNames[$this->sourceIndex]); + $line = $this->sourceLine; + + fwrite($this->stderr, "DEPRECATION WARNING: $warning\n on line $line of $fname\n\n"); + } + $out = [Type::T_COLOR]; foreach ([1, 2, 3] as $i) { @@ -3167,13 +3887,16 @@ protected function opColorColor($op, $left, $right) break; case '%': + if ($rval == 0) { + throw $this->error("color: Can't take modulo by zero"); + } + $out[] = $lval % $rval; break; case '/': if ($rval == 0) { - $this->throwError("color: Can't divide by zero"); - break 2; + throw $this->error("color: Can't divide by zero"); } $out[] = (int) ($lval / $rval); @@ -3186,8 +3909,7 @@ protected function opColorColor($op, $left, $right) return $this->opNeq($left, $right); default: - $this->throwError("color: unknown op $op"); - break 2; + throw $this->error("color: unknown op $op"); } } @@ -3205,13 +3927,21 @@ protected function opColorColor($op, $left, $right) * * @param string $op * @param array $left - * @param array $right + * @param Number $right * * @return array */ - protected function opColorNumber($op, $left, $right) + protected function opColorNumber($op, $left, Number $right) { - $value = $right[1]; + if ($op === '==') { + return static::$false; + } + + if ($op === '!=') { + return static::$true; + } + + $value = $right->getDimension(); return $this->opColorColor( $op, @@ -3224,14 +3954,22 @@ protected function opColorNumber($op, $left, $right) * Compare number and color * * @param string $op - * @param array $left + * @param Number $left * @param array $right * * @return array */ - protected function opNumberColor($op, $left, $right) + protected function opNumberColor($op, Number $left, $right) { - $value = $left[1]; + if ($op === '==') { + return static::$false; + } + + if ($op === '!=') { + return static::$true; + } + + $value = $left->getDimension(); return $this->opColorColor( $op, @@ -3243,8 +3981,8 @@ protected function opNumberColor($op, $left, $right) /** * Compare number1 == number2 * - * @param array $left - * @param array $right + * @param array|Number $left + * @param array|Number $right * * @return array */ @@ -3264,8 +4002,8 @@ protected function opEq($left, $right) /** * Compare number1 != number2 * - * @param array $left - * @param array $right + * @param array|Number $left + * @param array|Number $right * * @return array */ @@ -3283,70 +4021,81 @@ protected function opNeq($left, $right) } /** - * Compare number1 >= number2 + * Compare number1 == number2 * - * @param array $left - * @param array $right + * @param Number $left + * @param Number $right * * @return array */ - protected function opGteNumberNumber($left, $right) + protected function opEqNumberNumber(Number $left, Number $right) { - return $this->toBool($left[1] >= $right[1]); + return $this->toBool($left->equals($right)); } /** - * Compare number1 > number2 + * Compare number1 != number2 * - * @param array $left - * @param array $right + * @param Number $left + * @param Number $right * * @return array */ - protected function opGtNumberNumber($left, $right) + protected function opNeqNumberNumber(Number $left, Number $right) { - return $this->toBool($left[1] > $right[1]); + return $this->toBool(!$left->equals($right)); } /** - * Compare number1 <= number2 + * Compare number1 >= number2 * - * @param array $left - * @param array $right + * @param Number $left + * @param Number $right * * @return array */ - protected function opLteNumberNumber($left, $right) + protected function opGteNumberNumber(Number $left, Number $right) { - return $this->toBool($left[1] <= $right[1]); + return $this->toBool($left->greaterThanOrEqual($right)); } /** - * Compare number1 < number2 + * Compare number1 > number2 * - * @param array $left - * @param array $right + * @param Number $left + * @param Number $right * * @return array */ - protected function opLtNumberNumber($left, $right) + protected function opGtNumberNumber(Number $left, Number $right) { - return $this->toBool($left[1] < $right[1]); + return $this->toBool($left->greaterThan($right)); } /** - * Three-way comparison, aka spaceship operator + * Compare number1 <= number2 * - * @param array $left - * @param array $right + * @param Number $left + * @param Number $right * - * @return \ScssPhp\ScssPhp\Node\Number + * @return array */ - protected function opCmpNumberNumber($left, $right) + protected function opLteNumberNumber(Number $left, Number $right) { - $n = $left[1] - $right[1]; + return $this->toBool($left->lessThanOrEqual($right)); + } - return new Node\Number($n ? $n / abs($n) : 0, ''); + /** + * Compare number1 < number2 + * + * @param Number $left + * @param Number $right + * + * @return array + */ + protected function opLtNumberNumber(Number $left, Number $right) + { + return $this->toBool($left->lessThan($right)); } /** @@ -3363,6 +4112,48 @@ public function toBool($thing) return $thing ? static::$true : static::$false; } + /** + * Escape non printable chars in strings output as in dart-sass + * @param string $string + * @return string + */ + public function escapeNonPrintableChars($string, $inKeyword = false) + { + static $replacement = []; + if (empty($replacement[$inKeyword])) { + for ($i = 0; $i < 32; $i++) { + if ($i !== 9 || $inKeyword) { + $replacement[$inKeyword][chr($i)] = '\\' . dechex($i) . ($inKeyword ? ' ' : chr(0)); + } + } + } + $string = str_replace(array_keys($replacement[$inKeyword]), array_values($replacement[$inKeyword]), $string); + // chr(0) is not a possible char from the input, so any chr(0) comes from our escaping replacement + if (strpos($string, chr(0)) !== false) { + if (substr($string, -1) === chr(0)) { + $string = substr($string, 0, -1); + } + $string = str_replace( + [chr(0) . '\\',chr(0) . ' '], + [ '\\', ' '], + $string + ); + if (strpos($string, chr(0)) !== false) { + $parts = explode(chr(0), $string); + $string = array_shift($parts); + while (count($parts)) { + $next = array_shift($parts); + if (strpos("0123456789abcdefABCDEF" . chr(9), $next[0]) !== false) { + $string .= " "; + } + $string .= $next; + } + } + } + + return $string; + } + /** * Compiles a primitive value into a CSS property value. * @@ -3376,7 +4167,7 @@ public function toBool($thing) * * @api * - * @param array $value + * @param array|Number|string $value * * @return string */ @@ -3386,6 +4177,9 @@ public function compileValue($value) switch ($value[0]) { case Type::T_KEYWORD: + if (is_string($value[1])) { + $value[1] = $this->escapeNonPrintableChars($value[1], true); + } return $value[1]; case Type::T_COLOR: @@ -3395,14 +4189,38 @@ public function compileValue($value) // [4] - optional alpha component list(, $r, $g, $b) = $value; - $r = round($r); - $g = round($g); - $b = round($b); + $r = $this->compileRGBAValue($r); + $g = $this->compileRGBAValue($g); + $b = $this->compileRGBAValue($b); + + if (\count($value) === 5) { + $alpha = $this->compileRGBAValue($value[4], true); - if (count($value) === 5 && $value[4] !== 1) { // rgba - $a = new Node\Number($value[4], ''); + if (! is_numeric($alpha) || $alpha < 1) { + $colorName = Colors::RGBaToColorName($r, $g, $b, $alpha); - return 'rgba(' . $r . ', ' . $g . ', ' . $b . ', ' . $a . ')'; + if (! \is_null($colorName)) { + return $colorName; + } + + if (is_numeric($alpha)) { + $a = new Number($alpha, ''); + } else { + $a = $alpha; + } + + return 'rgba(' . $r . ', ' . $g . ', ' . $b . ', ' . $a . ')'; + } + } + + if (! is_numeric($r) || ! is_numeric($g) || ! is_numeric($b)) { + return 'rgb(' . $r . ', ' . $g . ', ' . $b . ')'; + } + + $colorName = Colors::RGBaToColorName($r, $g, $b); + + if (! \is_null($colorName)) { + return $colorName; } $h = sprintf('#%02x%02x%02x', $r, $g, $b); @@ -3418,13 +4236,42 @@ public function compileValue($value) return $value->output($this); case Type::T_STRING: - return $value[1] . $this->compileStringContent($value) . $value[1]; + $content = $this->compileStringContent($value); + + if ($value[1]) { + $content = str_replace('\\', '\\\\', $content); + + $content = $this->escapeNonPrintableChars($content); + + // force double quote as string quote for the output in certain cases + if ( + $value[1] === "'" && + (strpos($content, '"') === false or strpos($content, "'") !== false) && + strpbrk($content, '{}\\\'') !== false + ) { + $value[1] = '"'; + } elseif ( + $value[1] === '"' && + (strpos($content, '"') !== false and strpos($content, "'") === false) + ) { + $value[1] = "'"; + } + + $content = str_replace($value[1], '\\' . $value[1], $content); + } + + return $value[1] . $content . $value[1]; case Type::T_FUNCTION: $args = ! empty($value[2]) ? $this->compileValue($value[2]) : ''; return "$value[1]($args)"; + case Type::T_FUNCTION_REFERENCE: + $name = ! empty($value[2]) ? $value[2] : ''; + + return "get-function(\"$name\")"; + case Type::T_LIST: $value = $this->extractInterpolation($value); @@ -3433,29 +4280,72 @@ public function compileValue($value) } list(, $delim, $items) = $value; + $pre = $post = ''; + + if (! empty($value['enclosing'])) { + switch ($value['enclosing']) { + case 'parent': + //$pre = '('; + //$post = ')'; + break; + case 'forced_parent': + $pre = '('; + $post = ')'; + break; + case 'bracket': + case 'forced_bracket': + $pre = '['; + $post = ']'; + break; + } + } + + $prefix_value = ''; if ($delim !== ' ') { - $delim .= ' '; + $prefix_value = ' '; } $filtered = []; + $same_string_quote = null; foreach ($items as $item) { + if (\is_null($same_string_quote)) { + $same_string_quote = false; + if ($item[0] === Type::T_STRING) { + $same_string_quote = $item[1]; + foreach ($items as $ii) { + if ($ii[0] !== Type::T_STRING) { + $same_string_quote = false; + break; + } + } + } + } if ($item[0] === Type::T_NULL) { continue; } + if ($same_string_quote === '"' && $item[0] === Type::T_STRING && $item[1]) { + $item[1] = $same_string_quote; + } + + $compiled = $this->compileValue($item); + + if ($prefix_value && \strlen($compiled)) { + $compiled = $prefix_value . $compiled; + } - $filtered[] = $this->compileValue($item); + $filtered[] = $compiled; } - return implode("$delim", $filtered); + return $pre . substr(implode("$delim", $filtered), \strlen($prefix_value)) . $post; case Type::T_MAP: - $keys = $value[1]; - $values = $value[2]; + $keys = $value[1]; + $values = $value[2]; $filtered = []; - for ($i = 0, $s = count($keys); $i < $s; $i++) { + for ($i = 0, $s = \count($keys); $i < $s; $i++) { $filtered[$this->compileValue($keys[$i])] = $this->compileValue($values[$i]); } @@ -3471,17 +4361,22 @@ public function compileValue($value) list(,, $whiteLeft, $whiteRight) = $interpolate; $delim = $left[1]; - if ($delim && $delim !== ' ' && !$whiteLeft) { + + if ($delim && $delim !== ' ' && ! $whiteLeft) { $delim .= ' '; } - $left = count($left[2]) > 0 ? - $this->compileValue($left) . $delim . $whiteLeft: ''; + + $left = \count($left[2]) > 0 + ? $this->compileValue($left) . $delim . $whiteLeft + : ''; $delim = $right[1]; + if ($delim && $delim !== ' ') { $delim .= ' '; } - $right = count($right[2]) > 0 ? + + $right = \count($right[2]) > 0 ? $whiteRight . $delim . $this->compileValue($right) : ''; return $left . $this->compileValue($interpolate) . $right; @@ -3512,6 +4407,7 @@ public function compileValue($value) } $temp = $this->compileValue([Type::T_KEYWORD, $item]); + if ($temp[0] === Type::T_STRING) { $filtered[] = $this->compileStringContent($temp); } elseif ($temp[0] === Type::T_KEYWORD) { @@ -3525,7 +4421,7 @@ public function compileValue($value) break; case Type::T_STRING: - $reduced = [Type::T_KEYWORD, $this->compileStringContent($reduced)]; + $reduced = [Type::T_STRING, '', [$this->compileStringContent($reduced)]]; break; case Type::T_NULL: @@ -3537,8 +4433,29 @@ public function compileValue($value) case Type::T_NULL: return 'null'; + case Type::T_COMMENT: + return $this->compileCommentValue($value); + + default: + throw $this->error('unknown value type: ' . json_encode($value)); + } + } + + /** + * @param array $value + * + * @return array|string + */ + protected function compileDebugValue($value) + { + $value = $this->reduce($value, true); + + switch ($value[0]) { + case Type::T_STRING: + return $this->compileStringContent($value); + default: - $this->throwError("unknown value type: ".json_encode($value)); + return $this->compileValue($value); } } @@ -3566,7 +4483,7 @@ protected function compileStringContent($string) $parts = []; foreach ($string[2] as $part) { - if (is_array($part) || $part instanceof \ArrayAccess) { + if (\is_array($part) || $part instanceof \ArrayAccess) { $parts[] = $this->compileValue($part); } else { $parts[] = $part; @@ -3589,8 +4506,8 @@ protected function extractInterpolation($list) foreach ($items as $i => $item) { if ($item[0] === Type::T_INTERPOLATE) { - $before = [Type::T_LIST, $list[1], array_slice($items, 0, $i)]; - $after = [Type::T_LIST, $list[1], array_slice($items, $i + 1)]; + $before = [Type::T_LIST, $list[1], \array_slice($items, 0, $i)]; + $after = [Type::T_LIST, $list[1], \array_slice($items, $i + 1)]; return [Type::T_INTERPOLATED, $item, $before, $after]; } @@ -3615,7 +4532,7 @@ protected function multiplySelectors(Environment $env, $selfParent = null) $selfParentSelectors = null; - if (! is_null($selfParent) && $selfParent->selectors) { + if (! \is_null($selfParent) && $selfParent->selectors) { $selfParentSelectors = $this->evalSelectors($selfParent->selectors); } @@ -3627,12 +4544,12 @@ protected function multiplySelectors(Environment $env, $selfParent = null) $selectors = $env->selectors; do { - $stillHasSelf = false; + $stillHasSelf = false; $prevSelectors = $selectors; - $selectors = []; + $selectors = []; - foreach ($prevSelectors as $selector) { - foreach ($parentSelectors as $parent) { + foreach ($parentSelectors as $parent) { + foreach ($prevSelectors as $selector) { if ($selfParentSelectors) { foreach ($selfParentSelectors as $selfParent) { // if no '&' in the selector, each call will give same result, only add once @@ -3652,6 +4569,11 @@ protected function multiplySelectors(Environment $env, $selfParent = null) $selectors = array_values($selectors); + // case we are just starting a at-root : nothing to multiply but parentSelectors + if (! $selectors && $selfParentSelectors) { + $selectors = $selfParentSelectors; + } + return $selectors; } @@ -3660,7 +4582,7 @@ protected function multiplySelectors(Environment $env, $selfParent = null) * * @param array $parent * @param array $child - * @param boolean &$stillHasSelf + * @param boolean $stillHasSelf * @param array $selfParentSelectors * @return array @@ -3682,7 +4604,7 @@ protected function joinSelectors($parent, $child, &$stillHasSelf, $selfParentSel if ($p === static::$selfSelector && ! $setSelf) { $setSelf = true; - if (is_null($selfParentSelectors)) { + if (\is_null($selfParentSelectors)) { $selfParentSelectors = $parent; } @@ -3693,11 +4615,13 @@ protected function joinSelectors($parent, $child, &$stillHasSelf, $selfParentSel } foreach ($parentPart as $pp) { - if (is_array($pp)) { + if (\is_array($pp)) { $flatten = []; + array_walk_recursive($pp, function ($a) use (&$flatten) { $flatten[] = $a; }); + $pp = implode($flatten); } @@ -3725,7 +4649,8 @@ protected function joinSelectors($parent, $child, &$stillHasSelf, $selfParentSel */ protected function multiplyMedia(Environment $env = null, $childQueries = null) { - if (! isset($env) || + if ( + ! isset($env) || ! empty($env->block->type) && $env->block->type !== Type::T_MEDIA ) { return $childQueries; @@ -3741,12 +4666,14 @@ protected function multiplyMedia(Environment $env = null, $childQueries = null) : [[[Type::T_MEDIA_VALUE, $env->block->value]]]; $store = [$this->env, $this->storeEnv]; - $this->env = $env; + + $this->env = $env; $this->storeEnv = null; - $parentQueries = $this->evaluateMediaQuery($parentQueries); + $parentQueries = $this->evaluateMediaQuery($parentQueries); + list($this->env, $this->storeEnv) = $store; - if ($childQueries === null) { + if (\is_null($childQueries)) { $childQueries = $parentQueries; } else { $originalQueries = $childQueries; @@ -3769,9 +4696,11 @@ protected function multiplyMedia(Environment $env = null, $childQueries = null) /** * Convert env linked list to stack * - * @param \ScssPhp\ScssPhp\Compiler\Environment $env + * @param Environment $env * - * @return array + * @return Environment[] + * + * @phpstan-return non-empty-array */ protected function compactEnv(Environment $env) { @@ -3785,9 +4714,11 @@ protected function compactEnv(Environment $env) /** * Convert env stack to singly linked list * - * @param array $envs + * @param Environment[] $envs * - * @return \ScssPhp\ScssPhp\Compiler\Environment + * @return Environment + * + * @phpstan-param non-empty-array $envs */ protected function extractEnv($envs) { @@ -3808,25 +4739,47 @@ protected function extractEnv($envs) */ protected function pushEnv(Block $block = null) { - $env = new Environment; + $env = new Environment(); $env->parent = $this->env; + $env->parentStore = $this->storeEnv; $env->store = []; $env->block = $block; $env->depth = isset($this->env->depth) ? $this->env->depth + 1 : 0; $this->env = $env; + $this->storeEnv = null; return $env; } /** * Pop environment + * + * @return void */ protected function popEnv() { + $this->storeEnv = $this->env->parentStore; $this->env = $this->env->parent; } + /** + * Propagate vars from a just poped Env (used in @each and @for) + * + * @param array $store + * @param null|string[] $excludedVars + * + * @return void + */ + protected function backPropagateEnv($store, $excludedVars = null) + { + foreach ($store as $key => $value) { + if (empty($excludedVars) || ! \in_array($key, $excludedVars)) { + $this->set($key, $value, true); + } + } + } + /** * Get store environment * @@ -3845,6 +4798,8 @@ protected function getStoreEnv() * @param boolean $shadow * @param \ScssPhp\ScssPhp\Compiler\Environment $env * @param mixed $valueUnreduced + * + * @return void */ protected function set($name, $value, $shadow = false, Environment $env = null, $valueUnreduced = null) { @@ -3868,29 +4823,50 @@ protected function set($name, $value, $shadow = false, Environment $env = null, * @param mixed $value * @param \ScssPhp\ScssPhp\Compiler\Environment $env * @param mixed $valueUnreduced + * + * @return void */ protected function setExisting($name, $value, Environment $env, $valueUnreduced = null) { $storeEnv = $env; + $specialContentKey = static::$namespaces['special'] . 'content'; $hasNamespace = $name[0] === '^' || $name[0] === '@' || $name[0] === '%'; + $maxDepth = 10000; + for (;;) { - if (array_key_exists($name, $env->store)) { + if ($maxDepth-- <= 0) { break; } - if (! $hasNamespace && isset($env->marker)) { - $env = $storeEnv; + if (\array_key_exists($name, $env->store)) { break; } - if (! isset($env->parent)) { + if (! $hasNamespace && isset($env->marker)) { + if (! empty($env->store[$specialContentKey])) { + $env = $env->store[$specialContentKey]->scope; + continue; + } + + if (! empty($env->declarationScopeParent)) { + $env = $env->declarationScopeParent; + continue; + } else { + $env = $storeEnv; + break; + } + } + + if (isset($env->parentStore)) { + $env = $env->parentStore; + } elseif (isset($env->parent)) { + $env = $env->parent; + } else { $env = $storeEnv; break; } - - $env = $env->parent; } $env->store[$name] = $value; @@ -3907,6 +4883,8 @@ protected function setExisting($name, $value, Environment $env, $valueUnreduced * @param mixed $value * @param \ScssPhp\ScssPhp\Compiler\Environment $env * @param mixed $valueUnreduced + * + * @return void */ protected function setRaw($name, $value, Environment $env, $valueUnreduced = null) { @@ -3938,7 +4916,6 @@ public function get($name, $shouldThrow = true, Environment $env = null, $unredu $env = $this->getStoreEnv(); } - $nextIsRoot = false; $hasNamespace = $normalizedName[0] === '^' || $normalizedName[0] === '@' || $normalizedName[0] === '%'; $maxDepth = 10000; @@ -3948,7 +4925,7 @@ public function get($name, $shouldThrow = true, Environment $env = null, $unredu break; } - if (array_key_exists($normalizedName, $env->store)) { + if (\array_key_exists($normalizedName, $env->store)) { if ($unreduced && isset($env->storeUnreduced[$normalizedName])) { return $env->storeUnreduced[$normalizedName]; } @@ -3957,24 +4934,30 @@ public function get($name, $shouldThrow = true, Environment $env = null, $unredu } if (! $hasNamespace && isset($env->marker)) { - if (! $nextIsRoot && ! empty($env->store[$specialContentKey])) { + if (! empty($env->store[$specialContentKey])) { $env = $env->store[$specialContentKey]->scope; continue; } - $env = $this->rootEnv; + if (! empty($env->declarationScopeParent)) { + $env = $env->declarationScopeParent; + } else { + $env = $this->rootEnv; + } continue; } - if (! isset($env->parent)) { + if (isset($env->parentStore)) { + $env = $env->parentStore; + } elseif (isset($env->parent)) { + $env = $env->parent; + } else { break; } - - $env = $env->parent; } if ($shouldThrow) { - $this->throwError("Undefined variable \$$name" . ($maxDepth<=0 ? " (infinite recursion)" : "")); + throw $this->error("Undefined variable \$$name" . ($maxDepth <= 0 ? ' (infinite recursion)' : '')); } // found nothing @@ -3991,13 +4974,15 @@ public function get($name, $shouldThrow = true, Environment $env = null, $unredu */ protected function has($name, Environment $env = null) { - return $this->get($name, false, $env) !== null; + return ! \is_null($this->get($name, false, $env)); } /** * Inject variables * * @param array $args + * + * @return void */ protected function injectVariables(array $args) { @@ -4026,6 +5011,8 @@ protected function injectVariables(array $args) * @api * * @param array $variables + * + * @return void */ public function setVariables(array $variables) { @@ -4038,6 +5025,8 @@ public function setVariables(array $variables) * @api * * @param string $name + * + * @return void */ public function unsetVariable($name) { @@ -4062,10 +5051,12 @@ public function getVariables() * @api * * @param string $path + * + * @return void */ public function addParsedFile($path) { - if (isset($path) && file_exists($path)) { + if (isset($path) && is_file($path)) { $this->parsedFiles[realpath($path)] = filemtime($path); } } @@ -4088,10 +5079,12 @@ public function getParsedFiles() * @api * * @param string|callable $path + * + * @return void */ public function addImportPath($path) { - if (! in_array($path, $this->importPaths)) { + if (! \in_array($path, $this->importPaths)) { $this->importPaths[] = $path; } } @@ -4101,11 +5094,24 @@ public function addImportPath($path) * * @api * - * @param string|array $path + * @param string|array $path + * + * @return void */ public function setImportPaths($path) { - $this->importPaths = (array) $path; + $paths = (array) $path; + $actualImportPaths = array_filter($paths, function ($path) { + return $path !== ''; + }); + + $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); + } + + $this->importPaths = $actualImportPaths; } /** @@ -4114,10 +5120,42 @@ public function setImportPaths($path) * @api * * @param integer $numberPrecision + * + * @return void + * + * @deprecated The number precision is not configurable anymore. The default is enough for all browsers. */ public function setNumberPrecision($numberPrecision) { - Node\Number::$precision = $numberPrecision; + @trigger_error('The number precision is not configurable anymore. ' + . 'The default is enough for all browsers.', E_USER_DEPRECATED); + } + + /** + * Sets the output style. + * + * @api + * + * @param string $style One of the OutputStyle constants + * + * @return void + * + * @phpstan-param OutputStyle::* $style + */ + public function setOutputStyle($style) + { + switch ($style) { + case OutputStyle::EXPANDED: + $this->formatter = Expanded::class; + break; + + case OutputStyle::COMPRESSED: + $this->formatter = Compressed::class; + break; + + default: + throw new \InvalidArgumentException(sprintf('Invalid output style "%s".', $style)); + } } /** @@ -4126,9 +5164,18 @@ public function setNumberPrecision($numberPrecision) * @api * * @param string $formatterName + * + * @return void + * + * @deprecated Use {@see setOutputStyle} instead. */ public function setFormatter($formatterName) { + if (!\in_array($formatterName, [Expanded::class, Compressed::class], true)) { + @trigger_error('Formatters other than Expanded and Compressed are deprecated.', E_USER_DEPRECATED); + } + @trigger_error('The method "setFormatter" is deprecated. Use "setOutputStyle" instead.', E_USER_DEPRECATED); + $this->formatter = $formatterName; } @@ -4138,10 +5185,15 @@ public function setFormatter($formatterName) * @api * * @param string $lineNumberStyle + * + * @return void + * + * @deprecated The line number output is not supported anymore. Use source maps instead. */ public function setLineNumberStyle($lineNumberStyle) { - $this->lineNumberStyle = $lineNumberStyle; + @trigger_error('The line number output is not supported anymore. ' + . 'Use source maps instead.', E_USER_DEPRECATED); } /** @@ -4150,6 +5202,10 @@ public function setLineNumberStyle($lineNumberStyle) * @api * * @param integer $sourceMap + * + * @return void + * + * @phpstan-param self::SOURCE_MAP_* $sourceMap */ public function setSourceMap($sourceMap) { @@ -4162,6 +5218,8 @@ public function setSourceMap($sourceMap) * @api * * @param array $sourceMapOptions + * + * @return void */ public function setSourceMapOptions($sourceMapOptions) { @@ -4175,7 +5233,9 @@ public function setSourceMapOptions($sourceMapOptions) * * @param string $name * @param callable $func - * @param array $prototype + * @param array|null $prototype + * + * @return void */ public function registerFunction($name, $func, $prototype = null) { @@ -4188,6 +5248,8 @@ public function registerFunction($name, $func, $prototype = null) * @api * * @param string $name + * + * @return void */ public function unregisterFunction($name) { @@ -4200,9 +5262,15 @@ public function unregisterFunction($name) * @api * * @param string $name + * + * @return void + * + * @deprecated Registering additional features is deprecated. */ public function addFeature($name) { + @trigger_error('Registering additional features is deprecated.', E_USER_DEPRECATED); + $this->registeredFeatures[$name] = true; } @@ -4211,9 +5279,12 @@ public function addFeature($name) * * @param string $path * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $out + * + * @return void */ protected function importFile($path, OutputBlock $out) { + $this->pushCallStack('import ' . $this->getPrettyPath($path)); // see if tree is cached $realPath = realpath($path); @@ -4229,10 +5300,12 @@ protected function importFile($path, OutputBlock $out) $this->importCache[$realPath] = $tree; } - $pi = pathinfo($path); - array_unshift($this->importPaths, $pi['dirname']); + $currentDirectory = $this->currentDirectory; + $this->currentDirectory = dirname($path); + $this->compileChildrenNoReturn($tree->children, $out); - array_shift($this->importPaths); + $this->currentDirectory = $currentDirectory; + $this->popCallStack(); } /** @@ -4246,44 +5319,185 @@ protected function importFile($path, OutputBlock $out) */ public function findImport($url) { - $urls = []; - // for "normal" scss imports (ignore vanilla css and external requests) - if (! preg_match('/\.css$|^https?:\/\//', $url)) { - // try both normal and the _partial filename - $urls = [$url, preg_replace('/[^\/]+$/', '_\0', $url)]; - } + // Callback importers are still called for BC. + if (preg_match('~\.css$|^https?://|^//~', $url)) { + foreach ($this->importPaths as $dir) { + if (\is_string($dir)) { + continue; + } - $hasExtension = preg_match('/[.]s?css$/', $url); + if (\is_callable($dir)) { + // check custom callback for import path + $file = \call_user_func($dir, $url); - foreach ($this->importPaths as $dir) { - if (is_string($dir)) { - // check urls for normal import paths - foreach ($urls as $full) { - $separator = ( - ! empty($dir) && - substr($dir, -1) !== '/' && - substr($full, 0, 1) !== '/' - ) ? '/' : ''; - $full = $dir . $separator . $full; - - if ($this->fileExists($file = $full . '.scss') || - ($hasExtension && $this->fileExists($file = $full)) - ) { + if (! \is_null($file)) { return $file; } } - } elseif (is_callable($dir)) { + } + return null; + } + + if (!\is_null($this->currentDirectory)) { + $relativePath = $this->resolveImportPath($url, $this->currentDirectory); + + if (!\is_null($relativePath)) { + return $relativePath; + } + } + + foreach ($this->importPaths as $dir) { + if (\is_string($dir)) { + $path = $this->resolveImportPath($url, $dir); + + if (!\is_null($path)) { + return $path; + } + } elseif (\is_callable($dir)) { // check custom callback for import path - $file = call_user_func($dir, $url); + $file = \call_user_func($dir, $url); - if ($file !== null) { + if (! \is_null($file)) { return $file; } } } - return null; + if ($this->legacyCwdImportPath) { + $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); + + return $path; + } + } + + throw $this->error("`$url` file not found for @import"); + } + + /** + * @param string $url + * @param string $baseDir + * + * @return string|null + */ + private function resolveImportPath($url, $baseDir) + { + $path = Path::join($baseDir, $url); + + $hasExtension = preg_match('/.scss$/', $url); + + if ($hasExtension) { + return $this->checkImportPathConflicts($this->tryImportPath($path)); + } + + $result = $this->checkImportPathConflicts($this->tryImportPathWithExtensions($path)); + + if (!\is_null($result)) { + return $result; + } + + return $this->tryImportPathAsDirectory($path); + } + + /** + * @param string[] $paths + * + * @return string|null + */ + private function checkImportPathConflicts(array $paths) + { + if (\count($paths) === 0) { + return null; + } + + if (\count($paths) === 1) { + return $paths[0]; + } + + $formattedPrettyPaths = []; + + foreach ($paths as $path) { + $formattedPrettyPaths[] = ' ' . $this->getPrettyPath($path); + } + + throw $this->error("It's not clear which file to import. Found:\n" . implode("\n", $formattedPrettyPaths)); + } + + /** + * @param string $path + * + * @return string[] + */ + private function tryImportPathWithExtensions($path) + { + $result = $this->tryImportPath($path.'.scss'); + + if ($result) { + return $result; + } + + return $this->tryImportPath($path.'.css'); + } + + /** + * @param string $path + * + * @return string[] + */ + private function tryImportPath($path) + { + $partial = dirname($path).'/_'.basename($path); + + $candidates = []; + + if (is_file($partial)) { + $candidates[] = $partial; + } + + if (is_file($path)) { + $candidates[] = $path; + } + + return $candidates; + } + + /** + * @param string $path + * + * @return string|null + */ + private function tryImportPathAsDirectory($path) + { + if (!is_dir($path)) { + return null; + } + + return $this->checkImportPathConflicts($this->tryImportPathWithExtensions($path.'/index')); + } + + /** + * @param string $path + * + * @return string + */ + private function getPrettyPath($path) + { + $normalizedPath = $path; + $normalizedRootDirectory = $this->rootDirectory.'/'; + + if (\DIRECTORY_SEPARATOR === '\\') { + $normalizedRootDirectory = str_replace('\\', '/', $normalizedRootDirectory); + $normalizedPath = str_replace('\\', '/', $path); + } + + if (0 === strpos($normalizedPath, $normalizedRootDirectory)) { + return substr($normalizedPath, \strlen($normalizedRootDirectory)); + } + + return $path; } /** @@ -4292,6 +5506,8 @@ public function findImport($url) * @api * * @param string $encoding + * + * @return void */ public function setEncoding($encoding) { @@ -4306,14 +5522,30 @@ public function setEncoding($encoding) * @param boolean $ignoreErrors * * @return \ScssPhp\ScssPhp\Compiler + * + * @deprecated Ignoring Sass errors is not longer supported. */ public function setIgnoreErrors($ignoreErrors) { - $this->ignoreErrors = $ignoreErrors; + @trigger_error('Ignoring Sass errors is not longer supported.', E_USER_DEPRECATED); return $this; } + /** + * Get source position + * + * @api + * + * @return array + */ + public function getSourcePosition() + { + $sourceFile = isset($this->sourceNames[$this->sourceIndex]) ? $this->sourceNames[$this->sourceIndex] : ''; + + return [$sourceFile, $this->sourceLine, $this->sourceColumn]; + } + /** * Throw error (exception) * @@ -4322,33 +5554,85 @@ public function setIgnoreErrors($ignoreErrors) * @param string $msg Message with optional sprintf()-style vararg parameters * * @throws \ScssPhp\ScssPhp\Exception\CompilerException + * + * @deprecated use "error" and throw the exception in the caller instead. */ public function throwError($msg) { - if ($this->ignoreErrors) { - return; + @trigger_error( + 'The method "throwError" is deprecated. Use "error" and throw the exception in the caller instead', + E_USER_DEPRECATED + ); + + throw $this->error(...func_get_args()); + } + + /** + * Build an error (exception) + * + * @api + * + * @param string $msg Message with optional sprintf()-style vararg parameters + * + * @return CompilerException + */ + public function error($msg, ...$args) + { + if ($args) { + $msg = sprintf($msg, ...$args); } - $line = $this->sourceLine; - $column = $this->sourceColumn; + if (! $this->ignoreCallStackMessage) { + $line = $this->sourceLine; + $column = $this->sourceColumn; - $loc = isset($this->sourceNames[$this->sourceIndex]) - ? $this->sourceNames[$this->sourceIndex] . " on line $line, at column $column" - : "line: $line, column: $column"; + $loc = isset($this->sourceNames[$this->sourceIndex]) + ? $this->getPrettyPath($this->sourceNames[$this->sourceIndex]) . " on line $line, at column $column" + : "line: $line, column: $column"; - if (func_num_args() > 1) { - $msg = call_user_func_array('sprintf', func_get_args()); + $msg = "$msg: $loc"; + + $callStackMsg = $this->callStackMessage(); + + if ($callStackMsg) { + $msg .= "\nCall Stack:\n" . $callStackMsg; + } } - $msg = "$msg: $loc"; + return new CompilerException($msg); + } - $callStackMsg = $this->callStackMessage(); + /** + * @param string $functionName + * @param array $ExpectedArgs + * @param int $nbActual + * @return CompilerException + */ + public function errorArgsNumber($functionName, $ExpectedArgs, $nbActual) + { + $nbExpected = \count($ExpectedArgs); - if ($callStackMsg) { - $msg .= "\nCall Stack:\n" . $callStackMsg; - } + if ($nbActual > $nbExpected) { + return $this->error( + 'Error: Only %d arguments allowed in %s(), but %d were passed.', + $nbExpected, + $functionName, + $nbActual + ); + } else { + $missing = []; + + while (count($ExpectedArgs) && count($ExpectedArgs) > $nbActual) { + array_unshift($missing, array_pop($ExpectedArgs)); + } - throw new CompilerException($msg); + return $this->error( + 'Error: %s() argument%s %s missing.', + $functionName, + count($missing) > 1 ? 's' : '', + implode(', ', $missing) + ); + } } /** @@ -4367,14 +5651,15 @@ protected function callStackMessage($all = false, $limit = null) if ($this->callStack) { foreach (array_reverse($this->callStack) as $call) { if ($all || (isset($call['n']) && $call['n'])) { - $msg = "#" . $ncall++ . " " . $call['n'] . " "; + $msg = '#' . $ncall++ . ' ' . $call['n'] . ' '; $msg .= (isset($this->sourceNames[$call[Parser::SOURCE_INDEX]]) - ? $this->sourceNames[$call[Parser::SOURCE_INDEX]] + ? $this->getPrettyPath($this->sourceNames[$call[Parser::SOURCE_INDEX]]) : '(unknown file)'); - $msg .= " on line " . $call[Parser::SOURCE_LINE]; + $msg .= ' on line ' . $call[Parser::SOURCE_LINE]; + $callStackMsg[] = $msg; - if (! is_null($limit) && $ncall>$limit) { + if (! \is_null($limit) && $ncall > $limit) { break; } } @@ -4394,117 +5679,99 @@ protected function callStackMessage($all = false, $limit = null) protected function handleImportLoop($name) { for ($env = $this->env; $env; $env = $env->parent) { + if (! $env->block) { + continue; + } + $file = $this->sourceNames[$env->block->sourceIndex]; if (realpath($file) === $name) { - $this->throwError('An @import loop has been found: %s imports %s', $file, basename($file)); - break; + throw $this->error('An @import loop has been found: %s imports %s', $file, basename($file)); } } } - /** - * Does file exist? - * - * @param string $name - * - * @return boolean - */ - protected function fileExists($name) - { - return file_exists($name) && is_file($name); - } - /** * Call SCSS @function * - * @param string $name + * @param Object $func * @param array $argValues - * @param array $returnValue * - * @return boolean Returns true if returnValue is set; otherwise, false + * @return array */ - protected function callScssFunction($name, $argValues, &$returnValue) + protected function callScssFunction($func, $argValues) { - $func = $this->get(static::$namespaces['function'] . $name, false); - if (! $func) { - return false; + return static::$defaultValue; } + $name = $func->name; $this->pushEnv(); - $storeEnv = $this->storeEnv; - $this->storeEnv = $this->env; - // set the args if (isset($func->args)) { $this->applyArguments($func->args, $argValues); } // throw away lines and children - $tmp = new OutputBlock; + $tmp = new OutputBlock(); $tmp->lines = []; $tmp->children = []; $this->env->marker = 'function'; - $ret = $this->compileChildren($func->children, $tmp, $this->env->marker . " " . $name); + if (! empty($func->parentEnv)) { + $this->env->declarationScopeParent = $func->parentEnv; + } else { + throw $this->error("@function $name() without parentEnv"); + } - $this->storeEnv = $storeEnv; + $ret = $this->compileChildren($func->children, $tmp, $this->env->marker . ' ' . $name); $this->popEnv(); - $returnValue = ! isset($ret) ? static::$defaultValue : $ret; - - return true; + return ! isset($ret) ? static::$defaultValue : $ret; } /** * Call built-in and registered (PHP) functions * * @param string $name + * @param string|array $function + * @param array $prototype * @param array $args - * @param array $returnValue * - * @return boolean Returns true if returnValue is set; otherwise, false + * @return array|Number|null */ - protected function callNativeFunction($name, $args, &$returnValue) + protected function callNativeFunction($name, $function, $prototype, $args) { - // try a lib function - $name = $this->normalizeName($name); + $libName = (is_array($function) ? end($function) : null); + $sorted_kwargs = $this->sortNativeFunctionArgs($libName, $prototype, $args); - if (isset($this->userFunctions[$name])) { - // see if we can find a user function - list($f, $prototype) = $this->userFunctions[$name]; - } elseif (($f = $this->getBuiltinFunction($name)) && is_callable($f)) { - $libName = $f[1]; - $prototype = isset(static::$$libName) ? static::$$libName : null; - } else { - return false; + if (\is_null($sorted_kwargs)) { + return null; } + @list($sorted, $kwargs) = $sorted_kwargs; - @list($sorted, $kwargs) = $this->sortNativeFunctionArgs($prototype, $args); - - if ($name !== 'if' && $name !== 'call') { + if ($name !== 'if') { $inExp = true; + if ($name === 'join') { $inExp = false; } + foreach ($sorted as &$val) { $val = $this->reduce($val, $inExp); } } - $returnValue = call_user_func($f, $sorted, $kwargs); + $returnValue = \call_user_func($function, $sorted, $kwargs); if (! isset($returnValue)) { - return false; + return null; } - $returnValue = $this->coerceValue($returnValue); - - return true; + return $this->coerceValue($returnValue); } /** @@ -4516,6 +5783,18 @@ protected function callNativeFunction($name, $args, &$returnValue) */ protected function getBuiltinFunction($name) { + $libName = self::normalizeNativeFunctionName($name); + return [$this, $libName]; + } + + /** + * Normalize native function name + * @param string $name + * @return string + */ + public static function normalizeNativeFunctionName($name) + { + $name = str_replace("-", "_", $name); $libName = 'lib' . preg_replace_callback( '/_(.)/', function ($m) { @@ -4523,19 +5802,29 @@ function ($m) { }, ucfirst($name) ); + return $libName; + } - return [$this, $libName]; + /** + * Check if a function is a native built-in scss function, for css parsing + * @param string $name + * @return bool + */ + public static function isNativeFunction($name) + { + return method_exists(Compiler::class, self::normalizeNativeFunctionName($name)); } /** * Sorts keyword arguments * - * @param array $prototype - * @param array $args + * @param string $functionName + * @param array $prototypes + * @param array $args * - * @return array + * @return array|null */ - protected function sortNativeFunctionArgs($prototypes, $args) + protected function sortNativeFunctionArgs($functionName, $prototypes, $args) { static $parser = null; @@ -4543,25 +5832,44 @@ protected function sortNativeFunctionArgs($prototypes, $args) $keyArgs = []; $posArgs = []; + if (\is_array($args) && \count($args) && \end($args) === static::$null) { + array_pop($args); + } + // separate positional and keyword arguments foreach ($args as $arg) { list($key, $value) = $arg; - $key = $key[1]; - - if (empty($key)) { + if (empty($key) or empty($key[1])) { $posArgs[] = empty($arg[2]) ? $value : $arg; } else { - $keyArgs[$key] = $value; + $keyArgs[$key[1]] = $value; } } return [$posArgs, $keyArgs]; } + // specific cases ? + if (\in_array($functionName, ['libRgb', 'libRgba', 'libHsl', 'libHsla'])) { + // 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; + } + } + } + } + $finalArgs = []; - if (! is_array(reset($prototypes))) { + if (! \is_array(reset($prototypes))) { $prototypes = [$prototypes]; } @@ -4579,14 +5887,14 @@ protected function sortNativeFunctionArgs($prototypes, $args) $p = explode(':', $p, 2); $name = array_shift($p); - if (count($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)) { + if (\is_null($parser)) { $parser = $this->parserFactory(__METHOD__); } @@ -4604,7 +5912,18 @@ protected function sortNativeFunctionArgs($prototypes, $args) $argDef[] = [$name, $default, $isVariable]; } + $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 @@ -4641,10 +5960,20 @@ protected function sortNativeFunctionArgs($prototypes, $args) } catch (CompilerException $e) { $exceptionMessage = $e->getMessage(); } + $this->ignoreCallStackMessage = $ignoreCallStackMessage; } if ($exceptionMessage && ! $prototypeHasMatch) { - $this->throwError($exceptionMessage); + 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; + } + } + } + + throw $this->error($exceptionMessage); } return [$finalArgs, $keyArgs]; @@ -4653,21 +5982,28 @@ protected function sortNativeFunctionArgs($prototypes, $args) /** * Apply argument values per definition * - * @param array $argDef - * @param array $argValues - * @param bool $storeInEnv - * @param bool $reduce + * @param array $argDef + * @param array $argValues + * @param boolean $storeInEnv + * @param boolean $reduce * only used if $storeInEnv = false + * + * @return array + * * @throws \Exception */ protected function applyArguments($argDef, $argValues, $storeInEnv = true, $reduce = true) { $output = []; + if (\is_array($argValues) && \count($argValues) && end($argValues) === static::$null) { + array_pop($argValues); + } + if ($storeInEnv) { $storeEnv = $this->getStoreEnv(); - $env = new Environment; + $env = new Environment(); $env->store = $storeEnv->store; } @@ -4684,6 +6020,7 @@ protected function applyArguments($argDef, $argValues, $storeInEnv = true, $redu $splatSeparator = null; $keywordArgs = []; $deferredKeywordArgs = []; + $deferredNamedKeywordArgs = []; $remaining = []; $hasKeywordArgument = false; @@ -4692,28 +6029,38 @@ protected function applyArguments($argDef, $argValues, $storeInEnv = true, $redu if (! empty($arg[0])) { $hasKeywordArgument = true; - if (! isset($args[$arg[0][1]]) || $args[$arg[0][1]][3]) { + $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) { - $deferredKeywordArgs[$arg[0][1]] = $arg[1]; + $deferredNamedKeywordArgs[$name] = $arg[1]; } else { - $this->throwError("Mixin or function doesn't have an argument named $%s.", $arg[0][1]); - break; + throw $this->error("Mixin or function doesn't have an argument named $%s.", $arg[0][1]); } - } elseif ($args[$arg[0][1]][0] < count($remaining)) { - $this->throwError("The argument $%s was passed both by position and by name.", $arg[0][1]); - break; + } 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[$arg[0][1]] = $arg[1]; + $keywordArgs[$name] = $arg[1]; } - } elseif ($arg[2] === true) { + } 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])) { + if (! isset($args[$name])) { foreach (array_keys($args) as $an) { - if (str_replace("_", "-", $an) === str_replace("_", "-", $name)) { + if (str_replace('_', '-', $an) === str_replace('_', '-', $name)) { $name = $an; break; } @@ -4726,9 +6073,10 @@ protected function applyArguments($argDef, $argValues, $storeInEnv = true, $redu $keywordArgs[$name] = $item; } } else { - if (is_null($splatSeparator)) { + if (\is_null($splatSeparator)) { $splatSeparator = $val[1]; } + $remaining[] = $item; } } @@ -4738,23 +6086,25 @@ protected function applyArguments($argDef, $argValues, $storeInEnv = true, $redu $item = $val[2][$i]; if (! is_numeric($name)) { - if (!isset($args[$name])) { + if (! isset($args[$name])) { foreach (array_keys($args) as $an) { - if (str_replace("_", "-", $an) === str_replace("_", "-", $name)) { + if (str_replace('_', '-', $an) === str_replace('_', '-', $name)) { $name = $an; break; } } } + if ($hasVariable) { $deferredKeywordArgs[$name] = $item; } else { $keywordArgs[$name] = $item; } } else { - if (is_null($splatSeparator)) { + if (\is_null($splatSeparator)) { $splatSeparator = $val[1]; } + $remaining[] = $item; } } @@ -4762,8 +6112,7 @@ protected function applyArguments($argDef, $argValues, $storeInEnv = true, $redu $remaining[] = $val; } } elseif ($hasKeywordArgument) { - $this->throwError('Positional arguments must come before keyword arguments.'); - break; + throw $this->error('Positional arguments must come before keyword arguments.'); } else { $remaining[] = $arg[1]; } @@ -4773,15 +6122,27 @@ protected function applyArguments($argDef, $argValues, $storeInEnv = true, $redu list($i, $name, $default, $isVariable) = $arg; if ($isVariable) { - $val = [Type::T_LIST, is_null($splatSeparator) ? ',' : $splatSeparator , [], $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); + } + } - for ($count = count($remaining); $i < $count; $i++) { + $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])) { @@ -4789,8 +6150,7 @@ protected function applyArguments($argDef, $argValues, $storeInEnv = true, $redu } elseif (! empty($default)) { continue; } else { - $this->throwError("Missing argument $name"); - break; + throw $this->error("Missing argument $name"); } if ($storeInEnv) { @@ -4826,62 +6186,46 @@ protected function applyArguments($argDef, $argValues, $storeInEnv = true, $redu * * @param mixed $value * - * @return array|\ScssPhp\ScssPhp\Node\Number + * @return array|Number */ protected function coerceValue($value) { - if (is_array($value) || $value instanceof \ArrayAccess) { + if (\is_array($value) || $value instanceof \ArrayAccess) { return $value; } - if (is_bool($value)) { + if (\is_bool($value)) { return $this->toBool($value); } - if ($value === null) { + if (\is_null($value)) { return static::$null; } if (is_numeric($value)) { - return new Node\Number($value, ''); + return new Number($value, ''); } if ($value === '') { return static::$emptyString; } - if (preg_match('/^(#([0-9a-f]{6})|#([0-9a-f]{3}))$/i', $value, $m)) { - $color = [Type::T_COLOR]; - - if (isset($m[3])) { - $num = hexdec($m[3]); - - foreach ([3, 2, 1] as $i) { - $t = $num & 0xf; - $color[$i] = $t << 4 | $t; - $num >>= 4; - } - } else { - $num = hexdec($m[2]); - - foreach ([3, 2, 1] as $i) { - $color[$i] = $num & 0xff; - $num >>= 8; - } - } + $value = [Type::T_KEYWORD, $value]; + $color = $this->coerceColor($value); + if ($color) { return $color; } - return [Type::T_KEYWORD, $value]; + return $value; } /** * Coerce something to map * - * @param array $item + * @param array|Number $item * - * @return array + * @return array|Number */ protected function coerceMap($item) { @@ -4889,24 +6233,34 @@ protected function coerceMap($item) return $item; } - if ($item === static::$emptyList) { + if ( + $item[0] === static::$emptyList[0] && + $item[1] === static::$emptyList[1] && + $item[2] === static::$emptyList[2] + ) { return static::$emptyMap; } - return [Type::T_MAP, [$item], [static::$null]]; + return $item; } /** * Coerce something to list * - * @param array $item - * @param string $delim + * @param array $item + * @param string $delim + * @param boolean $removeTrailingNull * * @return array */ - protected function coerceList($item, $delim = ',') + protected function coerceList($item, $delim = ',', $removeTrailingNull = false) { if (isset($item) && $item[0] === Type::T_LIST) { + // remove trailing null from the list + if ($removeTrailingNull && end($item[2]) === static::$null) { + array_pop($item[2]); + } + return $item; } @@ -4915,13 +6269,15 @@ protected function coerceList($item, $delim = ',') $values = $item[2]; $list = []; - for ($i = 0, $s = count($keys); $i < $s; $i++) { + for ($i = 0, $s = \count($keys); $i < $s; $i++) { $key = $keys[$i]; $value = $values[$i]; switch ($key[0]) { case Type::T_LIST: case Type::T_MAP: + case Type::T_STRING: + case Type::T_NULL: break; default: @@ -4939,15 +6295,15 @@ protected function coerceList($item, $delim = ',') return [Type::T_LIST, ',', $list]; } - return [Type::T_LIST, $delim, ! isset($item) ? []: [$item]]; + return [Type::T_LIST, $delim, ! isset($item) ? [] : [$item]]; } /** * Coerce color for expression * - * @param array $value + * @param array|Number $value * - * @return array|null + * @return array|Number */ protected function coerceForExpression($value) { @@ -4961,25 +6317,112 @@ protected function coerceForExpression($value) /** * Coerce value to color * - * @param array $value + * @param array|Number $value + * @param bool $inRGBFunction * * @return array|null */ - protected function coerceColor($value) + protected function coerceColor($value, $inRGBFunction = false) { switch ($value[0]) { case Type::T_COLOR: + for ($i = 1; $i <= 3; $i++) { + if (! is_numeric($value[$i])) { + $cv = $this->compileRGBAValue($value[$i]); + + if (! is_numeric($cv)) { + return null; + } + + $value[$i] = $cv; + } + + if (isset($value[4])) { + if (! is_numeric($value[4])) { + $cv = $this->compileRGBAValue($value[4], true); + + if (! is_numeric($cv)) { + return null; + } + + $value[4] = $cv; + } + } + } + return $value; + case Type::T_LIST: + if ($inRGBFunction) { + if (\count($value[2]) == 3 || \count($value[2]) == 4) { + $color = $value[2]; + array_unshift($color, Type::T_COLOR); + + return $this->coerceColor($color); + } + } + + return null; + case Type::T_KEYWORD: + if (! \is_string($value[1])) { + return null; + } + $name = strtolower($value[1]); - if (isset(Colors::$cssColors[$name])) { - $rgba = explode(',', Colors::$cssColors[$name]); + // hexa color? + if (preg_match('/^#([0-9a-f]+)$/i', $name, $m)) { + $nofValues = \strlen($m[1]); + + if (\in_array($nofValues, [3, 4, 6, 8])) { + $nbChannels = 3; + $color = []; + $num = hexdec($m[1]); + + switch ($nofValues) { + case 4: + $nbChannels = 4; + // then continuing with the case 3: + case 3: + for ($i = 0; $i < $nbChannels; $i++) { + $t = $num & 0xf; + array_unshift($color, $t << 4 | $t); + $num >>= 4; + } + + break; + + case 8: + $nbChannels = 4; + // then continuing with the case 6: + case 6: + for ($i = 0; $i < $nbChannels; $i++) { + array_unshift($color, $num & 0xff); + $num >>= 8; + } + + break; + } + + if ($nbChannels === 4) { + if ($color[3] === 255) { + $color[3] = 1; // fully opaque + } else { + $color[3] = round($color[3] / 255, Number::PRECISION); + } + } + + array_unshift($color, Type::T_COLOR); + return $color; + } + } + + if ($rgba = Colors::colorNameToRGBa($name)) { return isset($rgba[3]) - ? [Type::T_COLOR, (int) $rgba[0], (int) $rgba[1], (int) $rgba[2], (int) $rgba[3]] - : [Type::T_COLOR, (int) $rgba[0], (int) $rgba[1], (int) $rgba[2]]; + ? [Type::T_COLOR, $rgba[0], $rgba[1], $rgba[2], $rgba[3]] + : [Type::T_COLOR, $rgba[0], $rgba[1], $rgba[2]]; } return null; @@ -4988,12 +6431,74 @@ protected function coerceColor($value) return null; } + /** + * @param integer|Number $value + * @param boolean $isAlpha + * + * @return integer|mixed + */ + protected function compileRGBAValue($value, $isAlpha = false) + { + if ($isAlpha) { + return $this->compileColorPartValue($value, 0, 1, false); + } + + return $this->compileColorPartValue($value, 0, 255, true); + } + + /** + * @param mixed $value + * @param integer|float $min + * @param integer|float $max + * @param boolean $isInt + * + * @return integer|mixed + */ + protected function compileColorPartValue($value, $min, $max, $isInt = true) + { + if (! is_numeric($value)) { + if (\is_array($value)) { + $reduced = $this->reduce($value); + + if ($reduced instanceof Number) { + $value = $reduced; + } + } + + if ($value instanceof Number) { + if ($value->unitless()) { + $num = $value->getDimension(); + } elseif ($value->hasUnit('%')) { + $num = $max * $value->getDimension() / 100; + } else { + throw $this->error('Expected %s to have no units or "%%".', $value); + } + + $value = $num; + } elseif (\is_array($value)) { + $value = $this->compileValue($value); + } + } + + if (is_numeric($value)) { + if ($isInt) { + $value = round($value); + } + + $value = min($max, max($min, $value)); + + return $value; + } + + return $value; + } + /** * Coerce value to string * - * @param array $value + * @param array|Number $value * - * @return array|null + * @return array */ protected function coerceString($value) { @@ -5004,21 +6509,51 @@ protected function coerceString($value) return [Type::T_STRING, '', [$this->compileValue($value)]]; } + /** + * Assert value is a string (or keyword) + * + * @api + * + * @param array|Number $value + * @param string $varName + * + * @return array + * + * @throws \Exception + */ + public function assertString($value, $varName = null) + { + // case of url(...) parsed a a function + if ($value[0] === Type::T_FUNCTION) { + $value = $this->coerceString($value); + } + + 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."); + } + + $value = $this->coerceString($value); + + return $value; + } + /** * Coerce value to a percentage * - * @param array $value + * @param array|Number $value * * @return integer|float */ protected function coercePercent($value) { - if ($value[0] === Type::T_NUMBER) { - if (! empty($value[2]['%'])) { - return $value[1] / 100; + if ($value instanceof Number) { + if ($value->hasUnit('%')) { + return $value->getDimension() / 100; } - return $value[1]; + return $value->getDimension(); } return 0; @@ -5029,7 +6564,7 @@ protected function coercePercent($value) * * @api * - * @param array $value + * @param array|Number $value * * @return array * @@ -5040,7 +6575,7 @@ public function assertMap($value) $value = $this->coerceMap($value); if ($value[0] !== Type::T_MAP) { - $this->throwError('expecting map, %s received', $value[0]); + throw $this->error('expecting map, %s received', $value[0]); } return $value; @@ -5051,7 +6586,7 @@ public function assertMap($value) * * @api * - * @param array $value + * @param array|Number $value * * @return array * @@ -5060,7 +6595,7 @@ public function assertMap($value) public function assertList($value) { if ($value[0] !== Type::T_LIST) { - $this->throwError('expecting list, %s received', $value[0]); + throw $this->error('expecting list, %s received', $value[0]); } return $value; @@ -5071,7 +6606,7 @@ public function assertList($value) * * @api * - * @param array $value + * @param array|Number $value * * @return array * @@ -5083,7 +6618,7 @@ public function assertColor($value) return $color; } - $this->throwError('expecting color, %s received', $value[0]); + throw $this->error('expecting color, %s received', $value[0]); } /** @@ -5091,21 +6626,49 @@ public function assertColor($value) * * @api * - * @param array $value + * @param array|Number $value + * @param string $varName * - * @return integer|float + * @return Number + * + * @throws \Exception + */ + 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."); + } + + return $value; + } + + /** + * Assert value is a integer + * + * @api + * + * @param array|Number $value + * @param string $varName + * + * @return integer * * @throws \Exception */ - public function assertNumber($value) + public function assertInteger($value, $varName = null) { - if ($value[0] !== Type::T_NUMBER) { - $this->throwError('expecting number, %s received', $value[0]); + + $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."); } - return $value[1]; + return intval($value); } + /** * Make sure a color's components don't go out of bounds * @@ -5194,7 +6757,7 @@ protected function hueToRGB($m1, $m2, $h) } if ($h * 3 < 2) { - return $m1 + ($m2 - $m1) * (2/3 - $h) * 6; + return $m1 + ($m2 - $m1) * (2 / 3 - $h) * 6; } return $m1; @@ -5224,9 +6787,9 @@ public function toRGB($hue, $saturation, $lightness) $m2 = $l <= 0.5 ? $l * ($s + 1) : $l + $s - $l * $s; $m1 = $l * 2 - $m2; - $r = $this->hueToRGB($m1, $m2, $h + 1/3) * 255; + $r = $this->hueToRGB($m1, $m2, $h + 1 / 3) * 255; $g = $this->hueToRGB($m1, $m2, $h) * 255; - $b = $this->hueToRGB($m1, $m2, $h - 1/3) * 255; + $b = $this->hueToRGB($m1, $m2, $h - 1 / 3) * 255; $out = [Type::T_COLOR, $r, $g, $b]; @@ -5235,10 +6798,27 @@ public function toRGB($hue, $saturation, $lightness) // Built in functions - protected static $libCall = ['name', 'args...']; + protected static $libCall = ['function', 'args...']; protected function libCall($args, $kwargs) { - $name = $this->compileStringContent($this->coerceString($this->reduce(array_shift($args), true))); + $functionReference = array_shift($args); + + 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" + . "in Sass 4.0. Use call(function-reference($name)) instead."; + fwrite($this->stderr, "$warning\n\n"); + $functionReference = $this->libGetFunction([$functionReference]); + } + + if ($functionReference === static::$null) { + return static::$null; + } + + if (! in_array($functionReference[0], [Type::T_FUNCTION_REFERENCE, Type::T_FUNCTION])) { + throw $this->error('Function reference expected, got ' . $functionReference[0]); + } + $callArgs = []; // $kwargs['args'] is [Type::T_LIST, ',', [..]] @@ -5252,7 +6832,29 @@ protected function libCall($args, $kwargs) $callArgs[] = [$varname, $arg, false]; } - return $this->reduce([Type::T_FUNCTION_CALL, $name, $callArgs]); + return $this->reduce([Type::T_FUNCTION_CALL, $functionReference, $callArgs]); + } + + + protected static $libGetFunction = [ + ['name'], + ['name', 'css'] + ]; + protected function libGetFunction($args) + { + $name = $this->compileStringContent($this->coerceString(array_shift($args))); + $isCss = false; + + if (count($args)) { + $isCss = array_shift($args); + $isCss = (($isCss === static::$true) ? true : false); + } + + if ($isCss) { + return [Type::T_FUNCTION, $name, [Type::T_LIST, ',', []]]; + } + + return $this->getFunctionReference($name, true); } protected static $libIf = ['condition', 'if-true', 'if-false:']; @@ -5272,11 +6874,8 @@ protected function libIndex($args) { list($list, $value) = $args; - if ($value[0] === Type::T_MAP) { - return static::$null; - } - - if ($list[0] === Type::T_MAP || + if ( + $list[0] === Type::T_MAP || $list[0] === Type::T_STRING || $list[0] === Type::T_KEYWORD || $list[0] === Type::T_INTERPOLATE @@ -5288,8 +6887,25 @@ protected function libIndex($args) return static::$null; } + // Numbers are represented with value objects, for which the PHP equality operator does not + // match the Sass rules (and we cannot overload it). As they are the only type of values + // represented with a value object for now, they require a special case. + if ($value instanceof Number) { + $key = 0; + foreach ($list[2] as $item) { + $key++; + $itemValue = $this->normalizeValue($item); + + if ($itemValue instanceof Number && $value->equals($itemValue)) { + return new Number($key, ''); + } + } + return static::$null; + } + $values = []; + foreach ($list[2] as $item) { $values[] = $this->normalizeValue($item); } @@ -5299,52 +6915,101 @@ protected function libIndex($args) return false === $key ? static::$null : $key + 1; } - protected static $libRgb = ['red', 'green', 'blue']; - protected function libRgb($args) + protected static $libRgb = [ + ['color'], + ['color', 'alpha'], + ['channels'], + ['red', 'green', 'blue'], + ['red', 'green', 'blue', 'alpha'] ]; + protected function libRgb($args, $kwargs, $funcName = 'rgb') { - list($r, $g, $b) = $args; + switch (\count($args)) { + case 1: + if (! $color = $this->coerceColor($args[0], true)) { + $color = [Type::T_STRING, '', [$funcName . '(', $args[0], ')']]; + } + break; - return [Type::T_COLOR, $r[1], $g[1], $b[1]]; - } + case 3: + $color = [Type::T_COLOR, $args[0], $args[1], $args[2]]; - protected static $libRgba = [ - ['color', 'alpha:1'], - ['red', 'green', 'blue', 'alpha:1'] ]; - protected function libRgba($args) - { - if ($color = $this->coerceColor($args[0])) { - $num = isset($args[3]) ? $args[3] : $args[1]; - $alpha = $this->assertNumber($num); - $color[4] = $alpha; + if (! $color = $this->coerceColor($color)) { + $color = [Type::T_STRING, '', [$funcName . '(', $args[0], ', ', $args[1], ', ', $args[2], ')']]; + } + + return $color; + + case 2: + if ($color = $this->coerceColor($args[0], true)) { + $alpha = $this->compileRGBAValue($args[1], true); + + if (is_numeric($alpha)) { + $color[4] = $alpha; + } else { + $color = [Type::T_STRING, '', + [$funcName . '(', $color[1], ', ', $color[2], ', ', $color[3], ', ', $alpha, ')']]; + } + } else { + $color = [Type::T_STRING, '', [$funcName . '(', $args[0], ')']]; + } + break; + + case 4: + default: + $color = [Type::T_COLOR, $args[0], $args[1], $args[2], $args[3]]; - return $color; + if (! $color = $this->coerceColor($color)) { + $color = [Type::T_STRING, '', + [$funcName . '(', $args[0], ', ', $args[1], ', ', $args[2], ', ', $args[3], ')']]; + } + break; } - list($r, $g, $b, $a) = $args; + return $color; + } - return [Type::T_COLOR, $r[1], $g[1], $b[1], $a[1]]; + protected static $libRgba = [ + ['color'], + ['color', 'alpha'], + ['channels'], + ['red', 'green', 'blue'], + ['red', 'green', 'blue', 'alpha'] ]; + protected function libRgba($args, $kwargs) + { + return $this->libRgb($args, $kwargs, 'rgba'); } - // helper function for adjust_color, change_color, and scale_color + /** + * Helper function for adjust_color, change_color, and scale_color + * + * @param array $args + * @param callable $fn + * + * @return array + */ protected function alterColor($args, $fn) { $color = $this->assertColor($args[0]); - foreach ([1, 2, 3, 7] as $i) { - if (isset($args[$i])) { - $val = $this->assertNumber($args[$i]); - $ii = $i === 7 ? 4 : $i; // alpha - $color[$ii] = call_user_func($fn, isset($color[$ii]) ? $color[$ii] : 0, $val, $i); + 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 (! empty($args[4]) || ! empty($args[5]) || ! empty($args[6])) { $hsl = $this->toHSL($color[1], $color[2], $color[3]); - foreach ([4, 5, 6] as $i) { - if (! empty($args[$i])) { - $val = $this->assertNumber($args[$i]); - $hsl[$i - 3] = call_user_func($fn, $hsl[$i - 3], $val, $i); + 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); } } @@ -5425,9 +7090,14 @@ protected function libScaleColor($args) protected function libIeHexStr($args) { $color = $this->coerceColor($args[0]); + + if (\is_null($color)) { + throw $this->error('Error: argument `$color` of `ie-hex-str($color)` must be a color'); + } + $color[4] = isset($color[4]) ? round(255 * $color[4]) : 255; - return sprintf('#%02X%02X%02X%02X', $color[4], $color[1], $color[2], $color[3]); + return [Type::T_STRING, '', [sprintf('#%02X%02X%02X%02X', $color[4], $color[1], $color[2], $color[3])]]; } protected static $libRed = ['color']; @@ -5435,6 +7105,10 @@ protected function libRed($args) { $color = $this->coerceColor($args[0]); + if (\is_null($color)) { + throw $this->error('Error: argument `$color` of `red($color)` must be a color'); + } + return $color[1]; } @@ -5443,6 +7117,10 @@ protected function libGreen($args) { $color = $this->coerceColor($args[0]); + if (\is_null($color)) { + throw $this->error('Error: argument `$color` of `green($color)` must be a color'); + } + return $color[2]; } @@ -5451,6 +7129,10 @@ protected function libBlue($args) { $color = $this->coerceColor($args[0]); + if (\is_null($color)) { + throw $this->error('Error: argument `$color` of `blue($color)` must be a color'); + } + return $color[3]; } @@ -5470,7 +7152,7 @@ protected function libOpacity($args) { $value = $args[0]; - if ($value[0] === Type::T_NUMBER) { + if ($value instanceof Number) { return null; } @@ -5478,7 +7160,10 @@ protected function libOpacity($args) } // mix two colors - protected static $libMix = ['color-1', 'color-2', 'weight:0.5']; + protected static $libMix = [ + ['color1', 'color2', 'weight:0.5'], + ['color-1', 'color-2', 'weight:0.5'] + ]; protected function libMix($args) { list($first, $second, $weight) = $args; @@ -5514,32 +7199,96 @@ protected function libMix($args) return $this->fixColor($new); } - protected static $libHsl = ['hue', 'saturation', 'lightness']; - protected function libHsl($args) + protected static $libHsl = [ + ['channels'], + ['hue', 'saturation', 'lightness'], + ['hue', 'saturation', 'lightness', 'alpha'] ]; + protected function libHsl($args, $kwargs, $funcName = 'hsl') { - list($h, $s, $l) = $args; + $args_to_check = $args; - return $this->toRGB($h[1], $s[1], $l[1]); - } + if (\count($args) == 1) { + if ($args[0][0] !== Type::T_LIST || \count($args[0][2]) < 3 || \count($args[0][2]) > 4) { + return [Type::T_STRING, '', [$funcName . '(', $args[0], ')']]; + } - protected static $libHsla = ['hue', 'saturation', 'lightness', 'alpha']; - protected function libHsla($args) - { - list($h, $s, $l, $a) = $args; + $args = $args[0][2]; + $args_to_check = $kwargs['channels'][2]; + } + + 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]); + $saturation = $this->reduce($args[1]); + $lightness = $this->reduce($args[2]); + $alpha = null; + + if (\count($args) === 4) { + $alpha = $this->compileColorPartValue($args[3], 0, 100, false); + + if (!$hue instanceof Number || !$saturation instanceof Number || ! $lightness instanceof Number || ! is_numeric($alpha)) { + return [Type::T_STRING, '', + [$funcName . '(', $args[0], ', ', $args[1], ', ', $args[2], ', ', $args[3], ')']]; + } + } else { + if (!$hue instanceof Number || !$saturation instanceof Number || ! $lightness instanceof Number) { + return [Type::T_STRING, '', [$funcName . '(', $args[0], ', ', $args[1], ', ', $args[2], ')']]; + } + } + + $hueValue = $hue->getDimension() % 360; + + while ($hueValue < 0) { + $hueValue += 360; + } - $color = $this->toRGB($h[1], $s[1], $l[1]); - $color[4] = $a[1]; + $color = $this->toRGB($hueValue, max(0, min($saturation->getDimension(), 100)), max(0, min($lightness->getDimension(), 100))); + + if (! \is_null($alpha)) { + $color[4] = $alpha; + } return $color; } + protected static $libHsla = [ + ['channels'], + ['hue', 'saturation', 'lightness'], + ['hue', 'saturation', 'lightness', 'alpha']]; + protected function libHsla($args, $kwargs) + { + return $this->libHsl($args, $kwargs, 'hsla'); + } + protected static $libHue = ['color']; protected function libHue($args) { $color = $this->assertColor($args[0]); $hsl = $this->toHSL($color[1], $color[2], $color[3]); - return new Node\Number($hsl[1], 'deg'); + return new Number($hsl[1], 'deg'); } protected static $libSaturation = ['color']; @@ -5548,7 +7297,7 @@ protected function libSaturation($args) $color = $this->assertColor($args[0]); $hsl = $this->toHSL($color[1], $color[2], $color[3]); - return new Node\Number($hsl[2], '%'); + return new Number($hsl[2], '%'); } protected static $libLightness = ['color']; @@ -5557,7 +7306,7 @@ protected function libLightness($args) $color = $this->assertColor($args[0]); $hsl = $this->toHSL($color[1], $color[2], $color[3]); - return new Node\Number($hsl[3], '%'); + return new Number($hsl[3], '%'); } protected function adjustHsl($color, $idx, $amount) @@ -5577,7 +7326,7 @@ protected function adjustHsl($color, $idx, $amount) protected function libAdjustHue($args) { $color = $this->assertColor($args[0]); - $degrees = $this->assertNumber($args[1]); + $degrees = $this->assertNumber($args[1])->getDimension(); return $this->adjustHsl($color, 1, $degrees); } @@ -5600,15 +7349,20 @@ protected function libDarken($args) return $this->adjustHsl($color, 3, -$amount); } - protected static $libSaturate = [['color', 'amount'], ['number']]; + protected static $libSaturate = [['color', 'amount'], ['amount']]; protected function libSaturate($args) { $value = $args[0]; - if ($value[0] === Type::T_NUMBER) { + if ($value instanceof Number) { 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]); @@ -5629,7 +7383,7 @@ protected function libGrayscale($args) { $value = $args[0]; - if ($value[0] === Type::T_NUMBER) { + if ($value instanceof Number) { return null; } @@ -5642,21 +7396,32 @@ protected function libComplement($args) return $this->adjustHsl($this->assertColor($args[0]), 1, 180); } - protected static $libInvert = ['color']; + protected static $libInvert = ['color', 'weight:1']; protected function libInvert($args) { - $value = $args[0]; + list($value, $weight) = $args; + + if (! isset($weight)) { + $weight = 1; + } else { + $weight = $this->coercePercent($weight); + } - if ($value[0] === Type::T_NUMBER) { + if ($value instanceof Number) { return null; } $color = $this->assertColor($value); - $color[1] = 255 - $color[1]; - $color[2] = 255 - $color[2]; - $color[3] = 255 - $color[3]; + $inverted = $color; + $inverted[1] = 255 - $inverted[1]; + $inverted[2] = 255 - $inverted[2]; + $inverted[3] = 255 - $inverted[3]; - return $color; + if ($weight < 1) { + return $this->libMix([$inverted, $color, new Number($weight, '')]); + } + + return $inverted; } // increases opacity by amount @@ -5715,131 +7480,120 @@ protected function libQuote($args) $value = $args[0]; if ($value[0] === Type::T_STRING && ! empty($value[1])) { + $value[1] = '"'; return $value; } return [Type::T_STRING, '"', [$value]]; } - protected static $libPercentage = ['value']; + protected static $libPercentage = ['number']; protected function libPercentage($args) { - return new Node\Number($this->coercePercent($args[0]) * 100, '%'); + $num = $this->assertNumber($args[0], 'number'); + $num->assertNoUnits('number'); + + return new Number($num->getDimension() * 100, '%'); } - protected static $libRound = ['value']; + protected static $libRound = ['number']; protected function libRound($args) { - $num = $args[0]; + $num = $this->assertNumber($args[0], 'number'); - return new Node\Number(round($num[1]), $num[2]); + return new Number(round($num->getDimension()), $num->getNumeratorUnits(), $num->getDenominatorUnits()); } - protected static $libFloor = ['value']; + protected static $libFloor = ['number']; protected function libFloor($args) { - $num = $args[0]; + $num = $this->assertNumber($args[0], 'number'); - return new Node\Number(floor($num[1]), $num[2]); + return new Number(floor($num->getDimension()), $num->getNumeratorUnits(), $num->getDenominatorUnits()); } - protected static $libCeil = ['value']; + protected static $libCeil = ['number']; protected function libCeil($args) { - $num = $args[0]; + $num = $this->assertNumber($args[0], 'number'); - return new Node\Number(ceil($num[1]), $num[2]); + return new Number(ceil($num->getDimension()), $num->getNumeratorUnits(), $num->getDenominatorUnits()); } - protected static $libAbs = ['value']; + protected static $libAbs = ['number']; protected function libAbs($args) { - $num = $args[0]; + $num = $this->assertNumber($args[0], 'number'); - return new Node\Number(abs($num[1]), $num[2]); + return new Number(abs($num->getDimension()), $num->getNumeratorUnits(), $num->getDenominatorUnits()); } protected function libMin($args) { - $numbers = $this->getNormalizedNumbers($args); + /** + * @var Number|null + */ $min = null; - foreach ($numbers as $key => $number) { - if (null === $min || $number[1] <= $min[1]) { - $min = [$key, $number[1]]; + foreach ($args as $arg) { + $number = $this->assertNumber($arg); + + if (\is_null($min) || $min->greaterThan($number)) { + $min = $number; } } - return $args[$min[0]]; - } - - protected function libMax($args) - { - $numbers = $this->getNormalizedNumbers($args); - $max = null; - - foreach ($numbers as $key => $number) { - if (null === $max || $number[1] >= $max[1]) { - $max = [$key, $number[1]]; - } + if (!\is_null($min)) { + return $min; } - return $args[$max[0]]; + throw $this->error('At least one argument must be passed.'); } - /** - * Helper to normalize args containing numbers - * - * @param array $args - * - * @return array - */ - protected function getNormalizedNumbers($args) + protected function libMax($args) { - $unit = null; - $originalUnit = null; - $numbers = []; - - foreach ($args as $key => $item) { - if ($item[0] !== Type::T_NUMBER) { - $this->throwError('%s is not a number', $item[0]); - break; - } + /** + * @var Number|null + */ + $max = null; - $number = $item->normalize(); + foreach ($args as $arg) { + $number = $this->assertNumber($arg); - if (null === $unit) { - $unit = $number[2]; - $originalUnit = $item->unitStr(); - } elseif ($number[1] && $unit !== $number[2]) { - $this->throwError('Incompatible units: "%s" and "%s".', $originalUnit, $item->unitStr()); - break; + if (\is_null($max) || $max->lessThan($number)) { + $max = $number; } + } - $numbers[$key] = $number; + if (!\is_null($max)) { + return $max; } - return $numbers; + throw $this->error('At least one argument must be passed.'); } protected static $libLength = ['list']; protected function libLength($args) { - $list = $this->coerceList($args[0]); + $list = $this->coerceList($args[0], ',', true); - return count($list[2]); + return \count($list[2]); } //protected static $libListSeparator = ['list...']; protected function libListSeparator($args) { - if (count($args) > 1) { + if (\count($args) > 1) { return 'comma'; } + if (! \in_array($args[0][0], [Type::T_LIST, Type::T_MAP])) { + return 'space'; + } + $list = $this->coerceList($args[0]); - if (count($list[2]) <= 1) { + if (\count($list[2]) <= 1 && empty($list['enclosing'])) { return 'space'; } @@ -5853,13 +7607,13 @@ protected function libListSeparator($args) protected static $libNth = ['list', 'n']; protected function libNth($args) { - $list = $this->coerceList($args[0]); - $n = $this->assertNumber($args[1]); + $list = $this->coerceList($args[0], ',', false); + $n = $this->assertNumber($args[1])->getDimension(); if ($n > 0) { $n--; } elseif ($n < 0) { - $n += count($list[2]); + $n += \count($list[2]); } return isset($list[2][$n]) ? $list[2][$n] : static::$defaultValue; @@ -5869,18 +7623,16 @@ protected function libNth($args) protected function libSetNth($args) { $list = $this->coerceList($args[0]); - $n = $this->assertNumber($args[1]); + $n = $this->assertNumber($args[1])->getDimension(); if ($n > 0) { $n--; } elseif ($n < 0) { - $n += count($list[2]); + $n += \count($list[2]); } if (! isset($list[2][$n])) { - $this->throwError('Invalid argument for "n"'); - - return null; + throw $this->error('Invalid argument for "n"'); } $list[2][$n] = $args[2]; @@ -5894,9 +7646,10 @@ protected function libMapGet($args) $map = $this->assertMap($args[0]); $key = $args[1]; - if (! is_null($key)) { + if (! \is_null($key)) { $key = $this->compileStringContent($this->coerceString($key)); - for ($i = count($map[1]) - 1; $i >= 0; $i--) { + + for ($i = \count($map[1]) - 1; $i >= 0; $i--) { if ($key === $this->compileStringContent($this->coerceString($map[1][$i]))) { return $map[2][$i]; } @@ -5924,14 +7677,20 @@ protected function libMapValues($args) return [Type::T_LIST, ',', $values]; } - protected static $libMapRemove = ['map', 'key']; + protected static $libMapRemove = ['map', 'key...']; protected function libMapRemove($args) { $map = $this->assertMap($args[0]); - $key = $this->compileStringContent($this->coerceString($args[1])); + $keyList = $this->assertList($args[1]); - for ($i = count($map[1]) - 1; $i >= 0; $i--) { - if ($key === $this->compileStringContent($this->coerceString($map[1][$i]))) { + $keys = []; + + foreach ($keyList[2] as $key) { + $keys[] = $this->compileStringContent($this->coerceString($key)); + } + + for ($i = \count($map[1]) - 1; $i >= 0; $i--) { + if (in_array($this->compileStringContent($this->coerceString($map[1][$i])), $keys)) { array_splice($map[1], $i, 1); array_splice($map[2], $i, 1); } @@ -5946,7 +7705,7 @@ protected function libMapHasKey($args) $map = $this->assertMap($args[0]); $key = $this->compileStringContent($this->coerceString($args[1])); - for ($i = count($map[1]) - 1; $i >= 0; $i--) { + for ($i = \count($map[1]) - 1; $i >= 0; $i--) { if ($key === $this->compileStringContent($this->coerceString($map[1][$i]))) { return true; } @@ -5955,7 +7714,10 @@ protected function libMapHasKey($args) return false; } - protected static $libMapMerge = ['map-1', 'map-2']; + protected static $libMapMerge = [ + ['map1', 'map2'], + ['map-1', 'map-2'] + ]; protected function libMapMerge($args) { $map1 = $this->assertMap($args[0]); @@ -5994,6 +7756,26 @@ protected function libKeywords($args) return [Type::T_MAP, $keys, $values]; } + protected static $libIsBracketed = ['list']; + protected function libIsBracketed($args) + { + $list = $args[0]; + $this->coerceList($list, ' '); + + if (! empty($list['enclosing']) && $list['enclosing'] === 'bracket') { + return true; + } + + return false; + } + + /** + * @param array $list1 + * @param array|Number|null $sep + * + * @return string + * @throws CompilerException + */ protected function listSeparatorForJoin($list1, $sep) { if (! isset($sep)) { @@ -6012,16 +7794,51 @@ protected function listSeparatorForJoin($list1, $sep) } } - protected static $libJoin = ['list1', 'list2', 'separator:null']; + protected static $libJoin = ['list1', 'list2', 'separator:null', 'bracketed:auto']; protected function libJoin($args) { - list($list1, $list2, $sep) = $args; + list($list1, $list2, $sep, $bracketed) = $args; + + $list1 = $this->coerceList($list1, ' ', true); + $list2 = $this->coerceList($list2, ' ', true); + $sep = $this->listSeparatorForJoin($list1, $sep); + + if ($bracketed === static::$true) { + $bracketed = true; + } elseif ($bracketed === static::$false) { + $bracketed = false; + } elseif ($bracketed === [Type::T_KEYWORD, 'auto']) { + $bracketed = 'auto'; + } elseif ($bracketed === static::$null) { + $bracketed = false; + } else { + $bracketed = $this->compileValue($bracketed); + $bracketed = ! ! $bracketed; + + if ($bracketed === true) { + $bracketed = true; + } + } + + if ($bracketed === 'auto') { + $bracketed = false; + + if (! empty($list1['enclosing']) && $list1['enclosing'] === 'bracket') { + $bracketed = true; + } + } + + $res = [Type::T_LIST, $sep, array_merge($list1[2], $list2[2])]; + + if (isset($list1['enclosing'])) { + $res['enlcosing'] = $list1['enclosing']; + } - $list1 = $this->coerceList($list1, ' '); - $list2 = $this->coerceList($list2, ' '); - $sep = $this->listSeparatorForJoin($list1, $sep); + if ($bracketed) { + $res['enclosing'] = 'bracket'; + } - return [Type::T_LIST, $sep, array_merge($list1[2], $list2[2])]; + return $res; } protected static $libAppend = ['list', 'val', 'separator:null']; @@ -6029,36 +7846,48 @@ protected function libAppend($args) { list($list1, $value, $sep) = $args; - $list1 = $this->coerceList($list1, ' '); - $sep = $this->listSeparatorForJoin($list1, $sep); + $list1 = $this->coerceList($list1, ' ', true); + $sep = $this->listSeparatorForJoin($list1, $sep); + $res = [Type::T_LIST, $sep, array_merge($list1[2], [$value])]; + + if (isset($list1['enclosing'])) { + $res['enclosing'] = $list1['enclosing']; + } - return [Type::T_LIST, $sep, array_merge($list1[2], [$value])]; + return $res; } protected function libZip($args) { - foreach ($args as $arg) { - $this->assertList($arg); + foreach ($args as $key => $arg) { + $args[$key] = $this->coerceList($arg); } $lists = []; $firstList = array_shift($args); - foreach ($firstList[2] as $key => $item) { - $list = [Type::T_LIST, '', [$item]]; + $result = [Type::T_LIST, ',', $lists]; + if (! \is_null($firstList)) { + foreach ($firstList[2] as $key => $item) { + $list = [Type::T_LIST, '', [$item]]; - foreach ($args as $arg) { - if (isset($arg[2][$key])) { - $list[2][] = $arg[2][$key]; - } else { - break 2; + foreach ($args as $arg) { + if (isset($arg[2][$key])) { + $list[2][] = $arg[2][$key]; + } else { + break 2; + } } + + $lists[] = $list; } - $lists[] = $list; + $result[2] = $lists; + } else { + $result['enclosing'] = 'parent'; } - return [Type::T_LIST, ',', $lists]; + return $result; } protected static $libTypeOf = ['value']; @@ -6080,6 +7909,9 @@ protected function libTypeOf($args) case Type::T_FUNCTION: return 'string'; + case Type::T_FUNCTION_REFERENCE: + return 'function'; + case Type::T_LIST: if (isset($value[3]) && $value[3]) { return 'arglist'; @@ -6096,7 +7928,7 @@ protected function libUnit($args) { $num = $args[0]; - if ($num[0] === Type::T_NUMBER) { + if ($num instanceof Number) { return [Type::T_STRING, '"', [$num->unitStr()]]; } @@ -6108,54 +7940,67 @@ protected function libUnitless($args) { $value = $args[0]; - return $value[0] === Type::T_NUMBER && $value->unitless(); + return $value instanceof Number && $value->unitless(); } - protected static $libComparable = ['number-1', 'number-2']; + protected static $libComparable = [ + ['number1', 'number2'], + ['number-1', 'number-2'] + ]; protected function libComparable($args) { list($number1, $number2) = $args; - if (! isset($number1[0]) || $number1[0] !== Type::T_NUMBER || - ! isset($number2[0]) || $number2[0] !== Type::T_NUMBER + if ( + ! $number1 instanceof Number || + ! $number2 instanceof Number ) { - $this->throwError('Invalid argument(s) for "comparable"'); - - return null; + throw $this->error('Invalid argument(s) for "comparable"'); } - $number1 = $number1->normalize(); - $number2 = $number2->normalize(); - - return $number1[2] === $number2[2] || $number1->unitless() || $number2->unitless(); + return $number1->isComparableTo($number2); } protected static $libStrIndex = ['string', 'substring']; protected function libStrIndex($args) { - $string = $this->coerceString($args[0]); + $string = $this->assertString($args[0], 'string'); $stringContent = $this->compileStringContent($string); - $substring = $this->coerceString($args[1]); + $substring = $this->assertString($args[1], 'substring'); $substringContent = $this->compileStringContent($substring); - $result = strpos($stringContent, $substringContent); + if (! \strlen($substringContent)) { + $result = 0; + } else { + $result = Util::mbStrpos($stringContent, $substringContent); + } - return $result === false ? static::$null : new Node\Number($result + 1, ''); + return $result === false ? static::$null : new Number($result + 1, ''); } protected static $libStrInsert = ['string', 'insert', 'index']; protected function libStrInsert($args) { - $string = $this->coerceString($args[0]); + $string = $this->assertString($args[0], 'string'); $stringContent = $this->compileStringContent($string); - $insert = $this->coerceString($args[1]); + $insert = $this->assertString($args[1], 'insert'); $insertContent = $this->compileStringContent($insert); - list(, $index) = $args[2]; + $index = $this->assertInteger($args[2], 'index'); + if ($index > 0) { + $index = $index - 1; + } + if ($index < 0) { + $index = Util::mbStrlen($stringContent) + 1 + $index; + } - $string[2] = [substr_replace($stringContent, $insertContent, $index - 1, 0)]; + $string[2] = [ + Util::mbSubstr($stringContent, 0, $index), + $insertContent, + Util::mbSubstr($stringContent, $index) + ]; return $string; } @@ -6163,10 +8008,10 @@ protected function libStrInsert($args) protected static $libStrLength = ['string']; protected function libStrLength($args) { - $string = $this->coerceString($args[0]); + $string = $this->assertString($args[0], 'string'); $stringContent = $this->compileStringContent($string); - return new Node\Number(strlen($stringContent), ''); + return new Number(Util::mbStrlen($stringContent), ''); } protected static $libStrSlice = ['string', 'start-at', 'end-at:-1']; @@ -6201,7 +8046,7 @@ protected function libToLowerCase($args) $string = $this->coerceString($args[0]); $stringContent = $this->compileStringContent($string); - $string[2] = [function_exists('mb_strtolower') ? mb_strtolower($stringContent) : strtolower($stringContent)]; + $string[2] = [$this->stringTransformAsciiOnly($stringContent, 'strtolower')]; return $string; } @@ -6212,11 +8057,38 @@ protected function libToUpperCase($args) $string = $this->coerceString($args[0]); $stringContent = $this->compileStringContent($string); - $string[2] = [function_exists('mb_strtoupper') ? mb_strtoupper($stringContent) : strtoupper($stringContent)]; + $string[2] = [$this->stringTransformAsciiOnly($stringContent, 'strtoupper')]; return $string; } + /** + * Apply a filter on a string content, only on ascii chars + * let extended chars untouched + * + * @param string $stringContent + * @param callable $filter + * @return string + */ + protected function stringTransformAsciiOnly($stringContent, $filter) + { + $mblength = Util::mbStrlen($stringContent); + if ($mblength === strlen($stringContent)) { + return $filter($stringContent); + } + $filteredString = ""; + for ($i = 0; $i < $mblength; $i++) { + $char = Util::mbSubstr($stringContent, $i, 1); + if (strlen($char) > 1) { + $filteredString .= $char; + } else { + $filteredString .= $filter($char); + } + } + + return $filteredString; + } + protected static $libFeatureExists = ['feature']; protected function libFeatureExists($args) { @@ -6224,7 +8096,7 @@ protected function libFeatureExists($args) $name = $this->compileStringContent($string); return $this->toBool( - array_key_exists($name, $this->registeredFeatures) ? $this->registeredFeatures[$name] : false + \array_key_exists($name, $this->registeredFeatures) ? $this->registeredFeatures[$name] : false ); } @@ -6248,7 +8120,7 @@ protected function libFunctionExists($args) // built-in functions $f = $this->getBuiltinFunction($name); - return $this->toBool(is_callable($f)); + return $this->toBool(\is_callable($f)); } protected static $libGlobalVariableExists = ['name']; @@ -6292,22 +8164,25 @@ protected function libCounter($args) return [Type::T_STRING, '', ['counter(' . implode(',', $list) . ')']]; } - protected static $libRandom = ['limit']; + protected static $libRandom = ['limit:null']; protected function libRandom($args) { - if (isset($args[0])) { - $n = $this->assertNumber($args[0]); + if (isset($args[0]) & $args[0] !== static::$null) { + $n = $this->assertNumber($args[0])->getDimension(); if ($n < 1) { - $this->throwError("limit must be greater than or equal to 1"); + throw $this->error("\$limit must be greater than or equal to 1"); + } - return null; + if (round($n - \intval($n), Number::PRECISION) > 0) { + throw $this->error("Expected \$limit to be an integer but got $n for `random`"); } - return new Node\Number(mt_rand(1, $n), ''); + return new Number(mt_rand(1, \intval($n)), ''); } - return new Node\Number(mt_rand(1, mt_getrandmax()), ''); + $max = mt_getrandmax(); + return new Number(mt_rand(0, $max - 1) / $max, ''); } protected function libUniqueId() @@ -6315,7 +8190,9 @@ protected function libUniqueId() static $id; if (! isset($id)) { - $id = mt_rand(0, pow(36, 8)); + $id = PHP_INT_SIZE === 4 + ? mt_rand(0, pow(36, 5)) . str_pad(mt_rand(0, pow(36, 5)) % 10000000, 7, '0', STR_PAD_LEFT) + : mt_rand(0, pow(36, 8)); } $id += mt_rand(0, 10) + 1; @@ -6323,14 +8200,47 @@ protected function libUniqueId() return [Type::T_STRING, '', ['u' . str_pad(base_convert($id, 10, 36), 8, '0', STR_PAD_LEFT)]]; } + protected function inspectFormatValue($value, $force_enclosing_display = false) + { + if ($value === static::$null) { + $value = [Type::T_KEYWORD, 'null']; + } + + $stringValue = [$value]; + + if ($value[0] === Type::T_LIST) { + if (end($value[2]) === static::$null) { + array_pop($value[2]); + $value[2][] = [Type::T_STRING, '', ['']]; + $force_enclosing_display = true; + } + + if ( + ! empty($value['enclosing']) && + ($force_enclosing_display || + ($value['enclosing'] === 'bracket') || + ! \count($value[2])) + ) { + $value['enclosing'] = 'forced_' . $value['enclosing']; + $force_enclosing_display = true; + } + + foreach ($value[2] as $k => $listelement) { + $value[2][$k] = $this->inspectFormatValue($listelement, $force_enclosing_display); + } + + $stringValue = [$value]; + } + + return [Type::T_STRING, '', $stringValue]; + } + protected static $libInspect = ['value']; protected function libInspect($args) { - if ($args[0] === static::$null) { - return [Type::T_KEYWORD, 'null']; - } + $value = $args[0]; - return $args[0]; + return $this->inspectFormatValue($value); } /** @@ -6340,27 +8250,68 @@ protected function libInspect($args) * * @return array|boolean */ - protected function getSelectorArg($arg) + protected function getSelectorArg($arg, $varname = null, $allowParent = false) { static $parser = null; - if (is_null($parser)) { + if (\is_null($parser)) { $parser = $this->parserFactory(__METHOD__); } + 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"); + } + $arg = $this->libUnquote([$arg]); $arg = $this->compileValue($arg); $parsedSelector = []; - if ($parser->parseSelector($arg, $parsedSelector)) { + if ($parser->parseSelector($arg, $parsedSelector, true)) { $selector = $this->evalSelectors($parsedSelector); $gluedSelector = $this->glueFunctionSelectors($selector); + if (! $allowParent) { + 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."); + } + } + } + } + return $gluedSelector; } - return false; + $var_display = ($varname ? ' $' . $varname . ':' : ''); + throw $this->error("Error:{$var_display} expected more input, invalid selector."); + } + + /** + * Check variable type for getSelectorArg() function + * @param array $arg + * @param int $maxDepth + * @return bool + */ + protected function checkSelectorArgType($arg, $maxDepth = 2) + { + if ($arg[0] === Type::T_LIST && $maxDepth > 0) { + foreach ($arg[2] as $elt) { + if (! $this->checkSelectorArgType($elt, $maxDepth - 1)) { + return false; + } + } + return true; + } + if (!in_array($arg[0], [Type::T_STRING, Type::T_KEYWORD])) { + return false; + } + return true; } /** @@ -6382,8 +8333,8 @@ protected function libIsSuperselector($args) { list($super, $sub) = $args; - $super = $this->getSelectorArg($super); - $sub = $this->getSelectorArg($sub); + $super = $this->getSelectorArg($super, 'super'); + $sub = $this->getSelectorArg($sub, 'sub'); return $this->isSuperSelector($super, $sub); } @@ -6399,12 +8350,30 @@ protected function libIsSuperselector($args) protected function isSuperSelector($super, $sub) { // one and only one selector for each arg - if (! $super || count($super) !== 1) { - $this->throwError("Invalid super selector for isSuperSelector()"); + if (! $super) { + throw $this->error('Invalid super selector for isSuperSelector()'); + } + + if (! $sub) { + throw $this->error('Invalid sub selector for isSuperSelector()'); + } + + if (count($sub) > 1) { + foreach ($sub as $s) { + if (! $this->isSuperSelector($super, [$s])) { + return false; + } + } + return true; } - if (! $sub || count($sub) !== 1) { - $this->throwError("Invalid sub selector for isSuperSelector()"); + if (count($super) > 1) { + foreach ($super as $s) { + if ($this->isSuperSelector([$s], $sub)) { + return true; + } + } + return false; } $super = reset($super); @@ -6431,7 +8400,7 @@ function ($value, $key) use (&$compound) { $nextMustMatch = true; $i++; } else { - while ($i < count($sub) && ! $this->isSuperPart($node, $sub[$i])) { + while ($i < \count($sub) && ! $this->isSuperPart($node, $sub[$i])) { if ($nextMustMatch) { return false; } @@ -6439,7 +8408,7 @@ function ($value, $key) use (&$compound) { $i++; } - if ($i >= count($sub)) { + if ($i >= \count($sub)) { return false; } @@ -6464,11 +8433,11 @@ protected function isSuperPart($superParts, $subParts) $i = 0; foreach ($superParts as $superPart) { - while ($i < count($subParts) && $subParts[$i] !== $superPart) { + while ($i < \count($subParts) && $subParts[$i] !== $superPart) { $i++; } - if ($i >= count($subParts)) { + if ($i >= \count($subParts)) { return false; } @@ -6484,11 +8453,15 @@ protected function libSelectorAppend($args) // get the selector... list $args = reset($args); $args = $args[2]; - if (count($args) < 1) { - $this->throwError("selector-append() needs at least 1 argument"); + + if (\count($args) < 1) { + throw $this->error('selector-append() needs at least 1 argument'); } - $selectors = array_map([$this, 'getSelectorArg'], $args); + $selectors = []; + foreach ($args as $arg) { + $selectors[] = $this->getSelectorArg($arg, 'selector'); + } return $this->formatOutputSelector($this->selectorAppend($selectors)); } @@ -6507,14 +8480,14 @@ protected function selectorAppend($selectors) $lastSelectors = array_pop($selectors); if (! $lastSelectors) { - $this->throwError("Invalid selector list in selector-append()"); + throw $this->error('Invalid selector list in selector-append()'); } - while (count($selectors)) { + while (\count($selectors)) { $previousSelectors = array_pop($selectors); if (! $previousSelectors) { - $this->throwError("Invalid selector list in selector-append()"); + throw $this->error('Invalid selector list in selector-append()'); } // do the trick, happening $lastSelector to $previousSelector @@ -6544,17 +8517,20 @@ protected function selectorAppend($selectors) return $lastSelectors; } - protected static $libSelectorExtend = ['selectors', 'extendee', 'extender']; + protected static $libSelectorExtend = [ + ['selector', 'extendee', 'extender'], + ['selectors', 'extendee', 'extender'] + ]; protected function libSelectorExtend($args) { list($selectors, $extendee, $extender) = $args; - $selectors = $this->getSelectorArg($selectors); - $extendee = $this->getSelectorArg($extendee); - $extender = $this->getSelectorArg($extender); + $selectors = $this->getSelectorArg($selectors, 'selector'); + $extendee = $this->getSelectorArg($extendee, 'extendee'); + $extender = $this->getSelectorArg($extender, 'extender'); if (! $selectors || ! $extendee || ! $extender) { - $this->throwError("selector-extend() invalid arguments"); + throw $this->error('selector-extend() invalid arguments'); } $extended = $this->extendOrReplaceSelectors($selectors, $extendee, $extender); @@ -6562,17 +8538,20 @@ protected function libSelectorExtend($args) return $this->formatOutputSelector($extended); } - protected static $libSelectorReplace = ['selectors', 'original', 'replacement']; + protected static $libSelectorReplace = [ + ['selector', 'original', 'replacement'], + ['selectors', 'original', 'replacement'] + ]; protected function libSelectorReplace($args) { list($selectors, $original, $replacement) = $args; - $selectors = $this->getSelectorArg($selectors); - $original = $this->getSelectorArg($original); - $replacement = $this->getSelectorArg($replacement); + $selectors = $this->getSelectorArg($selectors, 'selector'); + $original = $this->getSelectorArg($original, 'original'); + $replacement = $this->getSelectorArg($replacement, 'replacement'); if (! $selectors || ! $original || ! $replacement) { - $this->throwError("selector-replace() invalid arguments"); + throw $this->error('selector-replace() invalid arguments'); } $replaced = $this->extendOrReplaceSelectors($selectors, $original, $replacement, true); @@ -6611,12 +8590,12 @@ protected function extendOrReplaceSelectors($selectors, $extendee, $extender, $r $extended[] = $selector; } - $n = count($extended); + $n = \count($extended); $this->matchExtends($selector, $extended); // if didnt match, keep the original selector if we are in a replace operation - if ($replace and count($extended) === $n) { + if ($replace && \count($extended) === $n) { $extended[] = $selector; } } @@ -6633,13 +8612,18 @@ protected function libSelectorNest($args) // get the selector... list $args = reset($args); $args = $args[2]; - if (count($args) < 1) { - $this->throwError("selector-nest() needs at least 1 argument"); + + if (\count($args) < 1) { + throw $this->error('selector-nest() needs at least 1 argument'); } - $selectorsMap = array_map([$this, 'getSelectorArg'], $args); + $selectorsMap = []; + foreach ($args as $arg) { + $selectorsMap[] = $this->getSelectorArg($arg, 'selector', true); + } $envs = []; + foreach ($selectorsMap as $selectors) { $env = new Environment(); $env->selectors = $selectors; @@ -6647,18 +8631,21 @@ protected function libSelectorNest($args) $envs[] = $env; } - $envs = array_reverse($envs); - $env = $this->extractEnv($envs); + $envs = array_reverse($envs); + $env = $this->extractEnv($envs); $outputSelectors = $this->multiplySelectors($env); return $this->formatOutputSelector($outputSelectors); } - protected static $libSelectorParse = ['selectors']; + protected static $libSelectorParse = [ + ['selector'], + ['selectors'] + ]; protected function libSelectorParse($args) { $selectors = reset($args); - $selectors = $this->getSelectorArg($selectors); + $selectors = $this->getSelectorArg($selectors, 'selector'); return $this->formatOutputSelector($selectors); } @@ -6668,11 +8655,11 @@ protected function libSelectorUnify($args) { list($selectors1, $selectors2) = $args; - $selectors1 = $this->getSelectorArg($selectors1); - $selectors2 = $this->getSelectorArg($selectors2); + $selectors1 = $this->getSelectorArg($selectors1, 'selectors1'); + $selectors2 = $this->getSelectorArg($selectors2, 'selectors2'); if (! $selectors1 || ! $selectors2) { - $this->throwError("selector-unify() invalid arguments"); + throw $this->error('selector-unify() invalid arguments'); } // only consider the first compound of each @@ -6691,22 +8678,23 @@ protected function libSelectorUnify($args) * * @param array $compound1 * @param array $compound2 + * * @return array|mixed */ protected function unifyCompoundSelectors($compound1, $compound2) { - if (! count($compound1)) { + if (! \count($compound1)) { return $compound2; } - if (! count($compound2)) { + if (! \count($compound2)) { return $compound1; } // check that last part are compatible $lastPart1 = array_pop($compound1); $lastPart2 = array_pop($compound2); - $last = $this->mergeParts($lastPart1, $lastPart2); + $last = $this->mergeParts($lastPart1, $lastPart2); if (! $last) { return [[]]; @@ -6716,7 +8704,7 @@ protected function unifyCompoundSelectors($compound1, $compound2) $unifiedSelectors = [$unifiedCompound]; // do the rest - while (count($compound1) || count($compound2)) { + while (\count($compound1) || \count($compound2)) { $part1 = end($compound1); $part2 = end($compound2); @@ -6729,6 +8717,7 @@ protected function unifyCompoundSelectors($compound1, $compound2) $c = $this->mergeParts($part1, $part2); $unifiedSelectors = $this->prependSelectors($unifiedSelectors, [$c]); + $part1 = $part2 = null; array_pop($compound1); @@ -6743,6 +8732,7 @@ protected function unifyCompoundSelectors($compound1, $compound2) $c = $this->mergeParts($part2, $part1); $unifiedSelectors = $this->prependSelectors($unifiedSelectors, [$c]); + $part1 = $part2 = null; array_pop($compound2); @@ -6754,9 +8744,9 @@ protected function unifyCompoundSelectors($compound1, $compound2) array_pop($compound1); array_pop($compound2); - $s = $this->prependSelectors($unifiedSelectors, [$part2]); + $s = $this->prependSelectors($unifiedSelectors, [$part2]); $new = array_merge($new, $this->prependSelectors($s, [$part1])); - $s = $this->prependSelectors($unifiedSelectors, [$part1]); + $s = $this->prependSelectors($unifiedSelectors, [$part1]); $new = array_merge($new, $this->prependSelectors($s, [$part2])); } elseif ($part1) { array_pop($compound1); @@ -6805,16 +8795,16 @@ protected function prependSelectors($selectors, $parts) * @param array $part * @param array $compound * - * @return array|boolean + * @return array|false */ protected function matchPartInCompound($part, $compound) { $partTag = $this->findTagName($part); - $before = $compound; - $after = []; + $before = $compound; + $after = []; // try to find a match by tag name first - while (count($before)) { + while (\count($before)) { $p = array_pop($before); if ($partTag && $partTag !== '*' && $partTag == $this->findTagName($p)) { @@ -6828,11 +8818,11 @@ protected function matchPartInCompound($part, $compound) $before = $compound; $after = []; - while (count($before)) { + while (\count($before)) { $p = array_pop($before); if ($this->checkCompatibleTags($partTag, $this->findTagName($p))) { - if (count(array_intersect($part, $p))) { + if (\count(array_intersect($part, $p))) { return [$before, $p, $after]; } } @@ -6857,7 +8847,7 @@ protected function mergeParts($parts1, $parts2) { $tag1 = $this->findTagName($parts1); $tag2 = $this->findTagName($parts2); - $tag = $this->checkCompatibleTags($tag1, $tag2); + $tag = $this->checkCompatibleTags($tag1, $tag2); // not compatible tags if ($tag === false) { @@ -6900,7 +8890,7 @@ protected function mergeParts($parts1, $parts2) * @param string $tag1 * @param string $tag2 * - * @return array|boolean + * @return array|false */ protected function checkCompatibleTags($tag1, $tag2) { @@ -6908,12 +8898,12 @@ protected function checkCompatibleTags($tag1, $tag2) $tags = array_unique($tags); $tags = array_filter($tags); - if (count($tags)>1) { + if (\count($tags) > 1) { $tags = array_diff($tags, ['*']); } // not compatible nodes - if (count($tags)>1) { + if (\count($tags) > 1) { return false; } @@ -6942,7 +8932,7 @@ protected function findTagName($parts) protected function libSimpleSelectors($args) { $selector = reset($args); - $selector = $this->getSelectorArg($selector); + $selector = $this->getSelectorArg($selector, 'selector'); // remove selectors list layer, keeping the first one $selector = reset($selector); @@ -6958,4 +8948,23 @@ protected function libSimpleSelectors($args) return [Type::T_LIST, ',', $listParts]; } + + protected static $libScssphpGlob = ['pattern']; + protected function libScssphpGlob($args) + { + $string = $this->coerceString($args[0]); + $pattern = $this->compileStringContent($string); + $matches = glob($pattern); + $listParts = []; + + foreach ($matches as $match) { + if (! is_file($match)) { + continue; + } + + $listParts[] = [Type::T_STRING, '"', [$match]]; + } + + return [Type::T_LIST, ',', $listParts]; + } } diff --git a/scssphp/scssphp/src/Compiler/Environment.php b/scssphp/scssphp/src/Compiler/Environment.php index 03eb86a5d..dc2f86c1f 100644 --- a/scssphp/scssphp/src/Compiler/Environment.php +++ b/scssphp/scssphp/src/Compiler/Environment.php @@ -1,8 +1,9 @@ */ -class CompilerException extends \Exception +class CompilerException extends \Exception implements SassException { } diff --git a/scssphp/scssphp/src/Exception/ParserException.php b/scssphp/scssphp/src/Exception/ParserException.php index 2fa12dd7a..5237f3079 100644 --- a/scssphp/scssphp/src/Exception/ParserException.php +++ b/scssphp/scssphp/src/Exception/ParserException.php @@ -1,8 +1,9 @@ */ -class ParserException extends \Exception +class ParserException extends \Exception implements SassException { + /** + * @var array + */ + private $sourcePosition; + + /** + * Get source position + * + * @api + */ + public function getSourcePosition() + { + return $this->sourcePosition; + } + + /** + * Set source position + * + * @api + * + * @param array $sourcePosition + */ + public function setSourcePosition($sourcePosition) + { + $this->sourcePosition = $sourcePosition; + } } diff --git a/scssphp/scssphp/src/Exception/RangeException.php b/scssphp/scssphp/src/Exception/RangeException.php index ee36c97e1..b18c32d6c 100644 --- a/scssphp/scssphp/src/Exception/RangeException.php +++ b/scssphp/scssphp/src/Exception/RangeException.php @@ -1,8 +1,9 @@ */ -class RangeException extends \Exception +class RangeException extends \Exception implements SassException { } diff --git a/scssphp/scssphp/src/Exception/SassException.php b/scssphp/scssphp/src/Exception/SassException.php new file mode 100644 index 000000000..9f62b3cd2 --- /dev/null +++ b/scssphp/scssphp/src/Exception/SassException.php @@ -0,0 +1,7 @@ + */ -class ServerException extends \Exception +class ServerException extends \Exception implements SassException { } diff --git a/scssphp/scssphp/src/Formatter.php b/scssphp/scssphp/src/Formatter.php index 478aa6a56..d52a6744a 100644 --- a/scssphp/scssphp/src/Formatter.php +++ b/scssphp/scssphp/src/Formatter.php @@ -1,8 +1,9 @@ keepSemicolons) { - return; - } - - if (($count = count($lines)) && substr($lines[$count - 1], -1) === ';') { - $lines[$count - 1] = substr($lines[$count - 1], 0, -1); - } + return rtrim($name) . trim($this->assignSeparator) . $value . ';'; } /** @@ -139,8 +143,7 @@ public function stripSemicolon(&$lines) protected function blockLines(OutputBlock $block) { $inner = $this->indentStr(); - - $glue = $this->break . $inner; + $glue = $this->break . $inner; $this->write($inner . implode($glue, $block->lines)); @@ -207,6 +210,10 @@ protected function block(OutputBlock $block) if (! empty($block->selectors)) { $this->indentLevel--; + if (! $this->keepSemicolons) { + $this->strippedSemicolon = ''; + } + if (empty($block->children)) { $this->write($this->break); } @@ -217,8 +224,10 @@ protected function block(OutputBlock $block) /** * Test and clean safely empty children + * * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $block - * @return bool + * + * @return boolean */ protected function testEmptyChildren($block) { @@ -228,14 +237,16 @@ protected function testEmptyChildren($block) foreach ($block->children as $k => &$child) { if (! $this->testEmptyChildren($child)) { $isEmpty = false; - } else { - if ($child->type === Type::T_MEDIA || $child->type === Type::T_DIRECTIVE) { - $child->children = []; - $child->selectors = null; - } + continue; + } + + if ($child->type === Type::T_MEDIA || $child->type === Type::T_DIRECTIVE) { + $child->children = []; + $child->selectors = null; } } } + return $isEmpty; } @@ -254,8 +265,8 @@ public function format(OutputBlock $block, SourceMapGenerator $sourceMapGenerato $this->sourceMapGenerator = null; if ($sourceMapGenerator) { - $this->currentLine = 1; - $this->currentColumn = 0; + $this->currentLine = 1; + $this->currentColumn = 0; $this->sourceMapGenerator = $sourceMapGenerator; } @@ -271,27 +282,67 @@ public function format(OutputBlock $block, SourceMapGenerator $sourceMapGenerato } /** + * Output content + * * @param string $str */ protected function write($str) { - if ($this->sourceMapGenerator) { - $this->sourceMapGenerator->addMapping( - $this->currentLine, - $this->currentColumn, - $this->currentBlock->sourceLine, - //columns from parser are off by one - $this->currentBlock->sourceColumn > 0 ? $this->currentBlock->sourceColumn - 1 : 0, - $this->currentBlock->sourceName - ); + if (! empty($this->strippedSemicolon)) { + echo $this->strippedSemicolon; - $lines = explode("\n", $str); - $lineCount = count($lines); - $this->currentLine += $lineCount-1; + $this->strippedSemicolon = ''; + } + + /* + * Maybe Strip semi-colon appended by property(); it's a separator, not a terminator + * will be striped for real before a closing, otherwise displayed unchanged starting the next write + */ + if ( + ! $this->keepSemicolons && + $str && + (strpos($str, ';') !== false) && + (substr($str, -1) === ';') + ) { + $str = substr($str, 0, -1); + + $this->strippedSemicolon = ';'; + } + if ($this->sourceMapGenerator) { + $lines = explode("\n", $str); $lastLine = array_pop($lines); - $this->currentColumn = ($lineCount === 1 ? $this->currentColumn : 0) + strlen($lastLine); + foreach ($lines as $line) { + // If the written line starts is empty, adding a mapping would add it for + // a non-existent column as we are at the end of the line + if ($line !== '') { + $this->sourceMapGenerator->addMapping( + $this->currentLine, + $this->currentColumn, + $this->currentBlock->sourceLine, + //columns from parser are off by one + $this->currentBlock->sourceColumn > 0 ? $this->currentBlock->sourceColumn - 1 : 0, + $this->currentBlock->sourceName + ); + } + + $this->currentLine++; + $this->currentColumn = 0; + } + + if ($lastLine !== '') { + $this->sourceMapGenerator->addMapping( + $this->currentLine, + $this->currentColumn, + $this->currentBlock->sourceLine, + //columns from parser are off by one + $this->currentBlock->sourceColumn > 0 ? $this->currentBlock->sourceColumn - 1 : 0, + $this->currentBlock->sourceName + ); + } + + $this->currentColumn += \strlen($lastLine); } echo $str; diff --git a/scssphp/scssphp/src/Formatter/Compact.php b/scssphp/scssphp/src/Formatter/Compact.php index 591f0c92e..249920ef5 100644 --- a/scssphp/scssphp/src/Formatter/Compact.php +++ b/scssphp/scssphp/src/Formatter/Compact.php @@ -1,8 +1,9 @@ + * + * @deprecated since 1.4.0. Use the Compressed formatter instead. */ class Compact extends Formatter { @@ -25,6 +28,8 @@ class Compact extends Formatter */ public function __construct() { + @trigger_error('The Compact formatter is deprecated since 1.4.0. Use the Compressed formatter instead.', E_USER_DEPRECATED); + $this->indentLevel = 0; $this->indentChar = ''; $this->break = ''; diff --git a/scssphp/scssphp/src/Formatter/Compressed.php b/scssphp/scssphp/src/Formatter/Compressed.php index ec4722eaf..d666a6656 100644 --- a/scssphp/scssphp/src/Formatter/Compressed.php +++ b/scssphp/scssphp/src/Formatter/Compressed.php @@ -1,8 +1,9 @@ + * + * @deprecated since 1.4.0. Use the Compressed formatter instead. */ class Crunched extends Formatter { @@ -26,6 +28,8 @@ class Crunched extends Formatter */ public function __construct() { + @trigger_error('The Crunched formatter is deprecated since 1.4.0. Use the Compressed formatter instead.', E_USER_DEPRECATED); + $this->indentLevel = 0; $this->indentChar = ' '; $this->break = ''; diff --git a/scssphp/scssphp/src/Formatter/Debug.php b/scssphp/scssphp/src/Formatter/Debug.php index 94e70c815..c676601bb 100644 --- a/scssphp/scssphp/src/Formatter/Debug.php +++ b/scssphp/scssphp/src/Formatter/Debug.php @@ -1,8 +1,9 @@ + * + * @deprecated since 1.4.0. */ class Debug extends Formatter { @@ -26,6 +28,8 @@ class Debug extends Formatter */ public function __construct() { + @trigger_error('The Debug formatter is deprecated since 1.4.0.', E_USER_DEPRECATED); + $this->indentLevel = 0; $this->indentChar = ''; $this->break = "\n"; diff --git a/scssphp/scssphp/src/Formatter/Expanded.php b/scssphp/scssphp/src/Formatter/Expanded.php index 8eec47587..b7cbde18d 100644 --- a/scssphp/scssphp/src/Formatter/Expanded.php +++ b/scssphp/scssphp/src/Formatter/Expanded.php @@ -1,8 +1,9 @@ lines as $index => $line) { if (substr($line, 0, 2) === '/*') { - $block->lines[$index] = preg_replace('/(\r|\n)+/', $glue, $line); + $block->lines[$index] = preg_replace('/\r\n?|\n|\f/', $this->break, $line); } } diff --git a/scssphp/scssphp/src/Formatter/Nested.php b/scssphp/scssphp/src/Formatter/Nested.php index 50a70ce6a..3249c1828 100644 --- a/scssphp/scssphp/src/Formatter/Nested.php +++ b/scssphp/scssphp/src/Formatter/Nested.php @@ -1,8 +1,9 @@ + * + * @deprecated since 1.4.0. Use the Expanded formatter instead. */ class Nested extends Formatter { @@ -32,6 +34,8 @@ class Nested extends Formatter */ public function __construct() { + @trigger_error('The Nested formatter is deprecated since 1.4.0. Use the Expanded formatter instead.', E_USER_DEPRECATED); + $this->indentLevel = 0; $this->indentChar = ' '; $this->break = "\n"; @@ -58,29 +62,17 @@ protected function indentStr() protected function blockLines(OutputBlock $block) { $inner = $this->indentStr(); - - $glue = $this->break . $inner; + $glue = $this->break . $inner; foreach ($block->lines as $index => $line) { if (substr($line, 0, 2) === '/*') { - $block->lines[$index] = preg_replace('/(\r|\n)+/', $glue, $line); + $block->lines[$index] = preg_replace('/\r\n?|\n|\f/', $this->break, $line); } } $this->write($inner . implode($glue, $block->lines)); } - protected function hasFlatChild($block) - { - foreach ($block->children as $child) { - if (empty($child->selectors)) { - return true; - } - } - - return false; - } - /** * {@inheritdoc} */ @@ -101,7 +93,7 @@ protected function block(OutputBlock $block) $previousHasSelector = false; } - $isMediaOrDirective = in_array($block->type, [Type::T_DIRECTIVE, Type::T_MEDIA]); + $isMediaOrDirective = \in_array($block->type, [Type::T_DIRECTIVE, Type::T_MEDIA]); $isSupport = ($block->type === Type::T_DIRECTIVE && $block->selectors && strpos(implode('', $block->selectors), '@supports') !== false); @@ -109,7 +101,8 @@ protected function block(OutputBlock $block) array_pop($depths); $this->depth--; - if (!$this->depth && ($block->depth <= 1 || (!$this->indentLevel && $block->type === Type::T_COMMENT)) && + if ( + ! $this->depth && ($block->depth <= 1 || (! $this->indentLevel && $block->type === Type::T_COMMENT)) && (($block->selectors && ! $isMediaOrDirective) || $previousHasSelector) ) { $downLevel = $this->break; @@ -130,10 +123,12 @@ protected function block(OutputBlock $block) if ($block->depth > end($depths)) { if (! $previousEmpty || $this->depth < 1) { $this->depth++; + $depths[] = $block->depth; } else { // keep the current depth unchanged but take the block depth as a new reference for following blocks array_pop($depths); + $depths[] = $block->depth; } } @@ -170,15 +165,18 @@ protected function block(OutputBlock $block) } $this->blockLines($block); + $closeBlock = $this->break; } if (! empty($block->children)) { - if ($this->depth>0 && ($isMediaOrDirective || ! $this->hasFlatChild($block))) { + if ($this->depth > 0 && ($isMediaOrDirective || ! $this->hasFlatChild($block))) { array_pop($depths); + $this->depth--; $this->blockChildren($block); $this->depth++; + $depths[] = $block->depth; } else { $this->blockChildren($block); @@ -193,13 +191,19 @@ protected function block(OutputBlock $block) if (! empty($block->selectors)) { $this->indentLevel--; + if (! $this->keepSemicolons) { + $this->strippedSemicolon = ''; + } + $this->write($this->close); + $closeBlock = $this->break; if ($this->depth > 1 && ! empty($block->children)) { array_pop($depths); $this->depth--; } + if (! $isMediaOrDirective) { $previousHasSelector = true; } @@ -209,4 +213,22 @@ protected function block(OutputBlock $block) $this->write($this->break); } } + + /** + * Block has flat child + * + * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $block + * + * @return boolean + */ + private function hasFlatChild($block) + { + foreach ($block->children as $child) { + if (empty($child->selectors)) { + return true; + } + } + + return false; + } } diff --git a/scssphp/scssphp/src/Formatter/OutputBlock.php b/scssphp/scssphp/src/Formatter/OutputBlock.php index 3e6fd9289..fe0321bde 100644 --- a/scssphp/scssphp/src/Formatter/OutputBlock.php +++ b/scssphp/scssphp/src/Formatter/OutputBlock.php @@ -1,8 +1,9 @@ [ 'in' => 1, 'pc' => 6, @@ -64,75 +69,75 @@ class Number extends Node implements \ArrayAccess ], 'dpi' => [ 'dpi' => 1, - 'dpcm' => 2.54, - 'dppx' => 96, + 'dpcm' => 1 / 2.54, + 'dppx' => 1 / 96, ], ]; /** * @var integer|float */ - public $dimension; + private $dimension; /** - * @var array + * @var string[] + * @phpstan-var list */ - public $units; + private $numeratorUnits; /** - * Initialize number - * - * @param mixed $dimension - * @param mixed $initialUnit + * @var string[] + * @phpstan-var list */ - public function __construct($dimension, $initialUnit) - { - $this->type = Type::T_NUMBER; - $this->dimension = $dimension; - $this->units = is_array($initialUnit) - ? $initialUnit - : ($initialUnit ? [$initialUnit => 1] - : []); - } + private $denominatorUnits; /** - * Coerce number to target units + * Initialize number * - * @param array $units + * @param integer|float $dimension + * @param string[]|string $numeratorUnits + * @param string[] $denominatorUnits * - * @return \ScssPhp\ScssPhp\Node\Number + * @phpstan-param list|string $numeratorUnits + * @phpstan-param list $denominatorUnits */ - public function coerce($units) + public function __construct($dimension, $numeratorUnits, array $denominatorUnits = []) { - if ($this->unitless()) { - return new Number($this->dimension, $units); + if (is_string($numeratorUnits)) { + $numeratorUnits = $numeratorUnits ? [$numeratorUnits] : []; + } elseif (isset($numeratorUnits['numerator_units'], $numeratorUnits['denominator_units'])) { + // TODO get rid of this once `$number[2]` is not used anymore + $denominatorUnits = $numeratorUnits['denominator_units']; + $numeratorUnits = $numeratorUnits['numerator_units']; } - $dimension = $this->dimension; - - foreach (static::$unitTable['in'] as $unit => $conv) { - $from = isset($this->units[$unit]) ? $this->units[$unit] : 0; - $to = isset($units[$unit]) ? $units[$unit] : 0; - $factor = pow($conv, $from - $to); - $dimension /= $factor; - } - - return new Number($dimension, $units); + $this->dimension = $dimension; + $this->numeratorUnits = $numeratorUnits; + $this->denominatorUnits = $denominatorUnits; } /** - * Normalize number - * - * @return \ScssPhp\ScssPhp\Node\Number + * @return float|int */ - public function normalize() + public function getDimension() { - $dimension = $this->dimension; - $units = []; + return $this->dimension; + } - $this->normalizeUnits($dimension, $units, 'in'); + /** + * @return string[] + */ + public function getNumeratorUnits() + { + return $this->numeratorUnits; + } - return new Number($dimension, $units); + /** + * @return string[] + */ + public function getDenominatorUnits() + { + return $this->denominatorUnits; } /** @@ -141,14 +146,15 @@ public function normalize() public function offsetExists($offset) { if ($offset === -3) { - return $this->sourceColumn !== null; + return ! \is_null($this->sourceColumn); } if ($offset === -2) { - return $this->sourceLine !== null; + return ! \is_null($this->sourceLine); } - if ($offset === -1 || + if ( + $offset === -1 || $offset === 0 || $offset === 1 || $offset === 2 @@ -175,13 +181,13 @@ public function offsetGet($offset) return $this->sourceIndex; case 0: - return $this->type; + return Type::T_NUMBER; case 1: return $this->dimension; case 2: - return $this->units; + return array('numerator_units' => $this->numeratorUnits, 'denominator_units' => $this->denominatorUnits); } } @@ -190,17 +196,7 @@ public function offsetGet($offset) */ public function offsetSet($offset, $value) { - if ($offset === 1) { - $this->dimension = $value; - } elseif ($offset === 2) { - $this->units = $value; - } elseif ($offset == -1) { - $this->sourceIndex = $value; - } elseif ($offset == -2) { - $this->sourceLine = $value; - } elseif ($offset == -3) { - $this->sourceColumn = $value; - } + throw new \BadMethodCallException('Number is immutable'); } /** @@ -208,17 +204,7 @@ public function offsetSet($offset, $value) */ public function offsetUnset($offset) { - if ($offset === 1) { - $this->dimension = null; - } elseif ($offset === 2) { - $this->units = null; - } elseif ($offset === -1) { - $this->sourceIndex = null; - } elseif ($offset === -2) { - $this->sourceLine = null; - } elseif ($offset === -3) { - $this->sourceColumn = null; - } + throw new \BadMethodCallException('Number is immutable'); } /** @@ -228,7 +214,19 @@ public function offsetUnset($offset) */ public function unitless() { - return ! array_sum($this->units); + return \count($this->numeratorUnits) === 0 && \count($this->denominatorUnits) === 0; + } + + /** + * Checks whether the number has exactly this unit + * + * @param string $unit + * + * @return bool + */ + public function hasUnit($unit) + { + return \count($this->numeratorUnits) === 1 && \count($this->denominatorUnits) === 0 && $this->numeratorUnits[0] === $unit; } /** @@ -238,22 +236,234 @@ public function unitless() */ public function unitStr() { - $numerators = []; - $denominators = []; + if ($this->unitless()) { + return ''; + } + + return self::getUnitString($this->numeratorUnits, $this->denominatorUnits); + } + + /** + * @param string|null $varName + * + * @return void + */ + public function assertNoUnits($varName = null) + { + if ($this->unitless()) { + return; + } + + throw SassScriptException::forArgument(sprintf('Expected %s to have no units', $this), $varName); + } + + /** + * @param Number $other + * + * @return void + */ + public function assertSameUnitOrUnitless(Number $other) + { + if ($other->unitless()) { + return; + } + + if ($this->numeratorUnits === $other->numeratorUnits && $this->denominatorUnits === $other->denominatorUnits) { + return; + } + + throw new SassScriptException(sprintf( + 'Incompatible units %s and %s.', + self::getUnitString($this->numeratorUnits, $this->denominatorUnits), + self::getUnitString($other->numeratorUnits, $other->denominatorUnits) + )); + } + + /** + * @param Number $other + * + * @return bool + */ + public function isComparableTo(Number $other) + { + if ($this->unitless() || $other->unitless()) { + return true; + } + + try { + $this->greaterThan($other); + return true; + } catch (SassScriptException $e) { + return false; + } + } + + /** + * @param Number $other + * + * @return bool + */ + public function lessThan(Number $other) + { + return $this->coerceUnits($other, function ($num1, $num2) { + return $num1 < $num2; + }); + } + + /** + * @param Number $other + * + * @return bool + */ + public function lessThanOrEqual(Number $other) + { + return $this->coerceUnits($other, function ($num1, $num2) { + return $num1 <= $num2; + }); + } + + /** + * @param Number $other + * + * @return bool + */ + public function greaterThan(Number $other) + { + return $this->coerceUnits($other, function ($num1, $num2) { + return $num1 > $num2; + }); + } + + /** + * @param Number $other + * + * @return bool + */ + public function greaterThanOrEqual(Number $other) + { + return $this->coerceUnits($other, function ($num1, $num2) { + return $num1 >= $num2; + }); + } + + /** + * @param Number $other + * + * @return Number + */ + public function plus(Number $other) + { + return $this->coerceNumber($other, function ($num1, $num2) { + return $num1 + $num2; + }); + } + + /** + * @param Number $other + * + * @return Number + */ + public function minus(Number $other) + { + return $this->coerceNumber($other, function ($num1, $num2) { + return $num1 - $num2; + }); + } + + /** + * @return Number + */ + public function unaryMinus() + { + return new Number(-$this->dimension, $this->numeratorUnits, $this->denominatorUnits); + } + + /** + * @param Number $other + * + * @return Number + */ + public function modulo(Number $other) + { + return $this->coerceNumber($other, function ($num1, $num2) { + if ($num2 == 0) { + return NAN; + } + + $result = fmod($num1, $num2); - foreach ($this->units as $unit => $unitSize) { - if ($unitSize > 0) { - $numerators = array_pad($numerators, count($numerators) + $unitSize, $unit); - continue; + if ($result == 0) { + return 0; } - if ($unitSize < 0) { - $denominators = array_pad($denominators, count($denominators) + $unitSize, $unit); - continue; + if ($num2 < 0 xor $num1 < 0) { + $result += $num2; } + + return $result; + }); + } + + /** + * @param Number $other + * + * @return Number + */ + public function times(Number $other) + { + return $this->multiplyUnits($this->dimension * $other->dimension, $this->numeratorUnits, $this->denominatorUnits, $other->numeratorUnits, $other->denominatorUnits); + } + + /** + * @param Number $other + * + * @return Number + */ + public function dividedBy(Number $other) + { + if ($other->dimension == 0) { + if ($this->dimension == 0) { + $value = NAN; + } elseif ($this->dimension > 0) { + $value = INF; + } else { + $value = -INF; + } + } else { + $value = $this->dimension / $other->dimension; } - return implode('*', $numerators) . (count($denominators) ? '/' . implode('*', $denominators) : ''); + return $this->multiplyUnits($value, $this->numeratorUnits, $this->denominatorUnits, $other->denominatorUnits, $other->numeratorUnits); + } + + /** + * @param Number $other + * + * @return bool + */ + public function equals(Number $other) + { + // Unitless numbers are convertable to unit numbers, but not equal, so we special-case unitless here. + if ($this->unitless() !== $other->unitless()) { + return false; + } + + // In Sass, neither NaN nor Infinity are equal to themselves, while PHP defines INF==INF + if (is_nan($this->dimension) || is_nan($other->dimension) || !is_finite($this->dimension) || !is_finite($other->dimension)) { + return false; + } + + if ($this->unitless()) { + return round($this->dimension, self::PRECISION) == round($other->dimension, self::PRECISION); + } + + try { + return $this->coerceUnits($other, function ($num1, $num2) { + return round($num1,self::PRECISION) == round($num2, self::PRECISION); + }); + } catch (SassScriptException $e) { + return false; + } } /** @@ -265,35 +475,31 @@ public function unitStr() */ public function output(Compiler $compiler = null) { - $dimension = round($this->dimension, static::$precision); + $dimension = round($this->dimension, self::PRECISION); - $units = array_filter($this->units, function ($unitSize) { - return $unitSize; - }); - - if (count($units) > 1 && array_sum($units) === 0) { - $dimension = $this->dimension; - $units = []; - - $this->normalizeUnits($dimension, $units, 'in'); + if (is_nan($dimension)) { + return 'NaN'; + } - $dimension = round($dimension, static::$precision); - $units = array_filter($units, function ($unitSize) { - return $unitSize; - }); + if ($dimension === INF) { + return 'Infinity'; } - $unitSize = array_sum($units); + if ($dimension === -INF) { + return '-Infinity'; + } - if ($compiler && ($unitSize > 1 || $unitSize < 0 || count($units) > 1)) { - $compiler->throwError((string) $dimension . $this->unitStr() . " isn't a valid CSS value."); + if ($compiler) { + $unit = $this->unitStr(); + } elseif (isset($this->numeratorUnits[0])) { + $unit = $this->numeratorUnits[0]; + } else { + $unit = ''; } - reset($units); - $unit = key($units); - $dimension = number_format($dimension, static::$precision, '.', ''); + $dimension = number_format($dimension, self::PRECISION, '.', ''); - return (static::$precision ? rtrim(rtrim($dimension, '0'), '.') : $dimension) . $unit; + return rtrim(rtrim($dimension, '0'), '.') . $unit; } /** @@ -305,26 +511,227 @@ public function __toString() } /** - * Normalize units + * @param Number $other + * @param callable $operation + * + * @return Number + * + * @phpstan-param callable(int|float, int|float): (int|float) $operation + */ + private function coerceNumber(Number $other, $operation) + { + $result = $this->coerceUnits($other, $operation); + + if (!$this->unitless()) { + return new Number($result, $this->numeratorUnits, $this->denominatorUnits); + } + + return new Number($result, $other->numeratorUnits, $other->denominatorUnits); + } + + /** + * @param Number $other + * @param callable $operation + * + * @return mixed + * + * @phpstan-template T + * @phpstan-param callable(int|float, int|float): T $operation + * @phpstan-return T + */ + private function coerceUnits(Number $other, $operation) + { + if (!$this->unitless()) { + $num1 = $this->dimension; + $num2 = $other->valueInUnits($this->numeratorUnits, $this->denominatorUnits); + } else { + $num1 = $this->valueInUnits($other->numeratorUnits, $other->denominatorUnits); + $num2 = $other->dimension; + } + + return \call_user_func($operation, $num1, $num2); + } + + /** + * @param string[] $numeratorUnits + * @param string[] $denominatorUnits + * + * @return int|float + * + * @phpstan-param list $numeratorUnits + * @phpstan-param list $denominatorUnits + */ + private function valueInUnits(array $numeratorUnits, array $denominatorUnits) + { + if ( + $this->unitless() + || (\count($numeratorUnits) === 0 && \count($denominatorUnits) === 0) + || ($this->numeratorUnits === $numeratorUnits && $this->denominatorUnits === $denominatorUnits) + ) { + return $this->dimension; + } + + $value = $this->dimension; + $oldNumerators = $this->numeratorUnits; + + foreach ($numeratorUnits as $newNumerator) { + foreach ($oldNumerators as $key => $oldNumerator) { + $conversionFactor = self::getConversionFactor($newNumerator, $oldNumerator); + + if (\is_null($conversionFactor)) { + continue; + } + + $value *= $conversionFactor; + unset($oldNumerators[$key]); + continue 2; + } + + throw new SassScriptException(sprintf( + 'Incompatible units %s and %s.', + self::getUnitString($this->numeratorUnits, $this->denominatorUnits), + self::getUnitString($numeratorUnits, $denominatorUnits) + )); + } + + $oldDenominators = $this->denominatorUnits; + + foreach ($denominatorUnits as $newDenominator) { + foreach ($oldDenominators as $key => $oldDenominator) { + $conversionFactor = self::getConversionFactor($newDenominator, $oldDenominator); + + if (\is_null($conversionFactor)) { + continue; + } + + $value /= $conversionFactor; + unset($oldDenominators[$key]); + continue 2; + } + + throw new SassScriptException(sprintf( + 'Incompatible units %s and %s.', + self::getUnitString($this->numeratorUnits, $this->denominatorUnits), + self::getUnitString($numeratorUnits, $denominatorUnits) + )); + } + + if (\count($oldNumerators) || \count($oldDenominators)) { + throw new SassScriptException(sprintf( + 'Incompatible units %s and %s.', + self::getUnitString($this->numeratorUnits, $this->denominatorUnits), + self::getUnitString($numeratorUnits, $denominatorUnits) + )); + } + + return $value; + } + + /** + * @param int|float $value + * @param string[] $numerators1 + * @param string[] $denominators1 + * @param string[] $numerators2 + * @param string[] $denominators2 + * + * @return Number + * + * @phpstan-param list $numerators1 + * @phpstan-param list $denominators1 + * @phpstan-param list $numerators2 + * @phpstan-param list $denominators2 + */ + private function multiplyUnits($value, array $numerators1, array $denominators1, array $numerators2, array $denominators2) + { + $newNumerators = array(); + + foreach ($numerators1 as $numerator) { + foreach ($denominators2 as $key => $denominator) { + $conversionFactor = self::getConversionFactor($numerator, $denominator); + + if (\is_null($conversionFactor)) { + continue; + } + + $value /= $conversionFactor; + unset($denominators2[$key]); + continue 2; + } + + $newNumerators[] = $numerator; + } + + foreach ($numerators2 as $numerator) { + foreach ($denominators1 as $key => $denominator) { + $conversionFactor = self::getConversionFactor($numerator, $denominator); + + if (\is_null($conversionFactor)) { + continue; + } + + $value /= $conversionFactor; + unset($denominators1[$key]); + continue 2; + } + + $newNumerators[] = $numerator; + } + + $newDenominators = array_values(array_merge($denominators1, $denominators2)); + + return new Number($value, $newNumerators, $newDenominators); + } + + /** + * Returns the number of [unit1]s per [unit2]. * - * @param integer|float $dimension - * @param array $units - * @param string $baseUnit + * Equivalently, `1unit1 * conversionFactor(unit1, unit2) = 1unit2`. + * + * @param string $unit1 + * @param string $unit2 + * + * @return float|int|null */ - private function normalizeUnits(&$dimension, &$units, $baseUnit = 'in') + private static function getConversionFactor($unit1, $unit2) { - $dimension = $this->dimension; - $units = []; + if ($unit1 === $unit2) { + return 1; + } - foreach ($this->units as $unit => $exp) { - if (isset(static::$unitTable[$baseUnit][$unit])) { - $factor = pow(static::$unitTable[$baseUnit][$unit], $exp); + foreach (static::$unitTable as $unitVariants) { + if (isset($unitVariants[$unit1]) && isset($unitVariants[$unit2])) { + return $unitVariants[$unit1] / $unitVariants[$unit2]; + } + } + + return null; + } - $unit = $baseUnit; - $dimension /= $factor; + /** + * Returns unit(s) as the product of numerator units divided by the product of denominator units + * + * @param string[] $numerators + * @param string[] $denominators + * + * @phpstan-param list $numerators + * @phpstan-param list $denominators + * + * @return string + */ + private static function getUnitString(array $numerators, array $denominators) + { + if (!\count($numerators)) { + if (\count($denominators) === 0) { + return 'no units'; } - $units[$unit] = $exp + (isset($units[$unit]) ? $units[$unit] : 0); + if (\count($denominators) === 1) { + return $denominators[0] . '^-1'; + } + + return '(' . implode('*', $denominators) . ')^-1'; } + + return implode('*', $numerators) . (\count($denominators) ? '/' . implode('*', $denominators) : ''); } } diff --git a/scssphp/scssphp/src/OutputStyle.php b/scssphp/scssphp/src/OutputStyle.php new file mode 100644 index 000000000..c284639c1 --- /dev/null +++ b/scssphp/scssphp/src/OutputStyle.php @@ -0,0 +1,9 @@ + */ protected static $precedence = [ '=' => 0, @@ -38,7 +34,6 @@ class Parser 'and' => 2, '==' => 3, '!=' => 3, - '<=>' => 3, '<=' => 4, '>=' => 4, '<' => 4, @@ -50,38 +45,83 @@ class Parser '%' => 6, ]; + /** + * @var string + */ protected static $commentPattern; + /** + * @var string + */ protected static $operatorPattern; + /** + * @var string + */ protected static $whitePattern; + /** + * @var Cache|null + */ protected $cache; private $sourceName; private $sourceIndex; + /** + * @var array + */ private $sourcePositions; + /** + * @var array|null + */ private $charset; + /** + * The current offset in the buffer + * + * @var int + */ private $count; + /** + * @var Block + */ private $env; + /** + * @var bool + */ private $inParens; + /** + * @var bool + */ private $eatWhiteDefault; + /** + * @var bool + */ private $discardComments; + private $allowVars; + /** + * @var string + */ private $buffer; private $utf8; + /** + * @var string|null + */ private $encoding; private $patternModifiers; private $commentsSeen; + private $cssOnly; + /** * Constructor * * @api * - * @param string $sourceName - * @param integer $sourceIndex - * @param string $encoding - * @param \ScssPhp\ScssPhp\Cache $cache + * @param string $sourceName + * @param integer $sourceIndex + * @param string|null $encoding + * @param Cache|null $cache + * @param bool $cssOnly */ - public function __construct($sourceName, $sourceIndex = 0, $encoding = 'utf-8', $cache = null) + public function __construct($sourceName, $sourceIndex = 0, $encoding = 'utf-8', Cache $cache = null, $cssOnly = false) { $this->sourceName = $sourceName ?: '(stdin)'; $this->sourceIndex = $sourceIndex; @@ -89,10 +129,12 @@ public function __construct($sourceName, $sourceIndex = 0, $encoding = 'utf-8', $this->utf8 = ! $encoding || strtolower($encoding) === 'utf-8'; $this->patternModifiers = $this->utf8 ? 'Aisu' : 'Ais'; $this->commentsSeen = []; - $this->discardComments = false; + $this->commentsSeen = []; + $this->allowVars = true; + $this->cssOnly = $cssOnly; if (empty(static::$operatorPattern)) { - static::$operatorPattern = '([*\/%+-]|[!=]\=|\>\=?|\<\=\>|\<\=?|and|or)'; + static::$operatorPattern = '([*\/%+-]|[!=]\=|\>\=?|\<\=?|and|or)'; $commentSingle = '\/\/'; $commentMultiLeft = '\/\*'; @@ -104,9 +146,7 @@ public function __construct($sourceName, $sourceIndex = 0, $encoding = 'utf-8', : '/' . $commentSingle . '[^\n]*\s*|(' . static::$commentPattern . ')\s*|\s+/AisS'; } - if ($cache) { - $this->cache = $cache; - } + $this->cache = $cache; } /** @@ -128,9 +168,30 @@ public function getSourceName() * * @param string $msg * - * @throws \ScssPhp\ScssPhp\Exception\ParserException + * @throws ParserException + * + * @deprecated use "parseError" and throw the exception in the caller instead. */ public function throwParseError($msg = 'parse error') + { + @trigger_error( + 'The method "throwParseError" is deprecated. Use "parseError" and throw the exception in the caller instead', + E_USER_DEPRECATED + ); + + throw $this->parseError($msg); + } + + /** + * Creates a parser error + * + * @api + * + * @param string $msg + * + * @return ParserException + */ + public function parseError($msg = 'parse error') { list($line, $column) = $this->getSourcePosition($this->count); @@ -138,11 +199,21 @@ public function throwParseError($msg = 'parse error') ? "line: $line, column: $column" : "$this->sourceName on line $line, at column $column"; - if ($this->peek("(.*?)(\n|$)", $m, $this->count)) { - throw new ParserException("$msg: failed at `$m[1]` $loc"); + if ($this->peek('(.*?)(\n|$)', $m, $this->count)) { + $this->restoreEncoding(); + + $e = new ParserException("$msg: failed at `$m[1]` $loc"); + $e->setSourcePosition([$this->sourceName, $line, $column]); + + return $e; } - throw new ParserException("$msg: $loc"); + $this->restoreEncoding(); + + $e = new ParserException("$msg: $loc"); + $e->setSourcePosition([$this->sourceName, $line, $column]); + + return $e; } /** @@ -152,19 +223,19 @@ public function throwParseError($msg = 'parse error') * * @param string $buffer * - * @return \ScssPhp\ScssPhp\Block + * @return Block */ public function parse($buffer) { if ($this->cache) { - $cacheKey = $this->sourceName . ":" . md5($buffer); + $cacheKey = $this->sourceName . ':' . md5($buffer); $parseOptions = [ 'charset' => $this->charset, 'utf8' => $this->utf8, ]; - $v = $this->cache->getCache("parse", $cacheKey, $parseOptions); + $v = $this->cache->getCache('parse', $cacheKey, $parseOptions); - if (! is_null($v)) { + if (! \is_null($v)) { return $v; } } @@ -192,12 +263,12 @@ public function parse($buffer) ; } - if ($this->count !== strlen($this->buffer)) { - $this->throwParseError(); + if ($this->count !== \strlen($this->buffer)) { + throw $this->parseError(); } if (! empty($this->env->parent)) { - $this->throwParseError('unclosed block'); + throw $this->parseError('unclosed block'); } if ($this->charset) { @@ -207,7 +278,7 @@ public function parse($buffer) $this->restoreEncoding(); if ($this->cache) { - $this->cache->setCache("parse", $cacheKey, $this->env, $parseOptions); + $this->cache->setCache('parse', $cacheKey, $this->env, $parseOptions); } return $this->env; @@ -218,8 +289,8 @@ public function parse($buffer) * * @api * - * @param string $buffer - * @param string $out + * @param string $buffer + * @param string|array $out * * @return boolean */ @@ -232,6 +303,7 @@ public function parseValue($buffer, &$out) $this->buffer = (string) $buffer; $this->saveEncoding(); + $this->extractLineNumbers($this->buffer); $list = $this->valueList($out); @@ -245,12 +317,13 @@ public function parseValue($buffer, &$out) * * @api * - * @param string $buffer - * @param string $out + * @param string $buffer + * @param string|array $out + * @param bool $shouldValidate * * @return boolean */ - public function parseSelector($buffer, &$out) + public function parseSelector($buffer, &$out, $shouldValidate = true) { $this->count = 0; $this->env = null; @@ -259,11 +332,21 @@ public function parseSelector($buffer, &$out) $this->buffer = (string) $buffer; $this->saveEncoding(); + $this->extractLineNumbers($this->buffer); + + // discard space/comments at the start + $this->discardComments = true; + $this->whitespace(); + $this->discardComments = false; $selector = $this->selectors($out); $this->restoreEncoding(); + if ($shouldValidate && $this->count !== strlen($buffer)) { + throw $this->parseError("`" . substr($buffer, $this->count) . "` is not a valid Selector in `$buffer`"); + } + return $selector; } @@ -272,10 +355,10 @@ public function parseSelector($buffer, &$out) * * @api * - * @param string $buffer - * @param string $out + * @param string $buffer + * @param string|array $out * - * @return array + * @return boolean */ public function parseMediaQueryList($buffer, &$out) { @@ -286,7 +369,7 @@ public function parseMediaQueryList($buffer, &$out) $this->buffer = (string) $buffer; $this->saveEncoding(); - + $this->extractLineNumbers($this->buffer); $isMediaQuery = $this->mediaQueryList($out); @@ -340,24 +423,31 @@ protected function parseChunk() // the directives if (isset($this->buffer[$this->count]) && $this->buffer[$this->count] === '@') { - if ($this->literal('@at-root', 8) && + if ( + $this->literal('@at-root', 8) && ($this->selectors($selector) || true) && ($this->map($with) || true) && - (($this->matchChar('(') - && $this->interpolation($with) - && $this->matchChar(')')) || true) && + (($this->matchChar('(') && + $this->interpolation($with) && + $this->matchChar(')')) || true) && $this->matchChar('{', false) ) { + ! $this->cssOnly || $this->assertPlainCssValid(false, $s); + $atRoot = $this->pushSpecialBlock(Type::T_AT_ROOT, $s); $atRoot->selector = $selector; - $atRoot->with = $with; + $atRoot->with = $with; return true; } $this->seek($s); - if ($this->literal('@media', 6) && $this->mediaQueryList($mediaQueryList) && $this->matchChar('{', false)) { + if ( + $this->literal('@media', 6) && + $this->mediaQueryList($mediaQueryList) && + $this->matchChar('{', false) + ) { $media = $this->pushSpecialBlock(Type::T_MEDIA, $s); $media->queryList = $mediaQueryList[2]; @@ -366,11 +456,14 @@ protected function parseChunk() $this->seek($s); - if ($this->literal('@mixin', 6) && + if ( + $this->literal('@mixin', 6) && $this->keyword($mixinName) && ($this->argumentDef($args) || true) && $this->matchChar('{', false) ) { + ! $this->cssOnly || $this->assertPlainCssValid(false, $s); + $mixin = $this->pushSpecialBlock(Type::T_MIXIN, $s); $mixin->name = $mixinName; $mixin->args = $args; @@ -380,18 +473,27 @@ protected function parseChunk() $this->seek($s); - if ($this->literal('@include', 8) && - $this->keyword($mixinName) && - ($this->matchChar('(') && + if ( + ($this->literal('@include', 8) && + $this->keyword($mixinName) && + ($this->matchChar('(') && ($this->argValues($argValues) || true) && $this->matchChar(')') || true) && - ($this->end() || - ($this->literal('using', 5) && - $this->argumentDef($argUsing) && - ($this->end() || $this->matchChar('{') && $hasBlock = true)) || - $this->matchChar('{') && $hasBlock = true) + ($this->end()) || + ($this->literal('using', 5) && + $this->argumentDef($argUsing) && + ($this->end() || $this->matchChar('{') && $hasBlock = true)) || + $this->matchChar('{') && $hasBlock = true) ) { - $child = [Type::T_INCLUDE, $mixinName, isset($argValues) ? $argValues : null, null, isset($argUsing) ? $argUsing : null]; + ! $this->cssOnly || $this->assertPlainCssValid(false, $s); + + $child = [ + Type::T_INCLUDE, + $mixinName, + isset($argValues) ? $argValues : null, + null, + isset($argUsing) ? $argUsing : null + ]; if (! empty($hasBlock)) { $include = $this->pushSpecialBlock(Type::T_INCLUDE, $s); @@ -405,10 +507,13 @@ protected function parseChunk() $this->seek($s); - if ($this->literal('@scssphp-import-once', 20) && + if ( + $this->literal('@scssphp-import-once', 20) && $this->valueList($importPath) && $this->end() ) { + ! $this->cssOnly || $this->assertPlainCssValid(false, $s); + $this->append([Type::T_SCSSPHP_IMPORT_ONCE, $importPath], $s); return true; @@ -416,10 +521,18 @@ protected function parseChunk() $this->seek($s); - if ($this->literal('@import', 7) && + if ( + $this->literal('@import', 7) && $this->valueList($importPath) && + $importPath[0] !== Type::T_FUNCTION_CALL && $this->end() ) { + if ($this->cssOnly) { + $this->assertPlainCssValid([Type::T_IMPORT, $importPath], $s); + $this->append([Type::T_COMMENT, rtrim(substr($this->buffer, $s, $this->count - $s))]); + return true; + } + $this->append([Type::T_IMPORT, $importPath], $s); return true; @@ -427,10 +540,17 @@ protected function parseChunk() $this->seek($s); - if ($this->literal('@import', 7) && + if ( + $this->literal('@import', 7) && $this->url($importPath) && $this->end() ) { + if ($this->cssOnly) { + $this->assertPlainCssValid([Type::T_IMPORT, $importPath], $s); + $this->append([Type::T_COMMENT, rtrim(substr($this->buffer, $s, $this->count - $s))]); + return true; + } + $this->append([Type::T_IMPORT, $importPath], $s); return true; @@ -438,10 +558,13 @@ protected function parseChunk() $this->seek($s); - if ($this->literal('@extend', 7) && + if ( + $this->literal('@extend', 7) && $this->selectors($selectors) && $this->end() ) { + ! $this->cssOnly || $this->assertPlainCssValid(false, $s); + // check for '!flag' $optional = $this->stripOptionalFlag($selectors); $this->append([Type::T_EXTEND, $selectors, $optional], $s); @@ -451,11 +574,14 @@ protected function parseChunk() $this->seek($s); - if ($this->literal('@function', 9) && + if ( + $this->literal('@function', 9) && $this->keyword($fnName) && $this->argumentDef($args) && $this->matchChar('{', false) ) { + ! $this->cssOnly || $this->assertPlainCssValid(false, $s); + $func = $this->pushSpecialBlock(Type::T_FUNCTION, $s); $func->name = $fnName; $func->args = $args; @@ -465,23 +591,13 @@ protected function parseChunk() $this->seek($s); - if ($this->literal('@break', 6) && $this->end()) { - $this->append([Type::T_BREAK], $s); - - return true; - } - - $this->seek($s); - - if ($this->literal('@continue', 9) && $this->end()) { - $this->append([Type::T_CONTINUE], $s); - - return true; - } - - $this->seek($s); + if ( + $this->literal('@return', 7) && + ($this->valueList($retVal) || true) && + $this->end() + ) { + ! $this->cssOnly || $this->assertPlainCssValid(false, $s); - if ($this->literal('@return', 7) && ($this->valueList($retVal) || true) && $this->end()) { $this->append([Type::T_RETURN, isset($retVal) ? $retVal : [Type::T_NULL]], $s); return true; @@ -489,12 +605,15 @@ protected function parseChunk() $this->seek($s); - if ($this->literal('@each', 5) && + if ( + $this->literal('@each', 5) && $this->genericList($varNames, 'variable', ',', false) && $this->literal('in', 2) && $this->valueList($list) && $this->matchChar('{', false) ) { + ! $this->cssOnly || $this->assertPlainCssValid(false, $s); + $each = $this->pushSpecialBlock(Type::T_EACH, $s); foreach ($varNames[2] as $varName) { @@ -508,10 +627,22 @@ protected function parseChunk() $this->seek($s); - if ($this->literal('@while', 6) && + if ( + $this->literal('@while', 6) && $this->expression($cond) && $this->matchChar('{', false) ) { + ! $this->cssOnly || $this->assertPlainCssValid(false, $s); + + while ( + $cond[0] === Type::T_LIST && + ! empty($cond['enclosing']) && + $cond['enclosing'] === 'parent' && + \count($cond[2]) == 1 + ) { + $cond = reset($cond[2]); + } + $while = $this->pushSpecialBlock(Type::T_WHILE, $s); $while->cond = $cond; @@ -520,7 +651,8 @@ protected function parseChunk() $this->seek($s); - if ($this->literal('@for', 4) && + if ( + $this->literal('@for', 4) && $this->variable($varName) && $this->literal('from', 4) && $this->expression($start) && @@ -529,10 +661,12 @@ protected function parseChunk() $this->expression($end) && $this->matchChar('{', false) ) { + ! $this->cssOnly || $this->assertPlainCssValid(false, $s); + $for = $this->pushSpecialBlock(Type::T_FOR, $s); - $for->var = $varName[1]; + $for->var = $varName[1]; $for->start = $start; - $for->end = $end; + $for->end = $end; $for->until = isset($forUntil); return true; @@ -540,9 +674,24 @@ protected function parseChunk() $this->seek($s); - if ($this->literal('@if', 3) && $this->valueList($cond) && $this->matchChar('{', false)) { + if ( + $this->literal('@if', 3) && + $this->functionCallArgumentsList($cond, false, '{', false) + ) { + ! $this->cssOnly || $this->assertPlainCssValid(false, $s); + $if = $this->pushSpecialBlock(Type::T_IF, $s); - $if->cond = $cond; + + while ( + $cond[0] === Type::T_LIST && + ! empty($cond['enclosing']) && + $cond['enclosing'] === 'parent' && + \count($cond[2]) == 1 + ) { + $cond = reset($cond[2]); + } + + $if->cond = $cond; $if->cases = []; return true; @@ -550,10 +699,12 @@ protected function parseChunk() $this->seek($s); - if ($this->literal('@debug', 6) && - $this->valueList($value) && - $this->end() + if ( + $this->literal('@debug', 6) && + $this->functionCallArgumentsList($value, false) ) { + ! $this->cssOnly || $this->assertPlainCssValid(false, $s); + $this->append([Type::T_DEBUG, $value], $s); return true; @@ -561,10 +712,12 @@ protected function parseChunk() $this->seek($s); - if ($this->literal('@warn', 5) && - $this->valueList($value) && - $this->end() + if ( + $this->literal('@warn', 5) && + $this->functionCallArgumentsList($value, false) ) { + ! $this->cssOnly || $this->assertPlainCssValid(false, $s); + $this->append([Type::T_WARN, $value], $s); return true; @@ -572,10 +725,12 @@ protected function parseChunk() $this->seek($s); - if ($this->literal('@error', 6) && - $this->valueList($value) && - $this->end() + if ( + $this->literal('@error', 6) && + $this->functionCallArgumentsList($value, false) ) { + ! $this->cssOnly || $this->assertPlainCssValid(false, $s); + $this->append([Type::T_ERROR, $value], $s); return true; @@ -583,14 +738,16 @@ protected function parseChunk() $this->seek($s); - #if ($this->literal('@content', 8)) - - if ($this->literal('@content', 8) && + if ( + $this->literal('@content', 8) && ($this->end() || $this->matchChar('(') && - $this->argValues($argContent) && - $this->matchChar(')') && - $this->end())) { + $this->argValues($argContent) && + $this->matchChar(')') && + $this->end()) + ) { + ! $this->cssOnly || $this->assertPlainCssValid(false, $s); + $this->append([Type::T_MIXIN_CONTENT, isset($argContent) ? $argContent : null], $s); return true; @@ -606,7 +763,10 @@ protected function parseChunk() if ($this->literal('@else', 5)) { if ($this->matchChar('{', false)) { $else = $this->pushSpecialBlock(Type::T_ELSE, $s); - } elseif ($this->literal('if', 2) && $this->valueList($cond) && $this->matchChar('{', false)) { + } elseif ( + $this->literal('if', 2) && + $this->functionCallArgumentsList($cond, false, '{', false) + ) { $else = $this->pushSpecialBlock(Type::T_ELSEIF, $s); $else->cond = $cond; } @@ -623,7 +783,8 @@ protected function parseChunk() } // only retain the first @charset directive encountered - if ($this->literal('@charset', 8) && + if ( + $this->literal('@charset', 8) && $this->valueList($charset) && $this->end() ) { @@ -644,11 +805,13 @@ protected function parseChunk() $this->seek($s); - if ($this->literal('@supports', 9) && - ($t1=$this->supportsQuery($supportQuery)) && - ($t2=$this->matchChar('{', false)) ) { + if ( + $this->literal('@supports', 9) && + ($t1 = $this->supportsQuery($supportQuery)) && + ($t2 = $this->matchChar('{', false)) + ) { $directive = $this->pushSpecialBlock(Type::T_DIRECTIVE, $s); - $directive->name = 'supports'; + $directive->name = 'supports'; $directive->value = $supportQuery; return true; @@ -657,11 +820,16 @@ protected function parseChunk() $this->seek($s); // doesn't match built in directive, do generic one - if ($this->matchChar('@', false) && - $this->keyword($dirName) && - ($this->variable($dirValue) || $this->openString('{', $dirValue) || true) && - $this->matchChar('{', false) + if ( + $this->matchChar('@', false) && + $this->mixedKeyword($dirName) && + $this->directiveValue($dirValue, '{') ) { + if (count($dirName) === 1 && is_string(reset($dirName))) { + $dirName = reset($dirName); + } else { + $dirName = [Type::T_STRING, '', $dirName]; + } if ($dirName === 'media') { $directive = $this->pushSpecialBlock(Type::T_MEDIA, $s); } else { @@ -670,6 +838,7 @@ protected function parseChunk() } if (isset($dirValue)) { + ! $this->cssOnly || ($dirValue = $this->assertPlainCssValid($dirValue)); $directive->value = $dirValue; } @@ -678,12 +847,102 @@ protected function parseChunk() $this->seek($s); + // maybe it's a generic blockless directive + if ( + $this->matchChar('@', false) && + $this->mixedKeyword($dirName) && + ! $this->isKnownGenericDirective($dirName) && + ($this->end(false) || ($this->directiveValue($dirValue, '') && $this->end(false))) + ) { + if (\count($dirName) === 1 && \is_string(\reset($dirName))) { + $dirName = \reset($dirName); + } else { + $dirName = [Type::T_STRING, '', $dirName]; + } + if ( + ! empty($this->env->parent) && + $this->env->type && + ! \in_array($this->env->type, [Type::T_DIRECTIVE, Type::T_MEDIA]) + ) { + $plain = \trim(\substr($this->buffer, $s, $this->count - $s)); + throw $this->parseError( + "Unknown directive `{$plain}` not allowed in `" . $this->env->type . "` block" + ); + } + // blockless directives with a blank line after keeps their blank lines after + // sass-spec compliance purpose + $s = $this->count; + $hasBlankLine = false; + if ($this->match('\s*?\n\s*\n', $out, false)) { + $hasBlankLine = true; + $this->seek($s); + } + $isNotRoot = ! empty($this->env->parent); + $this->append([Type::T_DIRECTIVE, [$dirName, $dirValue, $hasBlankLine, $isNotRoot]], $s); + $this->whitespace(); + + return true; + } + + $this->seek($s); + return false; } + $inCssSelector = null; + if ($this->cssOnly) { + $inCssSelector = (! empty($this->env->parent) && + ! in_array($this->env->type, [Type::T_DIRECTIVE, Type::T_MEDIA])); + } + // custom properties : right part is static + if (($this->customProperty($name) ) && $this->matchChar(':', false)) { + $start = $this->count; + + // but can be complex and finish with ; or } + foreach ([';','}'] as $ending) { + if ( + $this->openString($ending, $stringValue, '(', ')', false) && + $this->end() + ) { + $end = $this->count; + $value = $stringValue; + + // check if we have only a partial value due to nested [] or { } to take in account + $nestingPairs = [['[', ']'], ['{', '}']]; + + foreach ($nestingPairs as $nestingPair) { + $p = strpos($this->buffer, $nestingPair[0], $start); + + if ($p && $p < $end) { + $this->seek($start); + + if ( + $this->openString($ending, $stringValue, $nestingPair[0], $nestingPair[1], false) && + $this->end() && + $this->count > $end + ) { + $end = $this->count; + $value = $stringValue; + } + } + } + + $this->seek($end); + $this->append([Type::T_CUSTOM_PROPERTY, $name, $value], $s); + + return true; + } + } + + // TODO: output an error here if nothing found according to sass spec + } + + $this->seek($s); + // property shortcut // captures most properties before having to parse a selector - if ($this->keyword($name, false) && + if ( + $this->keyword($name, false) && $this->literal(': ', 2) && $this->valueList($value) && $this->end() @@ -697,11 +956,14 @@ protected function parseChunk() $this->seek($s); // variable assigns - if ($this->variable($name) && + if ( + $this->variable($name) && $this->matchChar(':') && $this->valueList($value) && $this->end() ) { + ! $this->cssOnly || $this->assertPlainCssValid(false, $s); + // check for '!flag' $assignmentFlags = $this->stripAssignmentFlags($value); $this->append([Type::T_ASSIGN, $name, $value, $assignmentFlags], $s); @@ -717,12 +979,17 @@ protected function parseChunk() } // opening css block - if ($this->selectors($selectors) && $this->matchChar('{', false)) { + if ( + $this->selectors($selectors) && + $this->matchChar('{', false) + ) { + ! $this->cssOnly || ! $inCssSelector || $this->assertPlainCssValid(false); + $this->pushBlock($selectors, $s); if ($this->eatWhiteDefault) { $this->whitespace(); - $this->append(null); // collect comments at the begining if needed + $this->append(null); // collect comments at the beginning if needed } return true; @@ -731,12 +998,15 @@ protected function parseChunk() $this->seek($s); // property assign, or nested assign - if ($this->propertyName($name) && $this->matchChar(':')) { + if ( + $this->propertyName($name) && + $this->matchChar(':') + ) { $foundSomething = false; if ($this->valueList($value)) { if (empty($this->env->parent)) { - $this->throwParseError('expected "{"'); + throw $this->parseError('expected "{"'); } $this->append([Type::T_ASSIGN, $name, $value], $s); @@ -744,6 +1014,8 @@ protected function parseChunk() } if ($this->matchChar('{', false)) { + ! $this->cssOnly || $this->assertPlainCssValid(false); + $propBlock = $this->pushSpecialBlock(Type::T_NESTED_PROPERTY, $s); $propBlock->prefix = $name; $propBlock->hasValue = $foundSomething; @@ -764,7 +1036,7 @@ protected function parseChunk() if ($this->matchChar('}', false)) { $block = $this->popBlock(); - if (!isset($block->type) || $block->type !== Type::T_IF) { + if (! isset($block->type) || $block->type !== Type::T_IF) { if ($this->env->parent) { $this->append(null); // collect comments before next statement if needed } @@ -793,7 +1065,8 @@ protected function parseChunk() } // extra stuff - if ($this->matchChar(';') || + if ( + $this->matchChar(';') || $this->literal('