diff --git a/composer.json b/composer.json index 6612b101b..57599b9e1 100644 --- a/composer.json +++ b/composer.json @@ -14,7 +14,7 @@ "nikic/php-parser": "~4.13.2", "pear/archive_tar": "~1.4.14", "pelago/emogrifier": "~3.1.0", - "scssphp/scssphp": "~1.10.2", + "scssphp/scssphp": "1.0.6", "swiftmailer/swiftmailer": "~6.3.0", "symfony/console": "~3.4.47", "symfony/dotenv": "~3.4.47", diff --git a/composer.lock b/composer.lock index 7e7def5f1..7791458dc 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": "781e902cab2de8cfdfefbb4a62cc1b01", + "content-hash": "2e22d4aefde79b10575e4de746f22f32", "packages": [ { "name": "combodo/tcpdf", @@ -817,16 +817,16 @@ }, { "name": "scssphp/scssphp", - "version": "v1.10.2", + "version": "1.0.6", "source": { "type": "git", "url": "https://github.com/scssphp/scssphp.git", - "reference": "387f4f4abf5d99f16be16314c5ab856f81c82f46" + "reference": "5b3c9d704950d8f9637f5110c36c281ec47dc13c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/scssphp/scssphp/zipball/387f4f4abf5d99f16be16314c5ab856f81c82f46", - "reference": "387f4f4abf5d99f16be16314c5ab856f81c82f46", + "url": "https://api.github.com/repos/scssphp/scssphp/zipball/5b3c9d704950d8f9637f5110c36c281ec47dc13c", + "reference": "5b3c9d704950d8f9637f5110c36c281ec47dc13c", "shasum": "" }, "require": { @@ -835,20 +835,11 @@ "php": ">=5.6.0" }, "require-dev": { - "bamarni/composer-bin-plugin": "^1.4", - "phpunit/phpunit": "^5.7 || ^6.5 || ^7.5 || ^8.3 || ^9.4", - "sass/sass-spec": "*", - "squizlabs/php_codesniffer": "~3.5", - "symfony/phpunit-bridge": "^5.1", - "thoughtbot/bourbon": "^7.0", - "twbs/bootstrap": "~5.0", - "twbs/bootstrap4": "4.6.1", + "phpunit/phpunit": "^5.7 || ^6.5 || ^7.5 || ^8.3", + "squizlabs/php_codesniffer": "~2.5", + "twbs/bootstrap": "~4.3", "zurb/foundation": "~6.5" }, - "suggest": { - "ext-iconv": "Can be used as fallback when ext-mbstring is not available", - "ext-mbstring": "For best performance, mbstring should be installed as it is faster than ext-iconv" - }, "bin": [ "bin/pscss" ], @@ -885,9 +876,9 @@ ], "support": { "issues": "https://github.com/scssphp/scssphp/issues", - "source": "https://github.com/scssphp/scssphp/tree/v1.10.2" + "source": "https://github.com/scssphp/scssphp/tree/master" }, - "time": "2022-03-02T21:15:09+00:00" + "time": "2019-12-12T05:00:52+00:00" }, { "name": "swiftmailer/swiftmailer", diff --git a/lib/composer/autoload_classmap.php b/lib/composer/autoload_classmap.php index b2e6f9507..3ddc888cc 100644 --- a/lib/composer/autoload_classmap.php +++ b/lib/composer/autoload_classmap.php @@ -766,29 +766,13 @@ return array( 'ScalarOqlExpression' => $baseDir . '/core/oql/oqlquery.class.inc.php', 'ScssPhp\\ScssPhp\\Base\\Range' => $vendorDir . '/scssphp/scssphp/src/Base/Range.php', 'ScssPhp\\ScssPhp\\Block' => $vendorDir . '/scssphp/scssphp/src/Block.php', - 'ScssPhp\\ScssPhp\\Block\\AtRootBlock' => $vendorDir . '/scssphp/scssphp/src/Block/AtRootBlock.php', - 'ScssPhp\\ScssPhp\\Block\\CallableBlock' => $vendorDir . '/scssphp/scssphp/src/Block/CallableBlock.php', - 'ScssPhp\\ScssPhp\\Block\\ContentBlock' => $vendorDir . '/scssphp/scssphp/src/Block/ContentBlock.php', - 'ScssPhp\\ScssPhp\\Block\\DirectiveBlock' => $vendorDir . '/scssphp/scssphp/src/Block/DirectiveBlock.php', - 'ScssPhp\\ScssPhp\\Block\\EachBlock' => $vendorDir . '/scssphp/scssphp/src/Block/EachBlock.php', - 'ScssPhp\\ScssPhp\\Block\\ElseBlock' => $vendorDir . '/scssphp/scssphp/src/Block/ElseBlock.php', - 'ScssPhp\\ScssPhp\\Block\\ElseifBlock' => $vendorDir . '/scssphp/scssphp/src/Block/ElseifBlock.php', - 'ScssPhp\\ScssPhp\\Block\\ForBlock' => $vendorDir . '/scssphp/scssphp/src/Block/ForBlock.php', - 'ScssPhp\\ScssPhp\\Block\\IfBlock' => $vendorDir . '/scssphp/scssphp/src/Block/IfBlock.php', - 'ScssPhp\\ScssPhp\\Block\\MediaBlock' => $vendorDir . '/scssphp/scssphp/src/Block/MediaBlock.php', - 'ScssPhp\\ScssPhp\\Block\\NestedPropertyBlock' => $vendorDir . '/scssphp/scssphp/src/Block/NestedPropertyBlock.php', - 'ScssPhp\\ScssPhp\\Block\\WhileBlock' => $vendorDir . '/scssphp/scssphp/src/Block/WhileBlock.php', 'ScssPhp\\ScssPhp\\Cache' => $vendorDir . '/scssphp/scssphp/src/Cache.php', 'ScssPhp\\ScssPhp\\Colors' => $vendorDir . '/scssphp/scssphp/src/Colors.php', - 'ScssPhp\\ScssPhp\\CompilationResult' => $vendorDir . '/scssphp/scssphp/src/CompilationResult.php', 'ScssPhp\\ScssPhp\\Compiler' => $vendorDir . '/scssphp/scssphp/src/Compiler.php', - 'ScssPhp\\ScssPhp\\Compiler\\CachedResult' => $vendorDir . '/scssphp/scssphp/src/Compiler/CachedResult.php', 'ScssPhp\\ScssPhp\\Compiler\\Environment' => $vendorDir . '/scssphp/scssphp/src/Compiler/Environment.php', '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', @@ -798,22 +782,15 @@ return array( 'ScssPhp\\ScssPhp\\Formatter\\Expanded' => $vendorDir . '/scssphp/scssphp/src/Formatter/Expanded.php', 'ScssPhp\\ScssPhp\\Formatter\\Nested' => $vendorDir . '/scssphp/scssphp/src/Formatter/Nested.php', 'ScssPhp\\ScssPhp\\Formatter\\OutputBlock' => $vendorDir . '/scssphp/scssphp/src/Formatter/OutputBlock.php', - 'ScssPhp\\ScssPhp\\Logger\\LoggerInterface' => $vendorDir . '/scssphp/scssphp/src/Logger/LoggerInterface.php', - 'ScssPhp\\ScssPhp\\Logger\\QuietLogger' => $vendorDir . '/scssphp/scssphp/src/Logger/QuietLogger.php', - 'ScssPhp\\ScssPhp\\Logger\\StreamLogger' => $vendorDir . '/scssphp/scssphp/src/Logger/StreamLogger.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\\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\\ValueConverter' => $vendorDir . '/scssphp/scssphp/src/ValueConverter.php', 'ScssPhp\\ScssPhp\\Version' => $vendorDir . '/scssphp/scssphp/src/Version.php', - 'ScssPhp\\ScssPhp\\Warn' => $vendorDir . '/scssphp/scssphp/src/Warn.php', 'SearchMenuNode' => $baseDir . '/application/menunode.class.inc.php', 'SecurityException' => $baseDir . '/core/coreexception.class.inc.php', 'SeparatorPopupMenuItem' => $baseDir . '/application/applicationextension.inc.php', diff --git a/lib/composer/autoload_static.php b/lib/composer/autoload_static.php index 0e1856fcf..8e76a331b 100644 --- a/lib/composer/autoload_static.php +++ b/lib/composer/autoload_static.php @@ -1036,29 +1036,13 @@ class ComposerStaticInit0018331147de7601e7552f7da8e3bb8b 'ScalarOqlExpression' => __DIR__ . '/../..' . '/core/oql/oqlquery.class.inc.php', 'ScssPhp\\ScssPhp\\Base\\Range' => __DIR__ . '/..' . '/scssphp/scssphp/src/Base/Range.php', 'ScssPhp\\ScssPhp\\Block' => __DIR__ . '/..' . '/scssphp/scssphp/src/Block.php', - 'ScssPhp\\ScssPhp\\Block\\AtRootBlock' => __DIR__ . '/..' . '/scssphp/scssphp/src/Block/AtRootBlock.php', - 'ScssPhp\\ScssPhp\\Block\\CallableBlock' => __DIR__ . '/..' . '/scssphp/scssphp/src/Block/CallableBlock.php', - 'ScssPhp\\ScssPhp\\Block\\ContentBlock' => __DIR__ . '/..' . '/scssphp/scssphp/src/Block/ContentBlock.php', - 'ScssPhp\\ScssPhp\\Block\\DirectiveBlock' => __DIR__ . '/..' . '/scssphp/scssphp/src/Block/DirectiveBlock.php', - 'ScssPhp\\ScssPhp\\Block\\EachBlock' => __DIR__ . '/..' . '/scssphp/scssphp/src/Block/EachBlock.php', - 'ScssPhp\\ScssPhp\\Block\\ElseBlock' => __DIR__ . '/..' . '/scssphp/scssphp/src/Block/ElseBlock.php', - 'ScssPhp\\ScssPhp\\Block\\ElseifBlock' => __DIR__ . '/..' . '/scssphp/scssphp/src/Block/ElseifBlock.php', - 'ScssPhp\\ScssPhp\\Block\\ForBlock' => __DIR__ . '/..' . '/scssphp/scssphp/src/Block/ForBlock.php', - 'ScssPhp\\ScssPhp\\Block\\IfBlock' => __DIR__ . '/..' . '/scssphp/scssphp/src/Block/IfBlock.php', - 'ScssPhp\\ScssPhp\\Block\\MediaBlock' => __DIR__ . '/..' . '/scssphp/scssphp/src/Block/MediaBlock.php', - 'ScssPhp\\ScssPhp\\Block\\NestedPropertyBlock' => __DIR__ . '/..' . '/scssphp/scssphp/src/Block/NestedPropertyBlock.php', - 'ScssPhp\\ScssPhp\\Block\\WhileBlock' => __DIR__ . '/..' . '/scssphp/scssphp/src/Block/WhileBlock.php', 'ScssPhp\\ScssPhp\\Cache' => __DIR__ . '/..' . '/scssphp/scssphp/src/Cache.php', 'ScssPhp\\ScssPhp\\Colors' => __DIR__ . '/..' . '/scssphp/scssphp/src/Colors.php', - 'ScssPhp\\ScssPhp\\CompilationResult' => __DIR__ . '/..' . '/scssphp/scssphp/src/CompilationResult.php', 'ScssPhp\\ScssPhp\\Compiler' => __DIR__ . '/..' . '/scssphp/scssphp/src/Compiler.php', - 'ScssPhp\\ScssPhp\\Compiler\\CachedResult' => __DIR__ . '/..' . '/scssphp/scssphp/src/Compiler/CachedResult.php', 'ScssPhp\\ScssPhp\\Compiler\\Environment' => __DIR__ . '/..' . '/scssphp/scssphp/src/Compiler/Environment.php', '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', @@ -1068,22 +1052,15 @@ class ComposerStaticInit0018331147de7601e7552f7da8e3bb8b 'ScssPhp\\ScssPhp\\Formatter\\Expanded' => __DIR__ . '/..' . '/scssphp/scssphp/src/Formatter/Expanded.php', 'ScssPhp\\ScssPhp\\Formatter\\Nested' => __DIR__ . '/..' . '/scssphp/scssphp/src/Formatter/Nested.php', 'ScssPhp\\ScssPhp\\Formatter\\OutputBlock' => __DIR__ . '/..' . '/scssphp/scssphp/src/Formatter/OutputBlock.php', - 'ScssPhp\\ScssPhp\\Logger\\LoggerInterface' => __DIR__ . '/..' . '/scssphp/scssphp/src/Logger/LoggerInterface.php', - 'ScssPhp\\ScssPhp\\Logger\\QuietLogger' => __DIR__ . '/..' . '/scssphp/scssphp/src/Logger/QuietLogger.php', - 'ScssPhp\\ScssPhp\\Logger\\StreamLogger' => __DIR__ . '/..' . '/scssphp/scssphp/src/Logger/StreamLogger.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\\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\\ValueConverter' => __DIR__ . '/..' . '/scssphp/scssphp/src/ValueConverter.php', 'ScssPhp\\ScssPhp\\Version' => __DIR__ . '/..' . '/scssphp/scssphp/src/Version.php', - 'ScssPhp\\ScssPhp\\Warn' => __DIR__ . '/..' . '/scssphp/scssphp/src/Warn.php', 'SearchMenuNode' => __DIR__ . '/../..' . '/application/menunode.class.inc.php', 'SecurityException' => __DIR__ . '/../..' . '/core/coreexception.class.inc.php', 'SeparatorPopupMenuItem' => __DIR__ . '/../..' . '/application/applicationextension.inc.php', diff --git a/lib/composer/installed.json b/lib/composer/installed.json index 12bcf5f89..0cda41c11 100644 --- a/lib/composer/installed.json +++ b/lib/composer/installed.json @@ -853,17 +853,17 @@ }, { "name": "scssphp/scssphp", - "version": "v1.10.2", - "version_normalized": "1.10.2.0", + "version": "1.0.6", + "version_normalized": "1.0.6.0", "source": { "type": "git", "url": "https://github.com/scssphp/scssphp.git", - "reference": "387f4f4abf5d99f16be16314c5ab856f81c82f46" + "reference": "5b3c9d704950d8f9637f5110c36c281ec47dc13c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/scssphp/scssphp/zipball/387f4f4abf5d99f16be16314c5ab856f81c82f46", - "reference": "387f4f4abf5d99f16be16314c5ab856f81c82f46", + "url": "https://api.github.com/repos/scssphp/scssphp/zipball/5b3c9d704950d8f9637f5110c36c281ec47dc13c", + "reference": "5b3c9d704950d8f9637f5110c36c281ec47dc13c", "shasum": "" }, "require": { @@ -872,21 +872,12 @@ "php": ">=5.6.0" }, "require-dev": { - "bamarni/composer-bin-plugin": "^1.4", - "phpunit/phpunit": "^5.7 || ^6.5 || ^7.5 || ^8.3 || ^9.4", - "sass/sass-spec": "*", - "squizlabs/php_codesniffer": "~3.5", - "symfony/phpunit-bridge": "^5.1", - "thoughtbot/bourbon": "^7.0", - "twbs/bootstrap": "~5.0", - "twbs/bootstrap4": "4.6.1", + "phpunit/phpunit": "^5.7 || ^6.5 || ^7.5 || ^8.3", + "squizlabs/php_codesniffer": "~2.5", + "twbs/bootstrap": "~4.3", "zurb/foundation": "~6.5" }, - "suggest": { - "ext-iconv": "Can be used as fallback when ext-mbstring is not available", - "ext-mbstring": "For best performance, mbstring should be installed as it is faster than ext-iconv" - }, - "time": "2022-03-02T21:15:09+00:00", + "time": "2019-12-12T05:00:52+00:00", "bin": [ "bin/pscss" ], @@ -924,7 +915,7 @@ ], "support": { "issues": "https://github.com/scssphp/scssphp/issues", - "source": "https://github.com/scssphp/scssphp/tree/v1.10.2" + "source": "https://github.com/scssphp/scssphp/tree/master" }, "install-path": "../scssphp/scssphp" }, diff --git a/lib/composer/installed.php b/lib/composer/installed.php index 9eda3ec76..bd61b662c 100644 --- a/lib/composer/installed.php +++ b/lib/composer/installed.php @@ -5,7 +5,7 @@ 'type' => 'project', 'install_path' => __DIR__ . '/../../', 'aliases' => array(), - 'reference' => '8c60e05b1a1d7e135bcf0cbf0d1bf337a623ff15', + 'reference' => 'c4ae94fd4ca204c8d94f19a0f58f24492d015d9b', 'name' => '__root__', 'dev' => true, ), @@ -16,7 +16,7 @@ 'type' => 'project', 'install_path' => __DIR__ . '/../../', 'aliases' => array(), - 'reference' => '8c60e05b1a1d7e135bcf0cbf0d1bf337a623ff15', + 'reference' => 'c4ae94fd4ca204c8d94f19a0f58f24492d015d9b', 'dev_requirement' => false, ), 'combodo/tcpdf' => array( @@ -176,12 +176,12 @@ ), ), 'scssphp/scssphp' => array( - 'pretty_version' => 'v1.10.2', - 'version' => '1.10.2.0', + 'pretty_version' => '1.0.6', + 'version' => '1.0.6.0', 'type' => 'library', 'install_path' => __DIR__ . '/../scssphp/scssphp', 'aliases' => array(), - 'reference' => '387f4f4abf5d99f16be16314c5ab856f81c82f46', + 'reference' => '5b3c9d704950d8f9637f5110c36c281ec47dc13c', 'dev_requirement' => false, ), 'swiftmailer/swiftmailer' => array( diff --git a/lib/scssphp/scssphp/README.md b/lib/scssphp/scssphp/README.md index 65bb93ea7..f541e4888 100644 --- a/lib/scssphp/scssphp/README.md +++ b/lib/scssphp/scssphp/README.md @@ -1,12 +1,12 @@ # scssphp -### +### -![Build](https://github.com/scssphp/scssphp/workflows/CI/badge.svg) +[![Build](https://travis-ci.org/scssphp/scssphp.svg?branch=master)](http://travis-ci.org/scssphp/scssphp) [![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,7 +23,8 @@ 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. -* `SassSpecTest.php` extracts tests from the `sass/sass-spec` repository. +* `ScssTest.php` extracts (ruby) `scss` tests from the `tests/scss_test.rb` file. +* `ServerTest.php` contains functional tests for the `Server` class. When changing any of the tests in `tests/inputs`, the tests will most likely fail because the output has changed. Once you verify that the output is correct @@ -31,41 +32,16 @@ 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`. It also -updates the list of excluded specs from sass-spec. +This will compile all the tests, and save results into `tests/outputs`. -To enable the full `sass-spec` compatibility tests: +To enable the `scss` compatibility tests: - TEST_SASS_SPEC=1 vendor/bin/phpunit tests + TEST_SCSS_COMPAT=1 vendor/bin/phpunit tests ## Coding Standard -`scssphp` source conforms to [PSR12](https://www.php-fig.org/psr/psr-12/). +`scssphp` source conforms to [PSR2](http://www.php-fig.org/psr/psr-2/). Run the following command from the root directory to check the code for "sniffs". - vendor/bin/phpcs --standard=PSR12 --extensions=php bin src tests *.php - -## Static Analysis - -`scssphp` uses [phpstan](https://phpstan.org/) for static analysis. - -Run the following command from the root directory to analyse the codebase: - - make phpstan - -As most of the codebase is composed of legacy code which cannot be type-checked -fully, the setup contains a baseline file with all errors we want to ignore. In -particular, we ignore all errors related to not specifying the types inside arrays -when these arrays correspond to the representation of Sass values and Sass AST nodes -in the parser and compiler. -When contributing, the proper process to deal with static analysis is the following: - -1. Make your change in the codebase -2. Run `make phpstan` -3. Fix errors reported by phpstan when possible -4. Repeat step 2 and 3 until nothing gets fixed anymore at step 3 -5. Run `make phpstan-baseline` to regenerate the phpstan baseline - -Additions to the baseline will be reviewed to avoid ignoring errors that should have -been fixed. + vendor/bin/phpcs --standard=PSR2 bin src tests diff --git a/lib/scssphp/scssphp/bin/pscss b/lib/scssphp/scssphp/bin/pscss index 0f009d6bd..f944b6325 100644 --- a/lib/scssphp/scssphp/bin/pscss +++ b/lib/scssphp/scssphp/bin/pscss @@ -1,10 +1,9 @@ #!/usr/bin/env php parse($data)), true)); - fwrite(STDERR, 'Warning: the --dump-tree option is deprecated. Use proper debugging tools instead.'); - exit(); } $scss = new Compiler(); +if ($debugInfo) { + $scss->setLineNumberStyle(Compiler::DEBUG_INFO); +} + +if ($lineNumbers) { + $scss->setLineNumberStyle(Compiler::LINE_COMMENTS); +} + +if ($ignoreErrors) { + $scss->setIgnoreErrors($ignoreErrors); +} + if ($loadPaths) { - $scss->setImportPaths($loadPaths); + $scss->setImportPaths(explode(PATH_SEPARATOR, $loadPaths)); +} + +if ($precision) { + $scss->setNumberPrecision($precision); } if ($style) { - if ($style === OutputStyle::COMPRESSED || $style === OutputStyle::EXPANDED) { - $scss->setOutputStyle($style); - } else { - fwrite(STDERR, "WARNING: the $style style is deprecated.\n"); - $scss->setFormatter('ScssPhp\\ScssPhp\\Formatter\\' . ucfirst($style)); - } + $scss->setFormatter('ScssPhp\\ScssPhp\\Formatter\\' . ucfirst($style)); } -$outputFile = isset($arguments[1]) ? $arguments[1] : null; -$sourceMapFile = null; - if ($sourceMap) { - $sourceMapOptions = array( - 'outputSourceFiles' => $embedSources, - ); - if ($embedSourceMap || $outputFile === null) { - $scss->setSourceMap(Compiler::SOURCE_MAP_INLINE); - } else { - $sourceMapFile = $outputFile . '.map'; - $sourceMapOptions['sourceMapWriteTo'] = $sourceMapFile; - $sourceMapOptions['sourceMapURL'] = basename($sourceMapFile); - $sourceMapOptions['sourceMapBasepath'] = getcwd(); - $sourceMapOptions['sourceMapFilename'] = basename($outputFile); - - $scss->setSourceMap(Compiler::SOURCE_MAP_FILE); - } - - $scss->setSourceMapOptions($sourceMapOptions); + $scss->setSourceMap(Compiler::SOURCE_MAP_INLINE); } if ($encoding) { $scss->setEncoding($encoding); } -try { - $result = $scss->compileString($data, $inputFile); -} catch (SassException $e) { - fwrite(STDERR, 'Error: '.$e->getMessage()."\n"); - exit(1); -} +echo $scss->compile($data, $inputFile); -if ($outputFile) { - file_put_contents($outputFile, $result->getCss()); - - if ($sourceMapFile !== null && $result->getSourceMap() !== null) { - file_put_contents($sourceMapFile, $result->getSourceMap()); - } -} else { - echo $result->getCss(); +if ($changeDir) { + chdir($oldWorkingDir); } diff --git a/lib/scssphp/scssphp/composer.json b/lib/scssphp/scssphp/composer.json index 3c60ca897..ff0590fbd 100644 --- a/lib/scssphp/scssphp/composer.json +++ b/lib/scssphp/scssphp/composer.json @@ -30,82 +30,22 @@ "ext-json": "*", "ext-ctype": "*" }, - "suggest": { - "ext-mbstring": "For best performance, mbstring should be installed as it is faster than ext-iconv", - "ext-iconv": "Can be used as fallback when ext-mbstring is not available" - }, "require-dev": { - "bamarni/composer-bin-plugin": "^1.4", - "phpunit/phpunit": "^5.7 || ^6.5 || ^7.5 || ^8.3 || ^9.4", - "sass/sass-spec": "*", - "squizlabs/php_codesniffer": "~3.5", - "symfony/phpunit-bridge": "^5.1", - "thoughtbot/bourbon": "^7.0", - "twbs/bootstrap": "~5.0", - "twbs/bootstrap4": "4.6.1", + "squizlabs/php_codesniffer": "~2.5", + "phpunit/phpunit": "^5.7 || ^6.5 || ^7.5 || ^8.3", + "twbs/bootstrap": "~4.3", "zurb/foundation": "~6.5" }, - "repositories": [ - { - "type": "package", - "package": { - "name": "sass/sass-spec", - "version": "2022.02.24", - "source": { - "type": "git", - "url": "https://github.com/sass/sass-spec.git", - "reference": "f41b9bfb9a3013392f2136c79f7f3356f15fb8ba" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sass/sass-spec/zipball/f41b9bfb9a3013392f2136c79f7f3356f15fb8ba", - "reference": "f41b9bfb9a3013392f2136c79f7f3356f15fb8ba", - "shasum": "" - } - } - }, - { - "type": "package", - "package": { - "name": "thoughtbot/bourbon", - "version": "v7.0.0", - "source": { - "type": "git", - "url": "https://github.com/thoughtbot/bourbon.git", - "reference": "fbe338ee6807e7f7aa996d82c8a16f248bb149b3" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/thoughtbot/bourbon/zipball/fbe338ee6807e7f7aa996d82c8a16f248bb149b3", - "reference": "fbe338ee6807e7f7aa996d82c8a16f248bb149b3", - "shasum": "" - } - } - }, - { - "type": "package", - "package": { - "name": "twbs/bootstrap4", - "version": "v4.6.1", - "source": { - "type": "git", - "url": "https://github.com/twbs/bootstrap.git", - "reference": "043a03c95a2ad6738f85b65e53b9dbdfb03b8d10" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/twbs/bootstrap/zipball/043a03c95a2ad6738f85b65e53b9dbdfb03b8d10", - "reference": "043a03c95a2ad6738f85b65e53b9dbdfb03b8d10", - "shasum": "" - } - } - } - ], + "minimum-stability": "dev", "bin": ["bin/pscss"], - "config": { - "sort-packages": true, - "allow-plugins": { - "bamarni/composer-bin-plugin": true - } + "archive": { + "exclude": [ + "/Makefile", + "/.gitattributes", + "/.gitignore", + "/.travis.yml", + "/phpunit.xml.dist", + "/tests" + ] } } diff --git a/lib/scssphp/scssphp/phpcs.xml.dist b/lib/scssphp/scssphp/phpcs.xml.dist deleted file mode 100644 index b162dbd6b..000000000 --- a/lib/scssphp/scssphp/phpcs.xml.dist +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - - diff --git a/lib/scssphp/scssphp/scss.inc.php b/lib/scssphp/scssphp/scss.inc.php index 459837805..e4ec7f181 100644 --- a/lib/scssphp/scssphp/scss.inc.php +++ b/lib/scssphp/scssphp/scss.inc.php @@ -1,21 +1,34 @@ - * - * @internal */ class Range { - /** - * @var float|int - */ public $first; - - /** - * @var float|int - */ public $last; /** * Initialize range * - * @param int|float $first - * @param int|float $last + * @param integer|float $first + * @param integer|float $last */ public function __construct($first, $last) { @@ -46,9 +36,9 @@ class Range /** * Test for inclusion in range * - * @param int|float $value + * @param integer|float $value * - * @return bool + * @return boolean */ public function includes($value) { diff --git a/lib/scssphp/scssphp/src/Block.php b/lib/scssphp/scssphp/src/Block.php index 96668dc66..bb1afdd4b 100644 --- a/lib/scssphp/scssphp/src/Block.php +++ b/lib/scssphp/scssphp/src/Block.php @@ -1,9 +1,8 @@ - * - * @internal */ class Block { /** - * @var string|null + * @var string */ public $type; /** - * @var Block|null + * @var \ScssPhp\ScssPhp\Block */ public $parent; @@ -37,22 +34,22 @@ class Block public $sourceName; /** - * @var int + * @var integer */ public $sourceIndex; /** - * @var int + * @var integer */ public $sourceLine; /** - * @var int + * @var integer */ public $sourceColumn; /** - * @var array|null + * @var array */ public $selectors; @@ -67,7 +64,7 @@ class Block public $children; /** - * @var Block|null + * @var \ScssPhp\ScssPhp\Block */ public $selfParent; } diff --git a/lib/scssphp/scssphp/src/Block/AtRootBlock.php b/lib/scssphp/scssphp/src/Block/AtRootBlock.php deleted file mode 100644 index 41842c269..000000000 --- a/lib/scssphp/scssphp/src/Block/AtRootBlock.php +++ /dev/null @@ -1,37 +0,0 @@ -type = Type::T_AT_ROOT; - } -} diff --git a/lib/scssphp/scssphp/src/Block/CallableBlock.php b/lib/scssphp/scssphp/src/Block/CallableBlock.php deleted file mode 100644 index a18a87c2a..000000000 --- a/lib/scssphp/scssphp/src/Block/CallableBlock.php +++ /dev/null @@ -1,45 +0,0 @@ -type = $type; - } -} diff --git a/lib/scssphp/scssphp/src/Block/ContentBlock.php b/lib/scssphp/scssphp/src/Block/ContentBlock.php deleted file mode 100644 index 870849800..000000000 --- a/lib/scssphp/scssphp/src/Block/ContentBlock.php +++ /dev/null @@ -1,38 +0,0 @@ -type = Type::T_INCLUDE; - } -} diff --git a/lib/scssphp/scssphp/src/Block/DirectiveBlock.php b/lib/scssphp/scssphp/src/Block/DirectiveBlock.php deleted file mode 100644 index b1d3d1a81..000000000 --- a/lib/scssphp/scssphp/src/Block/DirectiveBlock.php +++ /dev/null @@ -1,37 +0,0 @@ -type = Type::T_DIRECTIVE; - } -} diff --git a/lib/scssphp/scssphp/src/Block/EachBlock.php b/lib/scssphp/scssphp/src/Block/EachBlock.php deleted file mode 100644 index b3289579d..000000000 --- a/lib/scssphp/scssphp/src/Block/EachBlock.php +++ /dev/null @@ -1,37 +0,0 @@ -type = Type::T_EACH; - } -} diff --git a/lib/scssphp/scssphp/src/Block/ElseBlock.php b/lib/scssphp/scssphp/src/Block/ElseBlock.php deleted file mode 100644 index 6abb4d775..000000000 --- a/lib/scssphp/scssphp/src/Block/ElseBlock.php +++ /dev/null @@ -1,27 +0,0 @@ -type = Type::T_ELSE; - } -} diff --git a/lib/scssphp/scssphp/src/Block/ElseifBlock.php b/lib/scssphp/scssphp/src/Block/ElseifBlock.php deleted file mode 100644 index 4622bca79..000000000 --- a/lib/scssphp/scssphp/src/Block/ElseifBlock.php +++ /dev/null @@ -1,32 +0,0 @@ -type = Type::T_ELSEIF; - } -} diff --git a/lib/scssphp/scssphp/src/Block/ForBlock.php b/lib/scssphp/scssphp/src/Block/ForBlock.php deleted file mode 100644 index a9cf6733b..000000000 --- a/lib/scssphp/scssphp/src/Block/ForBlock.php +++ /dev/null @@ -1,47 +0,0 @@ -type = Type::T_FOR; - } -} diff --git a/lib/scssphp/scssphp/src/Block/IfBlock.php b/lib/scssphp/scssphp/src/Block/IfBlock.php deleted file mode 100644 index 9f21bf88a..000000000 --- a/lib/scssphp/scssphp/src/Block/IfBlock.php +++ /dev/null @@ -1,37 +0,0 @@ - - */ - public $cases = []; - - public function __construct() - { - $this->type = Type::T_IF; - } -} diff --git a/lib/scssphp/scssphp/src/Block/MediaBlock.php b/lib/scssphp/scssphp/src/Block/MediaBlock.php deleted file mode 100644 index c49ee1b2b..000000000 --- a/lib/scssphp/scssphp/src/Block/MediaBlock.php +++ /dev/null @@ -1,37 +0,0 @@ -type = Type::T_MEDIA; - } -} diff --git a/lib/scssphp/scssphp/src/Block/NestedPropertyBlock.php b/lib/scssphp/scssphp/src/Block/NestedPropertyBlock.php deleted file mode 100644 index 1ea4a6c8a..000000000 --- a/lib/scssphp/scssphp/src/Block/NestedPropertyBlock.php +++ /dev/null @@ -1,37 +0,0 @@ -type = Type::T_NESTED_PROPERTY; - } -} diff --git a/lib/scssphp/scssphp/src/Block/WhileBlock.php b/lib/scssphp/scssphp/src/Block/WhileBlock.php deleted file mode 100644 index ac18d4e02..000000000 --- a/lib/scssphp/scssphp/src/Block/WhileBlock.php +++ /dev/null @@ -1,32 +0,0 @@ -type = Type::T_WHILE; - } -} diff --git a/lib/scssphp/scssphp/src/Cache.php b/lib/scssphp/scssphp/src/Cache.php index 9731c60a7..422497f49 100644 --- a/lib/scssphp/scssphp/src/Cache.php +++ b/lib/scssphp/scssphp/src/Cache.php @@ -1,9 +1,8 @@ - * - * @internal + * @author Cedric Morin */ class Cache { const CACHE_VERSION = 1; - /** - * directory used for storing data - * - * @var string|false - */ + // directory used for storing data public static $cacheDir = false; - /** - * prefix for the storing data - * - * @var string - */ + // prefix for the storing data public static $prefix = 'scssphp_'; - /** - * force a refresh : 'once' for refreshing the first hit on a cache only, true to never use the cache in this hit - * - * @var bool|string - */ + // force a refresh : 'once' for refreshing the first hit on a cache only, true to never use the cache in this hit public static $forceRefresh = false; - /** - * specifies the number of seconds after which data cached will be seen as 'garbage' and potentially cleaned up - * - * @var int - */ + // specifies the number of seconds after which data cached will be seen as 'garbage' and potentially cleaned up public static $gcLifetime = 604800; - /** - * array of already refreshed cache if $forceRefresh==='once' - * - * @var array - */ + // array of already refreshed cache if $forceRefresh==='once' protected static $refreshed = []; /** * Constructor * * @param array $options - * - * @phpstan-param array{cacheDir?: string, prefix?: string, forceRefresh?: string} $options */ public function __construct($options) { @@ -109,10 +84,10 @@ class Cache * Get the cached result of $operation on $what, * which is known as dependant from the content of $options * - * @param string $operation parse, compile... - * @param mixed $what content key (e.g., filename to be treated) - * @param array $options any option that affect the operation result on the content - * @param int|null $lastModified last modified timestamp + * @param string $operation parse, compile... + * @param mixed $what content key (e.g., filename to be treated) + * @param array $options any option that affect the operation result on the content + * @param integer $lastModified last modified timestamp * * @return mixed * @@ -122,20 +97,18 @@ class Cache { $fileCache = self::$cacheDir . self::cacheName($operation, $what, $options); - if ( - ((self::$forceRefresh === false) || (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']; } } @@ -152,8 +125,6 @@ class Cache * @param mixed $what * @param mixed $value * @param array $options - * - * @return void */ public function setCache($operation, $what, $value, $options = []) { @@ -161,7 +132,6 @@ class Cache $c = ['value' => $value]; $c = serialize($c); - file_put_contents($fileCache, $c); if (self::$forceRefresh === 'once') { @@ -183,7 +153,6 @@ class Cache { $t = [ 'version' => self::CACHE_VERSION, - 'scssphpVersion' => Version::VERSION, 'operation' => $operation, 'what' => $what, 'options' => $options @@ -200,8 +169,6 @@ class Cache /** * Check that the cache dir exists and is writeable * - * @return void - * * @throws \Exception */ public static function checkCacheDir() @@ -210,7 +177,9 @@ class Cache self::$cacheDir = rtrim(self::$cacheDir, '/') . '/'; if (! is_dir(self::$cacheDir)) { - throw new Exception('Cache directory doesn\'t exist: ' . self::$cacheDir); + if (! mkdir(self::$cacheDir)) { + throw new Exception('Cache directory couldn\'t be created: ' . self::$cacheDir); + } } if (! is_writable(self::$cacheDir)) { @@ -220,8 +189,6 @@ class Cache /** * Delete unused cached files - * - * @return void */ public static function cleanCache() { diff --git a/lib/scssphp/scssphp/src/Colors.php b/lib/scssphp/scssphp/src/Colors.php index 2df39992b..ef6409aca 100644 --- a/lib/scssphp/scssphp/src/Colors.php +++ b/lib/scssphp/scssphp/src/Colors.php @@ -1,9 +1,8 @@ - * - * @internal */ class Colors { @@ -26,13 +23,12 @@ class Colors * * @see http://www.w3.org/TR/css3-color * - * @var array + * @var array */ protected static $cssColors = [ 'aliceblue' => '240,248,255', 'antiquewhite' => '250,235,215', 'aqua' => '0,255,255', - 'cyan' => '0,255,255', 'aquamarine' => '127,255,212', 'azure' => '240,255,255', 'beige' => '245,245,220', @@ -50,12 +46,13 @@ 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', - 'darkgrey' => '169,169,169', 'darkgreen' => '0,100,0', + 'darkgrey' => '169,169,169', 'darkkhaki' => '189,183,107', 'darkmagenta' => '139,0,139', 'darkolivegreen' => '85,107,47', @@ -78,15 +75,14 @@ class Colors 'floralwhite' => '255,250,240', 'forestgreen' => '34,139,34', 'fuchsia' => '255,0,255', - 'magenta' => '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', @@ -102,8 +98,8 @@ class Colors 'lightcyan' => '224,255,255', 'lightgoldenrodyellow' => '250,250,210', 'lightgray' => '211,211,211', - 'lightgrey' => '211,211,211', 'lightgreen' => '144,238,144', + 'lightgrey' => '211,211,211', 'lightpink' => '255,182,193', 'lightsalmon' => '255,160,122', 'lightseagreen' => '32,178,170', @@ -115,6 +111,7 @@ 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', @@ -148,6 +145,7 @@ 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', @@ -169,6 +167,7 @@ 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', @@ -176,8 +175,6 @@ class Colors 'whitesmoke' => '245,245,245', 'yellow' => '255,255,0', 'yellowgreen' => '154,205,50', - 'rebeccapurple' => '102,51,153', - 'transparent' => '0,0,0,0', ]; /** @@ -185,11 +182,11 @@ class Colors * * @param string $colorName * - * @return int[]|null + * @return array|null */ public static function colorNameToRGBa($colorName) { - if (\is_string($colorName) && isset(static::$cssColors[$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 @@ -204,10 +201,10 @@ class Colors /** * Reverse conversion : from RGBA to a color name if possible * - * @param int $r - * @param int $g - * @param int $b - * @param int|float $a + * @param integer $r + * @param integer $g + * @param integer $b + * @param integer $a * * @return string|null */ @@ -220,26 +217,28 @@ class Colors } if ($a < 1) { + # specific case we dont' revert according to spec + #if (! $a && ! $r && ! $g && ! $b) { + # return 'transparent'; + #} + return null; } - if (\is_null($reverseColorTable)) { + 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 (count($rgb_str) == 3) { + $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)]; + if (isset($reverseColorTable[intval($r)][intval($g)][intval($b)])) { + return $reverseColorTable[intval($r)][intval($g)][intval($b)]; } return null; diff --git a/lib/scssphp/scssphp/src/CompilationResult.php b/lib/scssphp/scssphp/src/CompilationResult.php deleted file mode 100644 index 36adb0da4..000000000 --- a/lib/scssphp/scssphp/src/CompilationResult.php +++ /dev/null @@ -1,69 +0,0 @@ -css = $css; - $this->sourceMap = $sourceMap; - $this->includedFiles = $includedFiles; - } - - /** - * @return string - */ - public function getCss() - { - return $this->css; - } - - /** - * @return string[] - */ - public function getIncludedFiles() - { - return $this->includedFiles; - } - - /** - * The sourceMap content, if it was generated - * - * @return null|string - */ - public function getSourceMap() - { - return $this->sourceMap; - } -} diff --git a/lib/scssphp/scssphp/src/Compiler.php b/lib/scssphp/scssphp/src/Compiler.php index e414436c8..711d3382f 100644 --- a/lib/scssphp/scssphp/src/Compiler.php +++ b/lib/scssphp/scssphp/src/Compiler.php @@ -1,9 +1,8 @@ - * - * @final Extending the Compiler is deprecated */ class Compiler { - /** - * @deprecated - */ const LINE_COMMENTS = 1; - /** - * @deprecated - */ const DEBUG_INFO = 2; - /** - * @deprecated - */ const WITH_RULE = 1; - /** - * @deprecated - */ const WITH_MEDIA = 2; - /** - * @deprecated - */ const WITH_SUPPORTS = 4; - /** - * @deprecated - */ const WITH_ALL = 7; const SOURCE_MAP_NONE = 0; @@ -106,9 +71,9 @@ class Compiler const SOURCE_MAP_FILE = 2; /** - * @var array + * @var array */ - protected static $operatorNames = [ + static protected $operatorNames = [ '+' => 'add', '-' => 'sub', '*' => 'mul', @@ -122,261 +87,102 @@ class Compiler '<=' => 'lte', '>=' => 'gte', + '<=>' => 'cmp', ]; /** - * @var array + * @var array */ - protected static $namespaces = [ + static protected $namespaces = [ 'special' => '%', 'mixin' => '@', 'function' => '^', ]; - 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']; - private static $emptyArgumentList = [Type::T_LIST, '', [], []]; + 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']; - /** - * @var array - */ - protected $importPaths = []; - /** - * @var array - */ + protected $importPaths = ['']; protected $importCache = []; - - /** - * @var string[] - */ protected $importedFiles = []; - - /** - * @var array - * @phpstan-var array - */ protected $userFunctions = []; - /** - * @var array - */ protected $registeredVars = []; - /** - * @var array - */ protected $registeredFeatures = [ 'extend-selector-pseudoclass' => false, 'at-error' => true, - 'units-level-3' => true, + 'units-level-3' => false, 'global-variable-shadowing' => false, ]; - /** - * @var string|null - */ protected $encoding = null; - /** - * @var null - * @deprecated - */ protected $lineNumberStyle = null; - /** - * @var int|SourceMapGenerator - * @phpstan-var self::SOURCE_MAP_*|SourceMapGenerator - */ protected $sourceMap = self::SOURCE_MAP_NONE; - - /** - * @var array - * @phpstan-var array{sourceRoot?: string, sourceMapFilename?: string|null, sourceMapURL?: string|null, sourceMapWriteTo?: string|null, outputSourceFiles?: bool, sourceMapRootpath?: string, sourceMapBasepath?: string} - */ protected $sourceMapOptions = []; /** - * @var bool + * @var string|\ScssPhp\ScssPhp\Formatter */ - private $charset = true; + protected $formatter = 'ScssPhp\ScssPhp\Formatter\Nested'; - /** - * @var Formatter - */ - protected $formatter; - - /** - * @var string - * @phpstan-var class-string - */ - private $configuredFormatter = 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 - * - * @deprecated - */ protected $charsetSeen; - /** - * @var array - */ protected $sourceNames; - /** - * @var Cache|null - */ protected $cache; - /** - * @var bool - */ - protected $cacheCheckImportResolutions = false; - - /** - * @var int - */ protected $indentLevel; - /** - * @var array[] - */ protected $extends; - /** - * @var array - */ protected $extendsMap; - - /** - * @var array - */ - protected $parsedFiles = []; - - /** - * @var Parser|null - */ + protected $parsedFiles; protected $parser; - /** - * @var int|null - */ protected $sourceIndex; - /** - * @var int|null - */ protected $sourceLine; - /** - * @var int|null - */ protected $sourceColumn; - /** - * @var bool|null - */ + protected $stderr; protected $shouldEvaluate; - /** - * @var null - * @deprecated - */ protected $ignoreErrors; - /** - * @var bool - */ - protected $ignoreCallStackMessage = false; - /** - * @var array[] - */ protected $callStack = []; - /** - * @var array - * @phpstan-var list - */ - private $resolvedImports = []; - - /** - * The directory of the currently processed file - * - * @var string|null - */ - private $currentDirectory; - - /** - * The directory of the input file - * - * @var string - */ - private $rootDirectory; - - /** - * @var bool - */ - private $legacyCwdImportPath = true; - - /** - * @var LoggerInterface - */ - private $logger; - - /** - * @var array - */ - private $warnedChildFunctions = []; - /** * Constructor * * @param array|null $cacheOptions - * @phpstan-param array{cacheDir?: string, prefix?: string, forceRefresh?: string, checkImportResolutions?: bool}|null $cacheOptions */ public function __construct($cacheOptions = null) { + $this->parsedFiles = []; $this->sourceNames = []; if ($cacheOptions) { $this->cache = new Cache($cacheOptions); - if (!empty($cacheOptions['checkImportResolutions'])) { - $this->cacheCheckImportResolutions = true; - } } - $this->logger = new StreamLogger(fopen('php://stderr', 'w'), true); + $this->stderr = fopen('php://stderr', 'w'); } /** * Get compiler options * - * @return array - * - * @internal + * @return array */ public function getCompileOptions() { @@ -387,98 +193,55 @@ class Compiler 'encoding' => $this->encoding, 'sourceMap' => serialize($this->sourceMap), 'sourceMapOptions' => $this->sourceMapOptions, - 'formatter' => $this->configuredFormatter, - 'legacyImportPath' => $this->legacyCwdImportPath, + 'formatter' => $this->formatter, ]; return $options; } - /** - * Sets an alternative logger. - * - * Changing the logger in the middle of the compilation is not - * supported and will result in an undefined behavior. - * - * @param LoggerInterface $logger - * - * @return void - */ - public function setLogger(LoggerInterface $logger) - { - $this->logger = $logger; - } - /** * Set an alternative error output stream, for testing purpose only * * @param resource $handle - * - * @return void - * - * @deprecated Use {@see setLogger} instead */ public function setErrorOuput($handle) { - @trigger_error('The method "setErrorOuput" is deprecated. Use "setLogger" instead.', E_USER_DEPRECATED); - - $this->logger = new StreamLogger($handle); + $this->stderr = $handle; } /** * Compile scss * - * @param string $code - * @param string|null $path + * @api + * + * @param string $code + * @param string $path * * @return string - * - * @throws SassException when the source fails to compile - * - * @deprecated Use {@see compileString} instead. */ public function compile($code, $path = null) - { - @trigger_error(sprintf('The "%s" method is deprecated. Use "compileString" instead.', __METHOD__), E_USER_DEPRECATED); - - $result = $this->compileString($code, $path); - - $sourceMap = $result->getSourceMap(); - - if ($sourceMap !== null) { - if ($this->sourceMap instanceof SourceMapGenerator) { - $this->sourceMap->saveMap($sourceMap); - } elseif ($this->sourceMap === self::SOURCE_MAP_FILE) { - $sourceMapGenerator = new SourceMapGenerator($this->sourceMapOptions); - $sourceMapGenerator->saveMap($sourceMap); - } - } - - return $result->getCss(); - } - - /** - * Compile scss - * - * @param string $source - * @param string|null $path - * - * @return CompilationResult - * - * @throws SassException when the source fails to compile - */ - public function compileString($source, $path = null) { if ($this->cache) { - $cacheKey = ($path ? $path : '(stdin)') . ':' . md5($source); + $cacheKey = ($path ? $path : "(stdin)") . ":" . md5($code); $compileOptions = $this->getCompileOptions(); - $cachedResult = $this->cache->getCache('compile', $cacheKey, $compileOptions); + $cache = $this->cache->getCache("compile", $cacheKey, $compileOptions); - if ($cachedResult instanceof CachedResult && $this->isFreshCachedResult($cachedResult)) { - return $cachedResult->getResult(); + if (is_array($cache) && isset($cache['dependencies']) && isset($cache['out'])) { + // check if any dependency file changed before accepting the cache + foreach ($cache['dependencies'] as $file => $mtime) { + if (! is_file($file) || filemtime($file) !== $mtime) { + unset($cache); + break; + } + } + + if (isset($cache)) { + return $cache['out']; + } } } + $this->indentLevel = -1; $this->extends = []; $this->extendsMap = []; @@ -488,168 +251,73 @@ class Compiler $this->env = null; $this->scope = null; $this->storeEnv = null; + $this->charsetSeen = null; $this->shouldEvaluate = null; - $this->ignoreCallStackMessage = false; - $this->parsedFiles = []; - $this->importedFiles = []; - $this->resolvedImports = []; - if (!\is_null($path) && is_file($path)) { - $path = realpath($path) ?: $path; - $this->currentDirectory = dirname($path); - $this->rootDirectory = $this->currentDirectory; - } else { - $this->currentDirectory = null; - $this->rootDirectory = getcwd(); + $this->parser = $this->parserFactory($path); + $tree = $this->parser->parse($code); + $this->parser = null; + + $this->formatter = new $this->formatter(); + $this->rootBlock = null; + $this->rootEnv = $this->pushEnv($tree); + + $this->injectVariables($this->registeredVars); + $this->compileRoot($tree); + $this->popEnv(); + + $sourceMapGenerator = null; + + 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); + } } - try { - $this->parser = $this->parserFactory($path); - $tree = $this->parser->parse($source); - $this->parser = null; + $out = $this->formatter->format($this->scope, $sourceMapGenerator); - $this->formatter = new $this->configuredFormatter(); - $this->rootBlock = null; - $this->rootEnv = $this->pushEnv($tree); + if (! empty($out) && $this->sourceMap && $this->sourceMap !== self::SOURCE_MAP_NONE) { + $sourceMap = $sourceMapGenerator->generateJson(); + $sourceMapUrl = null; - $warnCallback = function ($message, $deprecation) { - $this->logger->warn($message, $deprecation); - }; - $previousWarnCallback = Warn::setCallback($warnCallback); + switch ($this->sourceMap) { + case self::SOURCE_MAP_INLINE: + $sourceMapUrl = sprintf('data:application/json,%s', Util::encodeURIComponent($sourceMap)); + break; - try { - $this->injectVariables($this->registeredVars); - $this->compileRoot($tree); - $this->popEnv(); - } finally { - Warn::setCallback($previousWarnCallback); + case self::SOURCE_MAP_FILE: + $sourceMapUrl = $sourceMapGenerator->saveMap($sourceMap); + break; } - $sourceMapGenerator = null; - - 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); - } - } - assert($this->scope !== null); - - $out = $this->formatter->format($this->scope, $sourceMapGenerator); - - $prefix = ''; - - if ($this->charset && strlen($out) !== Util::mbStrlen($out)) { - $prefix = '@charset "UTF-8";' . "\n"; - $out = $prefix . $out; - } - - $sourceMap = null; - - if (! empty($out) && $this->sourceMap && $this->sourceMap !== self::SOURCE_MAP_NONE) { - assert($sourceMapGenerator !== null); - $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: - if (isset($this->sourceMapOptions['sourceMapURL'])) { - $sourceMapUrl = $this->sourceMapOptions['sourceMapURL']; - } - break; - } - - if ($sourceMapUrl !== null) { - $out .= sprintf('/*# sourceMappingURL=%s */', $sourceMapUrl); - } - } - } catch (SassScriptException $e) { - throw new CompilerException($this->addLocationToMessage($e->getMessage()), 0, $e); + $out .= sprintf('/*# sourceMappingURL=%s */', $sourceMapUrl); } - $includedFiles = []; - - foreach ($this->resolvedImports as $resolvedImport) { - $includedFiles[$resolvedImport['filePath']] = $resolvedImport['filePath']; - } - - $result = new CompilationResult($out, $sourceMap, array_values($includedFiles)); - if ($this->cache && isset($cacheKey) && isset($compileOptions)) { - $this->cache->setCache('compile', $cacheKey, new CachedResult($result, $this->parsedFiles, $this->resolvedImports), $compileOptions); + $v = [ + 'dependencies' => $this->getParsedFiles(), + 'out' => &$out, + ]; + + $this->cache->setCache("compile", $cacheKey, $v, $compileOptions); } - // Reset state to free memory - // TODO in 2.0, reset parsedFiles as well when the getter is removed. - $this->resolvedImports = []; - $this->importedFiles = []; - - return $result; - } - - /** - * @param CachedResult $result - * - * @return bool - */ - private function isFreshCachedResult(CachedResult $result) - { - // check if any dependency file changed since the result was compiled - foreach ($result->getParsedFiles() as $file => $mtime) { - if (! is_file($file) || filemtime($file) !== $mtime) { - return false; - } - } - - if ($this->cacheCheckImportResolutions) { - $resolvedImports = []; - - foreach ($result->getResolvedImports() as $import) { - $currentDir = $import['currentDir']; - $path = $import['path']; - // store the check across all the results in memory to avoid multiple findImport() on the same path - // with same context. - // this is happening in a same hit with multiple compilations (especially with big frameworks) - if (empty($resolvedImports[$currentDir][$path])) { - $resolvedImports[$currentDir][$path] = $this->findImport($path, $currentDir); - } - - if ($resolvedImports[$currentDir][$path] !== $import['filePath']) { - return false; - } - } - } - - return true; + return $out; } /** * Instantiate parser * - * @param string|null $path + * @param string $path * * @return \ScssPhp\ScssPhp\Parser */ protected function parserFactory($path) { - // 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 ($path !== null && substr($path, -4) === '.css') { - $cssOnly = true; - } - - $parser = new Parser($path, \count($this->sourceNames), $this->encoding, $this->cache, $cssOnly, $this->logger); + $parser = new Parser($path, count($this->sourceNames), $this->encoding, $this->cache); $this->sourceNames[] = $path; $this->addParsedFile($path); @@ -663,12 +331,12 @@ class Compiler * @param array $target * @param array $origin * - * @return bool + * @return boolean */ protected function isSelfExtend($target, $origin) { foreach ($origin as $sel) { - if (\in_array($target, $sel)) { + if (in_array($target, $sel)) { return true; } } @@ -679,15 +347,17 @@ class Compiler /** * Push extends * - * @param string[] $target + * @param array $target * @param array $origin * @param array|null $block - * - * @return void */ protected function pushExtends($target, $origin, $block) { - $i = \count($this->extends); + if ($this->isSelfExtend($target, $origin)) { + return; + } + + $i = count($this->extends); $this->extends[] = [$target, $origin, $block]; foreach ($target as $part) { @@ -702,14 +372,14 @@ class Compiler /** * Make output block * - * @param string|null $type - * @param string[]|null $selectors + * @param string $type + * @param array $selectors * * @return \ScssPhp\ScssPhp\Formatter\OutputBlock */ protected function makeOutputBlock($type, $selectors = null) { - $out = new OutputBlock(); + $out = new OutputBlock; $out->type = $type; $out->lines = []; $out->children = []; @@ -722,9 +392,9 @@ class Compiler $out->sourceLine = $this->env->block->sourceLine; $out->sourceColumn = $this->env->block->sourceColumn; } else { - $out->sourceName = isset($this->sourceNames[$this->sourceIndex]) ? $this->sourceNames[$this->sourceIndex] : '(stdin)'; - $out->sourceLine = $this->sourceLine; - $out->sourceColumn = $this->sourceColumn; + $out->sourceName = null; + $out->sourceLine = null; + $out->sourceColumn = null; } return $out; @@ -734,23 +404,18 @@ class Compiler * Compile root * * @param \ScssPhp\ScssPhp\Block $rootBlock - * - * @return void */ protected function compileRoot(Block $rootBlock) { $this->rootBlock = $this->scope = $this->makeOutputBlock(Type::T_ROOT); $this->compileChildrenNoReturn($rootBlock->children, $this->scope); - assert($this->scope !== null); $this->flattenSelectors($this->scope); $this->missingSelectors(); } /** * Report missing selectors - * - * @return void */ protected function missingSelectors() { @@ -770,7 +435,7 @@ class Compiler $origin = $this->collapseSelectors($origin); $this->sourceLine = $block[Parser::SOURCE_LINE]; - throw $this->error("\"$origin\" failed to @extend \"$target\". The selector \"$target\" was not found."); + $this->throwError("\"$origin\" failed to @extend \"$target\". The selector \"$target\" was not found."); } } @@ -779,8 +444,6 @@ class Compiler * * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $block * @param string $parentKey - * - * @return void */ protected function flattenSelectors(OutputBlock $block, $parentKey = null) { @@ -790,7 +453,7 @@ class Compiler foreach ($block->selectors as $s) { $selectors[] = $s; - if (! \is_array($s)) { + if (! is_array($s)) { continue; } @@ -823,8 +486,7 @@ class Compiler $block->selectors[] = $this->compileSelector($selector); } - if ($placeholderSelector && 0 === \count($block->selectors) && null !== $parentKey) { - assert($block->parent !== null); + if ($placeholderSelector && 0 === count($block->selectors) && null !== $parentKey) { unset($block->parent->children[$parentKey]); return; @@ -837,7 +499,7 @@ class Compiler } /** - * Glue parts of :not( or :nth-child( ... that are in general split in selectors parts + * Glue parts of :not( or :nth-child( ... that are in general splitted in selectors parts * * @param array $parts * @@ -848,20 +510,19 @@ class Compiler $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 ) { - while (\count($new) > 1 && substr($new[\count($new) - 1], -1) !== '(') { + while (count($new)>1 && substr($new[count($new) - 1], -1) !== '(') { $part = array_pop($new) . $part; } - $new[\count($new) - 1] .= $part; + $new[count($new) - 1] .= $part; } else { $new[] = $part; } @@ -874,24 +535,21 @@ class Compiler /** * Match extends * - * @param array $selector - * @param array $out - * @param int $from - * @param bool $initial - * - * @return void + * @param array $selector + * @param array $out + * @param integer $from + * @param boolean $initial */ 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; @@ -899,43 +557,41 @@ class Compiler // 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; } } } $partsPile[] = $part; - if ($this->matchExtendsSingle($part, $origin, $initial)) { - $after = \array_slice($selector, $i + 1); - $before = \array_slice($selector, 0, $i); + $after = array_slice($selector, $i + 1); + $before = array_slice($selector, 0, $i); list($before, $nonBreakableBefore) = $this->extractRelationshipFromFragment($before); 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)) { + if (count($nonBreakableBefore) and $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; } } @@ -947,7 +603,7 @@ class Compiler } } - $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); @@ -966,18 +622,17 @@ class Compiler $this->pushOrMergeExtentedSelector($out, $result); // recursively check for more matches - $startRecurseFrom = \count($before) + min(\count($nonBreakableBefore), \count($mergedBefore)); - - if (\count($origin) > 1) { + $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, $nonBreakabl2) = $this->extractRelationshipFromFragment($afterBefore); @@ -997,8 +652,7 @@ class Compiler } array_pop($partsPile); } - - while (\count($outRecurs)) { + while (count($outRecurs)) { $result = array_shift($outRecurs); $this->pushOrMergeExtentedSelector($out, $result); } @@ -1006,21 +660,16 @@ class Compiler /** * Test a part for being a pseudo selector - * * @param string $part - * @param array $matches - * + * @param array $matches * @return bool */ protected function isPseudoSelector($part, &$matches) { - if ( - strpos($part, ':') === 0 && - preg_match(",^::?([\w-]+)\((.+)\)$,", $part, $matches) - ) { + if (strpos($part, ":") === 0 + && preg_match(",^::?([\w-]+)\((.+)\)$,", $part, $matches)) { return true; } - return false; } @@ -1030,35 +679,25 @@ class Compiler * - 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) { + 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' ]) - ) { + 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) { + if (count($prev) === 1 && count(reset($prev)) === 1) { $single = reset($prev); $part = reset($single); - - if ( - $this->isPseudoSelector($part, $matchesPrev) && - $matchesPrev[1] === $matchesExtended[1] - ) { + if ($this->isPseudoSelector($part, $matchesPrev) + && $matchesPrev[1] === $matchesExtended[1]) { $extended = explode($matchesExtended[1] . '(', $matchesExtended[0], 2); - $extended[1] = $matchesPrev[2] . ', ' . $extended[1]; + $extended[1] = $matchesPrev[2] . ", " . $extended[1]; $extended = implode($matchesExtended[1] . '(', $extended); $extended = [ [ $extended ]]; array_pop($out); @@ -1074,9 +713,9 @@ class Compiler * * @param array $rawSingle * @param array $outOrigin - * @param bool $initial + * @param bool $initial * - * @return bool + * @return boolean */ protected function matchExtendsSingle($rawSingle, &$outOrigin, $initial = true) { @@ -1084,18 +723,18 @@ class Compiler $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; } @@ -1103,7 +742,7 @@ class Compiler $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; } @@ -1117,31 +756,24 @@ class Compiler $counts[$idx] = isset($counts[$idx]) ? $counts[$idx] + 1 : 1; } } - - if ( - $initial && - $this->isPseudoSelector($part, $matches) && - ! \in_array($matches[1], [ 'not' ]) - ) { + if ($initial + && $this->isPseudoSelector($part, $matches) + && ! in_array($matches[1], [ 'not' ])) { $buffer = $matches[2]; $parser = $this->parserFactory(__METHOD__); - - if ($parser->parseSelector($buffer, $subSelectors, false)) { + if ($parser->parseSelector($buffer, $subSelectors)) { 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); + $singleExtended[$k] = str_replace("(".$buffer.")", "($subSelectorsExtended)", $part); $outOrigin[] = [ $singleExtended ]; $found = true; } @@ -1156,7 +788,7 @@ class Compiler $origin = $this->glueFunctionSelectors($origin); // check count - if ($count !== \count($target)) { + if ($count !== count($target)) { continue; } @@ -1166,15 +798,14 @@ class Compiler foreach ($origin as $j => $new) { // prevent infinite loop when target extends itself - if ($this->isSelfExtend($single, $origin) && ! $initial) { + if ($this->isSelfExtend($single, $origin)) { 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]); @@ -1183,8 +814,8 @@ class Compiler $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; } } @@ -1213,11 +844,11 @@ class Compiler $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); + $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])) { @@ -1243,15 +874,8 @@ class Compiler $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 ($single as $part) { if (preg_match('/^[\[:]/', $part)) { $out[] = $part; @@ -1259,26 +883,21 @@ class Compiler } elseif (preg_match('/^[\.#]/', $part)) { array_unshift($out, $part); $wasTag = false; - } elseif (preg_match('/^[^_-]/', $part) && $rang === 1) { + } elseif (preg_match('/^[^_-]/', $part)) { $tag[] = $part; $wasTag = true; } elseif ($wasTag) { - $tag[\count($tag) - 1] .= $part; + $tag[count($tag) - 1] .= $part; } else { - array_unshift($out, $part); + $out[] = $part; } - $rang--; } } - if (\count($tag)) { + if (count($tag)) { array_unshift($out, $tag[0]); } - while (\count($pseudo)) { - $out[] = array_shift($pseudo); - } - return $out; } @@ -1286,18 +905,14 @@ class Compiler * Compile media * * @param \ScssPhp\ScssPhp\Block $media - * - * @return void */ protected function compileMedia(Block $media) { - assert($media instanceof MediaBlock); $this->pushEnv($media); $mediaQueries = $this->compileMediaQuery($this->multiplyMedia($this->env)); - if (! empty($mediaQueries)) { - assert($this->scope !== null); + if (! empty($mediaQueries) && $mediaQueries) { $previousScope = $this->scope; $parentScope = $this->mediaParent($this->scope); @@ -1314,8 +929,7 @@ class Compiler 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 @@ -1326,7 +940,7 @@ class Compiler } if ($needsWrap) { - $wrapped = new Block(); + $wrapped = new Block; $wrapped->sourceName = $media->sourceName; $wrapped->sourceIndex = $media->sourceIndex; $wrapped->sourceLine = $media->sourceLine; @@ -1337,6 +951,30 @@ class Compiler $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); @@ -1370,33 +1008,18 @@ class Compiler /** * Compile directive * - * @param DirectiveBlock|array $directive + * @param \ScssPhp\ScssPhp\Block|array $block * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $out - * - * @return void */ protected function compileDirective($directive, OutputBlock $out) { - if (\is_array($directive)) { - $directiveName = $this->compileDirectiveName($directive[0]); - $s = '@' . $directiveName; - + if (is_array($directive)) { + $s = '@' . $directive[0]; 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 (empty($directive[3])) { - $this->appendRootDirective($s . ';' . $appendNewLine, $out, [Type::T_COMMENT, Type::T_DIRECTIVE]); - } else { - $this->appendOutputLine($out, Type::T_DIRECTIVE, $s . ';'); - } + $this->appendRootDirective($s . ';', $out); } else { - $directive->name = $this->compileDirectiveName($directive->name); $s = '@' . $directive->name; if (! empty($directive->value)) { @@ -1411,39 +1034,20 @@ class Compiler } } - /** - * directive names can include some interpolation - * - * @param string|array $directiveName - * @return 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) { - assert($block instanceof AtRootBlock); $env = $this->pushEnv($block); $envs = $this->compactEnv($env); list($with, $without) = $this->compileWith(isset($block->with) ? $block->with : null); // wrap inline selector if ($block->selector) { - $wrapped = new Block(); + $wrapped = new Block; $wrapped->sourceName = $block->sourceName; $wrapped->sourceIndex = $block->sourceIndex; $wrapped->sourceLine = $block->sourceLine; @@ -1459,11 +1063,8 @@ class Compiler } $selfParent = $block->selfParent; - assert($selfParent !== null, 'at-root blocks must have a selfParent set.'); - if ( - ! $selfParent->selectors && - isset($block->parent) && + if (! $block->selfParent->selectors && isset($block->parent) && $block->parent && isset($block->parent->selectors) && $block->parent->selectors ) { $selfParent = $block->parent; @@ -1471,15 +1072,13 @@ class Compiler $this->env = $this->filterWithWithout($envs, $with, $without); - assert($this->scope !== null); $saveScope = $this->scope; $this->scope = $this->filterScopeWithWithout($saveScope, $with, $without); // propagate selfParent to the children where they still can be useful $this->compileChildrenNoReturn($block->children, $this->scope, $selfParent); - assert($this->scope !== null); - $this->completeScope($this->scope, $saveScope); + $this->scope = $this->completeScope($this->scope, $saveScope); $this->scope = $saveScope; $this->env = $this->extractEnv($envs); @@ -1487,26 +1086,25 @@ class Compiler } /** - * Filter at-root scope depending on with/without option + * Filter at-root scope depending of with/without option * * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $scope * @param array $with * @param array $without * - * @return OutputBlock + * @return mixed */ protected function filterScopeWithWithout($scope, $with, $without) { $filteredScopes = []; $childStash = []; - if ($scope->type === Type::T_ROOT) { + if ($scope->type === TYPE::T_ROOT) { return $scope; } - assert($this->rootBlock !== null); // start from the root - while ($scope->parent && $scope->parent->type !== Type::T_ROOT) { + while ($scope->parent && $scope->parent->type !== TYPE::T_ROOT) { array_unshift($childStash, $scope); $scope = $scope->parent; } @@ -1529,7 +1127,7 @@ class Compiler $filteredScopes[] = $s; } - if (\count($childStash)) { + if (count($childStash)) { $scope = array_shift($childStash); } elseif ($scope->children) { $scope = end($scope->children); @@ -1538,7 +1136,7 @@ class Compiler } } - if (! \count($filteredScopes)) { + if (! count($filteredScopes)) { return $this->rootBlock; } @@ -1549,7 +1147,7 @@ class Compiler $p = &$newScope; - while (\count($filteredScopes)) { + while (count($filteredScopes)) { $s = array_shift($filteredScopes); $s->parent = $p; $p->children[] = $s; @@ -1567,11 +1165,11 @@ class Compiler * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $scope * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $previousScope * - * @return OutputBlock + * @return mixed */ protected function completeScope($scope, $previousScope) { - if (! $scope->type && ! $scope->selectors && \count($scope->lines)) { + if (! $scope->type && (! $scope->selectors || ! count($scope->selectors)) && count($scope->lines)) { $scope->selectors = $this->findScopeSelectors($previousScope, $scope->depth); } @@ -1588,7 +1186,7 @@ class Compiler * Find a selector by the depth node in the scope * * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $scope - * @param int $depth + * @param integer $depth * * @return array */ @@ -1612,11 +1210,9 @@ class Compiler /** * Compile @at-root's with: inclusion / without: exclusion into 2 lists uses to filter scope/env later * - * @param array|null $withCondition + * @param array $withCondition * * @return array - * - * @phpstan-return array{array, array} */ protected function compileWith($withCondition) { @@ -1625,21 +1221,9 @@ class Compiler $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; - } - } - - $withConfig = $this->mapGet($withCondition, static::$with); - if ($withConfig !== null) { + if ($this->libMapHasKey([$withCondition, static::$with])) { $without = []; // cancel the default - $list = $this->coerceList($withConfig); + $list = $this->coerceList($this->libMapGet([$withCondition, static::$with])); foreach ($list[2] as $item) { $keyword = $this->compileStringContent($this->coerceString($item)); @@ -1648,10 +1232,9 @@ class Compiler } } - $withoutConfig = $this->mapGet($withCondition, static::$without); - if ($withoutConfig !== null) { + if ($this->libMapHasKey([$withCondition, static::$without])) { $without = []; // cancel the default - $list = $this->coerceList($withoutConfig); + $list = $this->coerceList($this->libMapGet([$withCondition, static::$without])); foreach ($list[2] as $item) { $keyword = $this->compileStringContent($this->coerceString($item)); @@ -1667,13 +1250,11 @@ class Compiler /** * Filter env stack * - * @param Environment[] $envs + * @param array $envs * @param array $with * @param array $without * - * @return Environment - * - * @phpstan-param non-empty-array $envs + * @return \ScssPhp\ScssPhp\Compiler\Environment */ protected function filterWithWithout($envs, $with, $without) { @@ -1701,7 +1282,7 @@ class Compiler * @param array $with * @param array $without * - * @return bool + * @return boolean */ protected function isWith($block, $with, $without) { @@ -1711,9 +1292,8 @@ class Compiler } if ($block->type === Type::T_DIRECTIVE) { - assert($block instanceof DirectiveBlock || $block instanceof OutputBlock); if (isset($block->name)) { - return $this->testWithWithout($this->compileDirectiveName($block->name), $with, $without); + return $this->testWithWithout($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 { @@ -1722,14 +1302,14 @@ class Compiler } } 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) && $s instanceof Number) { + if (is_object($s) && $s instanceof Node\Number) { return $this->testWithWithout('keyframes', $with, $without); } } @@ -1747,13 +1327,14 @@ class Compiler * @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; } @@ -1766,9 +1347,7 @@ class Compiler * Compile keyframe block * * @param \ScssPhp\ScssPhp\Block $block - * @param string[] $selectors - * - * @return void + * @param array $selectors */ protected function compileKeyframeBlock(Block $block, $selectors) { @@ -1782,12 +1361,10 @@ class Compiler $this->scope = $this->makeOutputBlock($block->type, $selectors); $this->scope->depth = 1; - assert($this->scope->parent !== null); $this->scope->parent->children[] = $this->scope; $this->compileChildrenNoReturn($block->children, $this->scope); - assert($this->scope !== null); $this->scope = $this->scope->parent; $this->env = $this->extractEnv($envs); @@ -1797,14 +1374,11 @@ class Compiler /** * Compile nested properties lines * - * @param \ScssPhp\ScssPhp\Block $block - * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $out - * - * @return void + * @param \ScssPhp\ScssPhp\Block $block + * @param OutputBlock $out */ protected function compileNestedPropertiesBlock(Block $block, OutputBlock $out) { - assert($block instanceof NestedPropertyBlock); $prefix = $this->compileValue($block->prefix) . '-'; $nested = $this->makeOutputBlock($block->type); @@ -1823,7 +1397,6 @@ class Compiler break; case Type::T_NESTED_PROPERTY: - assert($child[1] instanceof NestedPropertyBlock); array_unshift($child[1]->prefix[2], $prefix); break; } @@ -1836,21 +1409,18 @@ class Compiler * Compile nested block * * @param \ScssPhp\ScssPhp\Block $block - * @param string[] $selectors - * - * @return void + * @param array $selectors */ protected function compileNestedBlock(Block $block, $selectors) { $this->pushEnv($block); $this->scope = $this->makeOutputBlock($block->type, $selectors); - assert($this->scope->parent !== null); $this->scope->parent->children[] = $this->scope; // wrap assign children in a block // except for @font-face - if (!$block instanceof DirectiveBlock || $this->compileDirectiveName($block->name) !== 'font-face') { + if ($block->type !== Type::T_DIRECTIVE || $block->name !== "font-face") { // need wrapping? $needWrapping = false; @@ -1862,7 +1432,7 @@ class Compiler } if ($needWrapping) { - $wrapped = new Block(); + $wrapped = new Block; $wrapped->sourceName = $block->sourceName; $wrapped->sourceIndex = $block->sourceIndex; $wrapped->sourceLine = $block->sourceLine; @@ -1879,7 +1449,6 @@ class Compiler $this->compileChildrenNoReturn($block->children, $this->scope); - assert($this->scope !== null); $this->scope = $this->scope->parent; $this->popEnv(); @@ -1902,21 +1471,41 @@ class Compiler * @see Compiler::compileChild() * * @param \ScssPhp\ScssPhp\Block $block - * - * @return void */ protected function compileBlock(Block $block) { $env = $this->pushEnv($block); - assert($block->selectors !== null); $env->selectors = $this->evalSelectors($block->selectors); $out = $this->makeOutputBlock(null); - assert($this->scope !== 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 @@ -1931,7 +1520,6 @@ class Compiler // and revert for the following children of the same block if ($selfParentSelectors) { - assert($block->selfParent !== null); $block->selfParent->selectors = $selfParentSelectors; } } @@ -1943,10 +1531,10 @@ class Compiler /** * Compile the value of a comment that can have interpolation * - * @param array $value - * @param bool $pushEnv + * @param array $value + * @param boolean $pushEnv * - * @return string + * @return array|mixed|string */ protected function compileCommentValue($value, $pushEnv = false) { @@ -1955,19 +1543,18 @@ class Compiler if (isset($value[2])) { if ($pushEnv) { $this->pushEnv(); + $storeEnv = $this->storeEnv; + $this->storeEnv = $this->env; } try { $c = $this->compileValue($value[2]); - } catch (SassScriptException $e) { - $this->logger->warn('Ignoring interpolation errors in multiline comments is deprecated and will be removed in ScssPhp 2.0. ' . $this->addLocationToMessage($e->getMessage()), true); - // ignore error in comment compilation which are only interpolation - } catch (SassException $e) { - $this->logger->warn('Ignoring interpolation errors in multiline comments is deprecated and will be removed in ScssPhp 2.0. ' . $e->getMessage(), true); + } catch (\Exception $e) { // ignore error in comment compilation which are only interpolation } if ($pushEnv) { + $this->storeEnv = $storeEnv; $this->popEnv(); } } @@ -1979,15 +1566,12 @@ class Compiler * 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); - assert($this->scope !== null); $this->scope->children[] = $out; } @@ -2002,25 +1586,15 @@ class Compiler { $this->shouldEvaluate = false; - $evaluatedSelectors = []; - foreach ($selectors as $selector) { - $evaluatedSelectors[] = $this->evalSelector($selector); - } - $selectors = $evaluatedSelectors; + $selectors = array_map([$this, 'evalSelector'], $selectors); // after evaluating interpolates, we might need a second pass if ($this->shouldEvaluate) { - $selectors = $this->replaceSelfSelector($selectors, '&'); + $selectors = $this->revertSelfSelector($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 ($isValid) { + if ($parser->parseSelector($buffer, $newSelectors)) { $selectors = array_map([$this, 'evalSelector'], $newSelectors); } } @@ -2034,8 +1608,6 @@ class Compiler * @param array $selector * * @return array - * - * @phpstan-impure */ protected function evalSelector($selector) { @@ -2048,23 +1620,20 @@ class Compiler * @param array $part * * @return array - * - * @phpstan-impure */ 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 self char or non standard char - if (preg_match(',[^\w-],', $p)) { + // force re-evaluation + if (strpos($p, '&') !== false || strpos($p, ',') !== false) { $this->shouldEvaluate = true; } - } elseif ( - \is_string($p) && \strlen($p) >= 2 && - ($p[0] === '"' || $p[0] === "'") && - substr($p, -1) === $p[0] + } elseif (is_string($p) && strlen($p) >= 2 && + ($first = $p[0]) && ($first === '"' || $first === "'") && + substr($p, -1) === $first ) { $p = substr($p, 1, -1); } @@ -2076,44 +1645,14 @@ class Compiler /** * Collapse selectors * - * @param array $selectors + * @param array $selectors + * @param boolean $selectorFormat + * if false return a collapsed string + * if true return an array description of a structured selector * * @return string */ - protected function collapseSelectors($selectors) - { - $parts = []; - - foreach ($selectors as $selector) { - $output = []; - - foreach ($selector as $node) { - $compound = ''; - - array_walk_recursive( - $node, - function ($value, $key) use (&$compound) { - $compound .= $value; - } - ); - - $output[] = $compound; - } - - $parts[] = implode(' ', $output); - } - - return implode(', ', $parts); - } - - /** - * Collapse selectors - * - * @param array $selectors - * - * @return array - */ - private function collapseSelectorsAsList($selectors) + protected function collapseSelectors($selectors, $selectorFormat = false) { $parts = []; @@ -2131,52 +1670,59 @@ class Compiler } ); - if ($this->isImmediateRelationshipCombinator($compound)) { - if (\count($output)) { - $output[\count($output) - 1] .= ' ' . $compound; + if ($selectorFormat && $this->isImmediateRelationshipCombinator($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; } } - foreach ($output as &$o) { - $o = [Type::T_STRING, '', [$o]]; + if ($selectorFormat) { + foreach ($output as &$o) { + $o = [Type::T_STRING, '', [$o]]; + } + + $output = [Type::T_LIST, ' ', $output]; + } else { + $output = implode(' ', $output); } - $parts[] = [Type::T_LIST, ' ', $output]; + $parts[] = $output; } - return [Type::T_LIST, ',', $parts]; + if ($selectorFormat) { + $parts = [Type::T_LIST, ',', $parts]; + } else { + $parts = implode(', ', $parts); + } + + return $parts; } /** * Parse down the selector and revert [self] to "&" before a reparsing * - * @param array $selectors - * @param string|null $replace + * @param array $selectors * * @return array */ - protected function replaceSelfSelector($selectors, $replace = null) + protected function revertSelfSelector($selectors) { foreach ($selectors as &$part) { - if (\is_array($part)) { + if (is_array($part)) { if ($part === [Type::T_SELF]) { - if (\is_null($replace)) { - $replace = $this->reduce([Type::T_SELF]); - $replace = $this->compileValue($replace); - } - $part = $replace; + $part = '&'; } else { - $part = $this->replaceSelfSelector($part, $replace); + $part = $this->revertSelfSelector($part); } } } @@ -2196,19 +1742,18 @@ class Compiler $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; } } @@ -2224,7 +1769,7 @@ class Compiler */ protected function compileSelector($selector) { - if (! \is_array($selector)) { + if (! is_array($selector)) { return $selector; // media and the like } @@ -2247,7 +1792,7 @@ class Compiler protected function compileSelectorPart($piece) { foreach ($piece as &$p) { - if (! \is_array($p)) { + if (! is_array($p)) { continue; } @@ -2270,17 +1815,17 @@ class Compiler * * @param array $selector * - * @return bool + * @return boolean */ 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; } } @@ -2289,11 +1834,6 @@ class Compiler return false; } - /** - * @param string $name - * - * @return void - */ protected function pushCallStack($name = '') { $this->callStack[] = [ @@ -2304,18 +1844,15 @@ class Compiler ]; // 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'; + $msg = "Infinite calling loop"; - throw $this->error($msg); + $this->throwError($msg); } } - /** - * @return void - */ protected function popCallStack() { array_pop($this->callStack); @@ -2328,7 +1865,7 @@ class Compiler * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $out * @param string $traceName * - * @return array|Number|null + * @return array|null */ protected function compileChildren($stms, OutputBlock $out, $traceName = '') { @@ -2338,8 +1875,6 @@ class Compiler $ret = $this->compileChild($stm, $out); if (isset($ret)) { - $this->popCallStack(); - return $ret; } } @@ -2350,15 +1885,13 @@ class Compiler } /** - * 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 = '') @@ -2366,11 +1899,11 @@ class Compiler $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']); @@ -2379,7 +1912,9 @@ class Compiler } if (isset($ret)) { - throw $this->error('@return may only be used within a function'); + $this->throwError('@return may only be used within a function'); + + return; } } @@ -2388,7 +1923,7 @@ class Compiler /** - * evaluate media query : compile internal value keeping the structure unchanged + * evaluate media query : compile internal value keeping the structure inchanged * * @param array $queryList * @@ -2404,13 +1939,12 @@ class Compiler $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 || @@ -2424,21 +1958,21 @@ class Compiler } if ($shouldReparse) { - if (\is_null($parser)) { + if (is_null($parser)) { $parser = $this->parserFactory(__METHOD__); } $queryString = $this->compileMediaQuery([$queryList[$kql]]); $queryString = reset($queryString); - if ($queryString !== false && strpos($queryString, '@media ') === 0) { + 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); } @@ -2458,14 +1992,14 @@ class Compiler * * @param array $queryList * - * @return string[] + * @return array */ protected function compileMediaQuery($queryList) { $start = '@media '; $default = trim($start); $out = []; - $current = ''; + $current = ""; foreach ($queryList as $query) { $type = null; @@ -2483,17 +2017,17 @@ class Compiler 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; } @@ -2504,7 +2038,7 @@ class Compiler $out[] = $start . $current; } - $current = ''; + $current = ""; $type = null; $parts = []; } @@ -2556,7 +2090,7 @@ class Compiler } if (! empty($parts)) { - if (\strlen($current)) { + if (strlen($current)) { $current .= $this->formatter->tagSeparator; } @@ -2638,7 +2172,7 @@ class Compiler return $type1; } - if (\count($type1) > 1) { + if (count($type1) > 1) { $m1 = strtolower($type1[0]); $t1 = strtolower($type1[1]); } else { @@ -2646,7 +2180,7 @@ class Compiler $t1 = strtolower($type1[0]); } - if (\count($type2) > 1) { + if (count($type2) > 1) { $m2 = strtolower($type2[0]); $t2 = strtolower($type2[1]); } else { @@ -2679,7 +2213,7 @@ class Compiler } // t1 == t2, neither m1 nor m2 are "not" - return [empty($m1) ? $m2 : $m1, $t1]; + return [empty($m1)? $m2 : $m1, $t1]; } /** @@ -2687,40 +2221,38 @@ class Compiler * * @param array $rawPath * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $out - * @param bool $once + * @param boolean $once * - * @return bool + * @return boolean */ protected function compileImport($rawPath, OutputBlock $out, $once = false) { if ($rawPath[0] === Type::T_STRING) { $path = $this->compileStringContent($rawPath); - if (strpos($path, 'url(') !== 0 && $filePath = $this->findImport($path, $this->currentDirectory)) { - $this->registerImport($this->currentDirectory, $path, $filePath); - - if (! $once || ! \in_array($filePath, $this->importedFiles)) { - $this->importFile($filePath, $out); - $this->importedFiles[] = $filePath; + if ($path = $this->findImport($path)) { + if (! $once || ! in_array($path, $this->importedFiles)) { + $this->importFile($path, $out); + $this->importedFiles[] = $path; } return true; } - $this->appendRootDirective('@import ' . $this->compileImportPath($rawPath) . ';', $out); + $this->appendRootDirective('@import ' . $this->compileValue($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->compileImportPath($rawPath) . ';', $out); + $this->appendRootDirective('@import ' . $this->compileValue($rawPath) . ';', $out); return false; } @@ -2733,68 +2265,19 @@ class Compiler return true; } - $this->appendRootDirective('@import ' . $this->compileImportPath($rawPath) . ';', $out); + $this->appendRootDirective('@import ' . $this->compileValue($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 \ScssPhp\ScssPhp\Formatter\OutputBlock $out - * @param array $allowed - * - * @return void + * @param string $line + * @param @param \ScssPhp\ScssPhp\Formatter\OutputBlock $out + * @param array $allowed */ protected function appendRootDirective($line, $out, $allowed = [Type::T_COMMENT]) { @@ -2806,8 +2289,8 @@ class Compiler $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; } @@ -2817,21 +2300,21 @@ class Compiler // 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] ?: '(stdin)'; + $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); } } @@ -2842,23 +2325,25 @@ class Compiler * * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $out * @param string $type - * @param string $line - * - * @return void + * @param string|mixed $line */ protected function appendOutputLine(OutputBlock $out, $type, $line) { $outWrite = &$out; - // check if it's a flat output or not - if (\count($out->children)) { - $lastChild = &$out->children[\count($out->children) - 1]; + if ($type === Type::T_COMMENT) { + $parent = $out->parent; - if ( - $lastChild->depth === $out->depth && - \is_null($lastChild->selectors) && - ! \count($lastChild->children) - ) { + if (end($parent->children) !== $out) { + $outWrite = &$parent->children[count($parent->children) - 1]; + } + } + + // check if it's a flat output or not + if (count($out->children)) { + $lastChild = &$out->children[count($out->children) - 1]; + + if ($lastChild->depth === $out->depth && is_null($lastChild->selectors) && ! count($lastChild->children)) { $outWrite = $lastChild; } else { $nextLines = $this->makeOutputBlock($type); @@ -2879,7 +2364,7 @@ class Compiler * @param array $child * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $out * - * @return array|Number|null + * @return array */ protected function compileChild($child, OutputBlock $out) { @@ -2887,19 +2372,18 @@ class Compiler $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) && $child[1] instanceof Block) { + } 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; - $sourceIndex = array_search($out->sourceName, $this->sourceNames); + $this->sourceIndex = array_search($out->sourceName, $this->sourceNames); $this->sourceColumn = $out->sourceColumn; - if ($sourceIndex === false) { - $sourceIndex = null; + if ($this->sourceIndex === false) { + $this->sourceIndex = null; } - $this->sourceIndex = $sourceIndex; } switch ($child[0]) { @@ -2932,30 +2416,10 @@ class Compiler break; case Type::T_CHARSET: - 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; - } + if (! $this->charsetSeen) { + $this->charsetSeen = true; + $this->appendRootDirective('@charset ' . $this->compileValue($child[1]) . ';', $out); } - - $compiledValue = $this->compileValue($value); - - $line = $this->formatter->customProperty( - $compiledName, - $compiledValue - ); - - $this->appendOutputLine($out, Type::T_ASSIGN, $line); break; case Type::T_ASSIGN: @@ -2963,8 +2427,8 @@ class Compiler if ($name[0] === Type::T_VARIABLE) { $flags = isset($child[3]) ? $child[3] : []; - $isDefault = \in_array('!default', $flags); - $isGlobal = \in_array('!global', $flags); + $isDefault = in_array('!default', $flags); + $isGlobal = in_array('!global', $flags); if ($isGlobal) { $this->set($name[1], $this->reduce($value), false, $this->rootEnv, $value); @@ -2972,7 +2436,7 @@ class Compiler } $shouldSet = $isDefault && - (\is_null($result = $this->get($name[1], false)) || + (is_null($result = $this->get($name[1], false)) || $result === static::$null); if (! $isDefault || $shouldSet) { @@ -2984,7 +2448,7 @@ class Compiler $compiledName = $this->compileValue($name); // handle shorthand syntaxes : size / line-height... - if (\in_array($compiledName, ['font', 'grid-row', 'grid-column', 'border-radius'])) { + 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 @@ -3004,7 +2468,7 @@ class Compiler break; } - if ($compiledName === 'font' && $value[0] === Type::T_LIST && $value[1] === ',') { + if ($compiledName === 'font' and $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 $shorthandValue=&$value[2][0]; @@ -3016,11 +2480,11 @@ class Compiler if ($shorthandDividerNeedsUnit) { $divider = $shorthandValue[3]; - if (\is_array($divider)) { + if (is_array($divider)) { $divider = $this->reduce($divider, true); } - if ($divider instanceof Number && \intval($divider->getDimension()) && $divider->unitless()) { + if (intval($divider->dimension) and !count($divider->units)) { $revert = false; } } @@ -3033,18 +2497,17 @@ class Compiler if ($item[0] === Type::T_EXPRESSION && $item[1] === '/') { 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 (is_null($maxListElements) or count($shorthandValue[2]) <= $maxListElements) { if ($shorthandDividerNeedsUnit) { $divider = $item[3]; - if (\is_array($divider)) { + if (is_array($divider)) { $divider = $this->reduce($divider, true); } - if ($divider instanceof Number && \intval($divider->getDimension()) && $divider->unitless()) { + if (intval($divider->dimension) and !count($divider->units)) { $revert = false; } } @@ -3072,14 +2535,11 @@ class Compiler $compiledValue = $this->compileValue($value); - // ignore empty value - if (\strlen($compiledValue)) { - $line = $this->formatter->property( - $compiledName, - $compiledValue - ); - $this->appendOutputLine($out, Type::T_ASSIGN, $line); - } + $line = $this->formatter->property( + $compiledName, + $compiledValue + ); + $this->appendOutputLine($out, Type::T_ASSIGN, $line); break; case Type::T_COMMENT: @@ -3095,7 +2555,6 @@ class Compiler case Type::T_MIXIN: case Type::T_FUNCTION: list(, $block) = $child; - assert($block instanceof CallableBlock); // 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); @@ -3103,42 +2562,16 @@ class Compiler case Type::T_EXTEND: foreach ($child[1] as $sel) { - $replacedSel = $this->replaceSelfSelector($sel); - - if ($replacedSel !== $sel) { - throw $this->error('Parent selectors aren\'t allowed here.'); - } - $results = $this->evalSelectors([$sel]); foreach ($results as $result) { - if (\count($result) !== 1) { - throw $this->error('complex selectors may not be extended.'); - } - // only use the first one - $result = $result[0]; + $result = current($result); $selectors = $out->selectors; if (! $selectors && isset($child['selfParent'])) { $selectors = $this->multiplySelectors($this->env, $child['selfParent']); } - assert($selectors !== null); - - if (\count($result) > 1) { - $replacement = implode(', ', $result); - $fname = $this->getPrettyPath($this->sourceNames[$this->sourceIndex]); - $line = $this->sourceLine; - - $message = <<logger->warn($message); - } $this->pushExtends($result, $selectors, $child); } @@ -3147,16 +2580,14 @@ EOL; case Type::T_IF: list(, $if) = $child; - assert($if instanceof IfBlock); if ($this->isTruthy($this->reduce($if->cond, true))) { return $this->compileChildren($if->children, $out); } foreach ($if->cases as $case) { - if ( - $case instanceof ElseBlock || - $case instanceof ElseifBlock && $this->isTruthy($this->reduce($case->cond)) + if ($case->type === Type::T_ELSE || + $case->type === Type::T_ELSEIF && $this->isTruthy($this->reduce($case->cond)) ) { return $this->compileChildren($case->children, $out); } @@ -3165,14 +2596,13 @@ EOL; case Type::T_EACH: list(, $each) = $child; - assert($each instanceof EachBlock); - $list = $this->coerceList($this->reduce($each->list), ',', true); + $list = $this->coerceList($this->reduce($each->list)); $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); @@ -3185,78 +2615,87 @@ EOL; $ret = $this->compileChildren($each->children, $out); if ($ret) { - $store = $this->env->store; - $this->popEnv(); - $this->backPropagateEnv($store, $each->vars); + if ($ret[0] !== Type::T_CONTROL) { + $this->popEnv(); - return $ret; + return $ret; + } + + if ($ret[1]) { + break; + } } } - $store = $this->env->store; - $this->popEnv(); - $this->backPropagateEnv($store, $each->vars); + $this->popEnv(); break; case Type::T_WHILE: list(, $while) = $child; - assert($while instanceof WhileBlock); while ($this->isTruthy($this->reduce($while->cond, true))) { $ret = $this->compileChildren($while->children, $out); if ($ret) { - return $ret; + if ($ret[0] !== Type::T_CONTROL) { + return $ret; + } + + if ($ret[1]) { + break; + } } } break; case Type::T_FOR: list(, $for) = $child; - assert($for instanceof ForBlock); - $startNumber = $this->assertNumber($this->reduce($for->start, true)); - $endNumber = $this->assertNumber($this->reduce($for->end, true)); + $start = $this->reduce($for->start, true); + $end = $this->reduce($for->end, true); - $start = $this->assertInteger($startNumber); + if (! ($start[2] == $end[2] || $end->unitless())) { + $this->throwError('Incompatible units: "%s" and "%s".', $start->unitStr(), $end->unitStr()); - $numeratorUnits = $startNumber->getNumeratorUnits(); - $denominatorUnits = $startNumber->getDenominatorUnits(); + break; + } - $end = $this->assertInteger($endNumber->coerce($numeratorUnits, $denominatorUnits)); + $unit = $start[2]; + $start = $start[1]; + $end = $end[1]; $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 Number($start, $numeratorUnits, $denominatorUnits)); + $this->set($for->var, new Node\Number($start, $unit)); $start += $d; $ret = $this->compileChildren($for->children, $out); if ($ret) { - $store = $this->env->store; - $this->popEnv(); - $this->backPropagateEnv($store, [$for->var]); + if ($ret[0] !== Type::T_CONTROL) { + return $ret; + } - return $ret; + if ($ret[1]) { + break; + } } } - - $store = $this->env->store; - $this->popEnv(); - $this->backPropagateEnv($store, [$for->var]); - break; + case Type::T_BREAK: + return [Type::T_CONTROL, true]; + + case Type::T_CONTINUE: + return [Type::T_CONTROL, false]; + case Type::T_RETURN: return $this->reduce($child[1], true); @@ -3271,22 +2710,24 @@ EOL; $mixin = $this->get(static::$namespaces['mixin'] . $name, false); if (! $mixin) { - throw $this->error("Undefined mixin $name"); + $this->throwError("Undefined mixin $name"); + break; } - assert($mixin instanceof CallableBlock); - $callingScope = $this->getStoreEnv(); // push scope, apply args $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; - if (isset($child['selfParent']) && $child['selfParent'] instanceof Block && isset($child['selfParent']->selectors)) { + if (isset($child['selfParent']) && isset($child['selfParent']->selectors)) { $selfParent = $child['selfParent']; } else { $parentSelectors = $this->multiplySelectors($this->env); @@ -3296,7 +2737,7 @@ EOL; $parent->selectors = $parentSelectors; foreach ($mixin->children as $k => $child) { - if (isset($child[1]) && $child[1] instanceof Block) { + if (isset($child[1]) && is_object($child[1]) && $child[1] instanceof Block) { $mixin->children[$k][1]->parent = $parent; } } @@ -3330,10 +2771,12 @@ EOL; if (! empty($mixin->parentEnv)) { $this->env->declarationScopeParent = $mixin->parentEnv; } else { - throw $this->error("@mixin $name() without parentEnv"); + $this->throwError("@mixin $name() without parentEnv"); } - $this->compileChildrenNoReturn($mixin->children, $out, $selfParent, $this->env->marker . ' ' . $name); + $this->compileChildrenNoReturn($mixin->children, $out, $selfParent, $this->env->marker . " " . $name); + + $this->storeEnv = $storeEnv; $this->popEnv(); break; @@ -3345,6 +2788,9 @@ EOL; $argContent = $child[1]; if (! $content) { + $content = new \stdClass(); + $content->scope = new \stdClass(); + $content->children = $env->parent->block->children; break; } @@ -3353,7 +2799,7 @@ EOL; if (isset($argUsing) && isset($argContent)) { // Get the arguments provided for the content with the names provided in the "using" argument list - $this->storeEnv = null; + $this->storeEnv = $this->env; $varsUsing = $this->applyArguments($argUsing, $argContent, false); } @@ -3373,58 +2819,54 @@ EOL; case Type::T_DEBUG: list(, $value) = $child; - $fname = $this->getPrettyPath($this->sourceNames[$this->sourceIndex]); + $fname = $this->sourceNames[$this->sourceIndex]; $line = $this->sourceLine; - $value = $this->compileDebugValue($value); + $value = $this->compileValue($this->reduce($value, true)); - $this->logger->debug("$fname:$line DEBUG: $value"); + fwrite($this->stderr, "File $fname on line $line DEBUG: $value\n"); break; case Type::T_WARN: list(, $value) = $child; - $fname = $this->getPrettyPath($this->sourceNames[$this->sourceIndex]); + $fname = $this->sourceNames[$this->sourceIndex]; $line = $this->sourceLine; - $value = $this->compileDebugValue($value); + $value = $this->compileValue($this->reduce($value, true)); - $this->logger->warn("$value\n on line $line of $fname"); + fwrite($this->stderr, "File $fname on line $line WARN: $value\n"); break; case Type::T_ERROR: list(, $value) = $child; - $fname = $this->getPrettyPath($this->sourceNames[$this->sourceIndex]); + $fname = $this->sourceNames[$this->sourceIndex]; $line = $this->sourceLine; $value = $this->compileValue($this->reduce($value, true)); - throw $this->error("File $fname on line $line ERROR: $value\n"); + $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; default: - throw $this->error("unknown child type: $child[0]"); + $this->throwError("unknown child type: $child[0]"); } - - return null; } /** * Reduce expression to string * * @param array $exp - * @param bool $keepParens * * @return array */ - protected function expToString($exp, $keepParens = false) + protected function expToString($exp) { - list(, $op, $left, $right, $inParens, $whiteLeft, $whiteRight) = $exp; + list(, $op, $left, $right, /* $inParens */, $whiteLeft, $whiteRight) = $exp; - $content = []; - - if ($keepParens && $inParens) { - $content[] = '('; - } - - $content[] = $this->reduce($left); + $content = [$this->reduce($left)]; if ($whiteLeft) { $content[] = ' '; @@ -3438,21 +2880,17 @@ EOL; $content[] = $this->reduce($right); - if ($keepParens && $inParens) { - $content[] = ')'; - } - return [Type::T_STRING, '', $content]; } /** * Is truthy? * - * @param array|Number $value + * @param array $value * - * @return bool + * @return boolean */ - public function isTruthy($value) + protected function isTruthy($value) { return $value !== static::$false && $value !== static::$null; } @@ -3462,7 +2900,7 @@ EOL; * * @param string $value * - * @return bool + * @return boolean */ protected function isImmediateRelationshipCombinator($value) { @@ -3474,7 +2912,7 @@ EOL; * * @param array $value * - * @return bool + * @return boolean */ protected function shouldEval($value) { @@ -3496,15 +2934,15 @@ EOL; /** * Reduce value * - * @param array|Number $value - * @param bool $inExp + * @param array $value + * @param boolean $inExp * - * @return array|Number + * @return null|string|array|\ScssPhp\ScssPhp\Node\Number */ protected function reduce($value, $inExp = false) { - if ($value instanceof Number) { - return $value; + if (is_null($value)) { + return null; } switch ($value[0]) { @@ -3521,9 +2959,8 @@ EOL; } // special case: looks like css shorthand - if ( - $opName == 'div' && ! $inParens && ! $inExp && - (($right[0] !== Type::T_NUMBER && isset($right[2]) && $right[2] != '') || + if ($opName == 'div' && ! $inParens && ! $inExp && isset($right[2]) && + (($right[0] !== Type::T_NUMBER && $right[2] != '') || ($right[0] === Type::T_NUMBER && ! $right->unitless())) ) { return $this->expToString($value); @@ -3538,24 +2975,73 @@ EOL; $ucLType = ucfirst($ltype); $ucRType = ucfirst($rtype); - $shouldEval = $inParens || $inExp; - // this tries: // 1. op[op name][left type][right type] - // 2. op[left type][right type] (passing the op as first arg) + // 2. op[left type][right type] (passing the op as first arg // 3. op[op name] - if (\is_callable([$this, $fn = "op${ucOpName}${ucLType}${ucRType}"])) { - $out = $this->$fn($left, $right, $shouldEval); - } elseif (\is_callable([$this, $fn = "op${ucLType}${ucRType}"])) { - $out = $this->$fn($op, $left, $right, $shouldEval); - } elseif (\is_callable([$this, $fn = "op${ucOpName}"])) { - $out = $this->$fn($left, $right, $shouldEval); - } else { - $out = null; - } + $fn = "op${ucOpName}${ucLType}${ucRType}"; - if (isset($out)) { - return $out; + if (is_callable([$this, $fn]) || + (($fn = "op${ucLType}${ucRType}") && + is_callable([$this, $fn]) && + $passOp = true) || + (($fn = "op${ucOpName}") && + 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)) { + $out = $this->$fn($op, $left, $right, $shouldEval); + } else { + $out = $this->$fn($left, $right, $shouldEval); + } + + if (isset($out)) { + if ($coerceUnit && $out[0] === Type::T_NUMBER) { + $out = $out->coerce($targetUnit); + } + + return $out; + } } return $this->expToString($value); @@ -3566,13 +3052,13 @@ EOL; $inExp = $inExp || $this->shouldEval($exp); $exp = $this->reduce($exp); - if ($exp instanceof Number) { + if ($exp[0] === Type::T_NUMBER) { switch ($op) { case '+': - return $exp; + return new Node\Number($exp[1], $exp[2]); case '-': - return $exp->unaryMinus(); + return new Node\Number(-$exp[1], $exp[2]); } } @@ -3597,14 +3083,6 @@ EOL; foreach ($value[2] as &$item) { $item = $this->reduce($item); } - unset($item); - - if (isset($value[3]) && \is_array($value[3])) { - foreach ($value[3] as &$item) { - $item = $this->reduce($item); - } - unset($item); - } return $value; @@ -3621,7 +3099,7 @@ EOL; case Type::T_STRING: foreach ($value[2] as &$item) { - if (\is_array($item) || $item instanceof Number) { + if (is_array($item) || $item instanceof \ArrayAccess) { $item = $this->reduce($item); } } @@ -3632,7 +3110,7 @@ EOL; $value[1] = $this->reduce($value[1]); if ($inExp) { - return [Type::T_KEYWORD, $this->compileValue($value, false)]; + return $value[1]; } return $value; @@ -3641,9 +3119,8 @@ EOL; return $this->fncall($value[1], $value[2]); case Type::T_SELF: - $selfParent = ! empty($this->env->block->selfParent) ? $this->env->block->selfParent : null; - $selfSelector = $this->multiplySelectors($this->env, $selfParent); - $selfSelector = $this->collapseSelectorsAsList($selfSelector); + $selfSelector = $this->multiplySelectors($this->env); + $selfSelector = $this->collapseSelectors($selfSelector, true); return $selfSelector; @@ -3655,301 +3132,35 @@ EOL; /** * Function caller * - * @param string|array $functionReference - * @param array $argValues - * - * @return array|Number - */ - protected function fncall($functionReference, $argValues) - { - // a string means this is a static hard reference coming from the parsing - if (is_string($functionReference)) { - $name = $functionReference; - - $functionReference = $this->getFunctionReference($name); - if ($functionReference === static::$null || $functionReference[0] !== Type::T_FUNCTION_REFERENCE) { - $functionReference = [Type::T_FUNCTION, $name, [Type::T_LIST, ',', []]]; - } - } - - // 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]) || count($argValues) === 1) { - $listArgs[] = $this->reduce($this->stringifyFncallArgs($arg[1])); - } - } - - return [Type::T_FUNCTION, $functionReference[1], [Type::T_LIST, ',', $listArgs]]; - } - - if ($functionReference === static::$null || $functionReference[0] !== Type::T_FUNCTION_REFERENCE) { - return static::$defaultValue; - } - - - switch ($functionReference[1]) { - // SCSS @function - case 'scss': - return $this->callScssFunction($functionReference[3], $argValues); - - // native PHP functions - case 'user': - case 'native': - list(,,$name, $fn, $prototype) = $functionReference; - - // special cases of css valid functions min/max - $name = strtolower($name); - if (\in_array($name, ['min', 'max']) && count($argValues) >= 1) { - $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); - - if (! isset($returnValue)) { - return $this->fncall([Type::T_FUNCTION, $name, [Type::T_LIST, ',', []]], $argValues); - } - - return $returnValue; - - default: - return static::$defaultValue; - } - } - - /** - * @param array|Number $arg - * @param string[] $allowed_function - * @param bool $inFunction - * - * @return array|Number|false - */ - protected function cssValidArg($arg, $allowed_function = [], $inFunction = false) - { - if ($arg instanceof Number) { - return $this->stringifyFncallArgs($arg); - } - - switch ($arg[0]) { - case Type::T_INTERPOLATE: - return [Type::T_KEYWORD, $this->CompileValue($arg)]; - - 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_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 array|Number $arg - * - * @return array|Number - */ - protected function stringifyFncallArgs($arg) - { - if ($arg instanceof Number) { - return $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 + * @param array $argValues + * + * @return array|null */ - protected function getFunctionReference($name, $safeCopy = false) + protected function fncall($name, $argValues) { // SCSS @function - if ($func = $this->get(static::$namespaces['function'] . $name, false)) { - if ($safeCopy) { - $func = clone $func; - } - - return [Type::T_FUNCTION_REFERENCE, 'scss', $name, $func]; + if ($this->callScssFunction($name, $argValues, $returnValue)) { + return $returnValue; } // native PHP functions - - // try to find a native lib function - $normalizedName = $this->normalizeName($name); - - 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 ($this->callNativeFunction($name, $argValues, $returnValue)) { + return $returnValue; } - $lowercasedName = strtolower($normalizedName); + // for CSS functions, simply flatten the arguments into a list + $listArgs = []; - // Special functions overriding a CSS function are case-insensitive. We normalize them as lowercase - // to avoid the deprecation warning about the wrong case being used. - if ($lowercasedName === 'min' || $lowercasedName === 'max') { - $normalizedName = $lowercasedName; - } - - if (($f = $this->getBuiltinFunction($normalizedName)) && \is_callable($f)) { - /** @var string $libName */ - $libName = $f[1]; - $prototype = isset(static::$$libName) ? static::$$libName : null; - - // All core functions have a prototype defined. Not finding the - // prototype can mean 2 things: - // - the function comes from a child class (deprecated just after) - // - the function was found with a different case, which relates to calling the - // wrong Sass function due to our camelCase usage (`fade-in()` vs `fadein()`), - // because PHP method names are case-insensitive while property names are - // case-sensitive. - if ($prototype === null || strtolower($normalizedName) !== $normalizedName) { - $r = new \ReflectionMethod($this, $libName); - $actualLibName = $r->name; - - if ($actualLibName !== $libName || strtolower($normalizedName) !== $normalizedName) { - $kebabCaseName = preg_replace('~(?<=\\w)([A-Z])~', '-$1', substr($actualLibName, 3)); - assert($kebabCaseName !== null); - $originalName = strtolower($kebabCaseName); - $warning = "Calling built-in functions with a non-standard name is deprecated since Scssphp 1.8.0 and will not work anymore in 2.0 (they will be treated as CSS function calls instead).\nUse \"$originalName\" instead of \"$name\"."; - @trigger_error($warning, E_USER_DEPRECATED); - $fname = $this->getPrettyPath($this->sourceNames[$this->sourceIndex]); - $line = $this->sourceLine; - Warn::deprecation("$warning\n on line $line of $fname"); - - // Use the actual function definition - $prototype = isset(static::$$actualLibName) ? static::$$actualLibName : null; - $f[1] = $libName = $actualLibName; - } + foreach ((array) $argValues as $arg) { + if (empty($arg[0])) { + $listArgs[] = $this->reduce($arg[1]); } - - if (\get_class($this) !== __CLASS__ && !isset($this->warnedChildFunctions[$libName])) { - $r = new \ReflectionMethod($this, $libName); - $declaringClass = $r->getDeclaringClass()->name; - - $needsWarning = $this->warnedChildFunctions[$libName] = $declaringClass !== __CLASS__; - - if ($needsWarning) { - if (method_exists(__CLASS__, $libName)) { - @trigger_error(sprintf('Overriding the "%s" core function by extending the Compiler is deprecated and will be unsupported in 2.0. Remove the "%s::%s" method.', $normalizedName, $declaringClass, $libName), E_USER_DEPRECATED); - } else { - @trigger_error(sprintf('Registering custom functions by extending the Compiler and using the lib* discovery mechanism is deprecated and will be removed in 2.0. Replace the "%s::%s" method with registering the "%s" function through "Compiler::registerFunction".', $declaringClass, $libName, $normalizedName), E_USER_DEPRECATED); - } - } - } - - return [Type::T_FUNCTION_REFERENCE, 'native', $name, $f, $prototype]; } - return static::$null; + return [Type::T_FUNCTION, $name, [Type::T_LIST, ',', $listArgs]]; } - /** * Normalize name * @@ -3965,20 +3176,14 @@ EOL; /** * Normalize value * - * @internal + * @param array $value * - * @param array|Number $value - * - * @return array|Number + * @return array */ public function normalizeValue($value) { $value = $this->coerceForExpression($this->reduce($value)); - if ($value instanceof Number) { - return $value; - } - switch ($value[0]) { case Type::T_LIST: $value = $this->extractInterpolation($value); @@ -3995,15 +3200,14 @@ EOL; unset($value['enclosing']); } - if ($value[1] === '' && count($value[2]) > 1) { - $value[1] = ' '; - } - return $value; case Type::T_STRING: return [$value[0], '"', [$this->compileStringContent($value)]]; + case Type::T_NUMBER: + return $value->normalize(); + case Type::T_INTERPOLATE: return [Type::T_KEYWORD, $this->compileValue($value)]; @@ -4015,66 +3219,70 @@ EOL; /** * Add numbers * - * @param Number $left - * @param Number $right + * @param array $left + * @param array $right * - * @return Number + * @return \ScssPhp\ScssPhp\Node\Number */ - protected function opAddNumberNumber(Number $left, Number $right) + protected function opAddNumberNumber($left, $right) { - return $left->plus($right); + return new Node\Number($left[1] + $right[1], $left[2]); } /** * Multiply numbers * - * @param Number $left - * @param Number $right + * @param array $left + * @param array $right * - * @return Number + * @return \ScssPhp\ScssPhp\Node\Number */ - protected function opMulNumberNumber(Number $left, Number $right) + protected function opMulNumberNumber($left, $right) { - return $left->times($right); + return new Node\Number($left[1] * $right[1], $left[2]); } /** * Subtract numbers * - * @param Number $left - * @param Number $right + * @param array $left + * @param array $right * - * @return Number + * @return \ScssPhp\ScssPhp\Node\Number */ - protected function opSubNumberNumber(Number $left, Number $right) + protected function opSubNumberNumber($left, $right) { - return $left->minus($right); + return new Node\Number($left[1] - $right[1], $left[2]); } /** * Divide numbers * - * @param Number $left - * @param Number $right + * @param array $left + * @param array $right * - * @return Number + * @return array|\ScssPhp\ScssPhp\Node\Number */ - protected function opDivNumberNumber(Number $left, Number $right) + protected function opDivNumberNumber($left, $right) { - return $left->dividedBy($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]); } /** * Mod numbers * - * @param Number $left - * @param Number $right + * @param array $left + * @param array $right * - * @return Number + * @return \ScssPhp\ScssPhp\Node\Number */ - protected function opModNumberNumber(Number $left, Number $right) + protected function opModNumberNumber($left, $right) { - return $left->modulo($right); + return new Node\Number($left[1] % $right[1], $left[2]); } /** @@ -4113,11 +3321,11 @@ EOL; /** * Boolean and * - * @param array|Number $left - * @param array|Number $right - * @param bool $shouldEval + * @param array $left + * @param array $right + * @param boolean $shouldEval * - * @return array|Number|null + * @return array|null */ protected function opAnd($left, $right, $shouldEval) { @@ -4141,11 +3349,11 @@ EOL; /** * Boolean or * - * @param array|Number $left - * @param array|Number $right - * @param bool $shouldEval + * @param array $left + * @param array $right + * @param boolean $shouldEval * - * @return array|Number|null + * @return array|null */ protected function opOr($left, $right, $shouldEval) { @@ -4177,15 +3385,6 @@ EOL; */ 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; - - Warn::deprecation("$warning\n on line $line of $fname"); - } - $out = [Type::T_COLOR]; foreach ([1, 2, 3] as $i) { @@ -4206,16 +3405,13 @@ EOL; break; case '%': - if ($rval == 0) { - throw $this->error("color: Can't take modulo by zero"); - } - $out[] = $lval % $rval; break; case '/': if ($rval == 0) { - throw $this->error("color: Can't divide by zero"); + $this->throwError("color: Can't divide by zero"); + break 2; } $out[] = (int) ($lval / $rval); @@ -4228,7 +3424,8 @@ EOL; return $this->opNeq($left, $right); default: - throw $this->error("color: unknown op $op"); + $this->throwError("color: unknown op $op"); + break 2; } } @@ -4246,21 +3443,13 @@ EOL; * * @param string $op * @param array $left - * @param Number $right + * @param array $right * * @return array */ - protected function opColorNumber($op, $left, Number $right) + protected function opColorNumber($op, $left, $right) { - if ($op === '==') { - return static::$false; - } - - if ($op === '!=') { - return static::$true; - } - - $value = $right->getDimension(); + $value = $right[1]; return $this->opColorColor( $op, @@ -4273,22 +3462,14 @@ EOL; * Compare number and color * * @param string $op - * @param Number $left + * @param array $left * @param array $right * * @return array */ - protected function opNumberColor($op, Number $left, $right) + protected function opNumberColor($op, $left, $right) { - if ($op === '==') { - return static::$false; - } - - if ($op === '!=') { - return static::$true; - } - - $value = $left->getDimension(); + $value = $left[1]; return $this->opColorColor( $op, @@ -4300,8 +3481,8 @@ EOL; /** * Compare number1 == number2 * - * @param array|Number $left - * @param array|Number $right + * @param array $left + * @param array $right * * @return array */ @@ -4321,8 +3502,8 @@ EOL; /** * Compare number1 != number2 * - * @param array|Number $left - * @param array|Number $right + * @param array $left + * @param array $right * * @return array */ @@ -4339,82 +3520,71 @@ EOL; return $this->toBool($left !== $right); } - /** - * Compare number1 == number2 - * - * @param Number $left - * @param Number $right - * - * @return array - */ - protected function opEqNumberNumber(Number $left, Number $right) - { - return $this->toBool($left->equals($right)); - } - - /** - * Compare number1 != number2 - * - * @param Number $left - * @param Number $right - * - * @return array - */ - protected function opNeqNumberNumber(Number $left, Number $right) - { - return $this->toBool(!$left->equals($right)); - } - /** * Compare number1 >= number2 * - * @param Number $left - * @param Number $right + * @param array $left + * @param array $right * * @return array */ - protected function opGteNumberNumber(Number $left, Number $right) + protected function opGteNumberNumber($left, $right) { - return $this->toBool($left->greaterThanOrEqual($right)); + return $this->toBool($left[1] >= $right[1]); } /** * Compare number1 > number2 * - * @param Number $left - * @param Number $right + * @param array $left + * @param array $right * * @return array */ - protected function opGtNumberNumber(Number $left, Number $right) + protected function opGtNumberNumber($left, $right) { - return $this->toBool($left->greaterThan($right)); + return $this->toBool($left[1] > $right[1]); } /** * Compare number1 <= number2 * - * @param Number $left - * @param Number $right + * @param array $left + * @param array $right * * @return array */ - protected function opLteNumberNumber(Number $left, Number $right) + protected function opLteNumberNumber($left, $right) { - return $this->toBool($left->lessThanOrEqual($right)); + return $this->toBool($left[1] <= $right[1]); } /** * Compare number1 < number2 * - * @param Number $left - * @param Number $right + * @param array $left + * @param array $right * * @return array */ - protected function opLtNumberNumber(Number $left, Number $right) + protected function opLtNumberNumber($left, $right) { - return $this->toBool($left->lessThan($right)); + return $this->toBool($left[1] < $right[1]); + } + + /** + * Three-way comparison, aka spaceship operator + * + * @param array $left + * @param array $right + * + * @return \ScssPhp\ScssPhp\Node\Number + */ + protected function opCmpNumberNumber($left, $right) + { + $n = $left[1] - $right[1]; + + return new Node\Number($n ? $n / abs($n) : 0, ''); } /** @@ -4422,7 +3592,7 @@ EOL; * * @api * - * @param bool $thing + * @param mixed $thing * * @return array */ @@ -4431,53 +3601,6 @@ EOL; return $thing ? static::$true : static::$false; } - /** - * Escape non printable chars in strings output as in dart-sass - * - * @internal - * - * @param string $string - * @param bool $inKeyword - * - * @return string - */ - public function escapeNonPrintableChars($string, $inKeyword = false) - { - 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. * @@ -4491,22 +3614,17 @@ EOL; * * @api * - * @param array|Number $value - * @param bool $quote + * @param array $value * - * @return string + * @return string|array */ - public function compileValue($value, $quote = true) + public function compileValue($value) { $value = $this->reduce($value); - if ($value instanceof Number) { - return $value->output($this); - } - switch ($value[0]) { case Type::T_KEYWORD: - return $this->escapeNonPrintableChars($value[1], true); + return $value[1]; case Type::T_COLOR: // [1] - red component (either number for a %) @@ -4519,18 +3637,18 @@ EOL; $g = $this->compileRGBAValue($g); $b = $this->compileRGBAValue($b); - if (\count($value) === 5) { + if (count($value) === 5) { $alpha = $this->compileRGBAValue($value[4], true); if (! is_numeric($alpha) || $alpha < 1) { $colorName = Colors::RGBaToColorName($r, $g, $b, $alpha); - if (! \is_null($colorName)) { + if (! is_null($colorName)) { return $colorName; } if (is_numeric($alpha)) { - $a = new Number($alpha, ''); + $a = new Node\Number($alpha, ''); } else { $a = $alpha; } @@ -4545,7 +3663,7 @@ EOL; $colorName = Colors::RGBaToColorName($r, $g, $b); - if (! \is_null($colorName)) { + if (! is_null($colorName)) { return $colorName; } @@ -4558,119 +3676,72 @@ EOL; return $h; + case Type::T_NUMBER: + return $value->output($this); + case Type::T_STRING: - $content = $this->compileStringContent($value, $quote); - - if ($value[1] && $quote) { - $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) - ) { - $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]; + return $value[1] . $this->compileStringContent($value) . $value[1]; case Type::T_FUNCTION: - $args = ! empty($value[2]) ? $this->compileValue($value[2], $quote) : ''; + $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); if ($value[0] !== Type::T_LIST) { - return $this->compileValue($value, $quote); + return $this->compileValue($value); } list(, $delim, $items) = $value; - $pre = $post = ''; - + $pre = $post = ""; if (! empty($value['enclosing'])) { switch ($value['enclosing']) { case 'parent': - //$pre = '('; - //$post = ')'; + //$pre = "("; + //$post = ")"; break; case 'forced_parent': - $pre = '('; - $post = ')'; + $pre = "("; + $post = ")"; break; case 'bracket': case 'forced_bracket': - $pre = '['; - $post = ']'; + $pre = "["; + $post = "]"; break; } } - $separator = $delim === '/' ? ' /' : $delim; - $prefix_value = ''; - if ($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, $quote); - - if ($prefix_value && \strlen($compiled)) { + $compiled = $this->compileValue($item); + if ($prefix_value && strlen($compiled)) { $compiled = $prefix_value . $compiled; } - $filtered[] = $compiled; } - return $pre . substr(implode($separator, $filtered), \strlen($prefix_value)) . $post; + return $pre . substr(implode("$delim", $filtered), strlen($prefix_value)) . $post; case Type::T_MAP: $keys = $value[1]; $values = $value[2]; $filtered = []; - for ($i = 0, $s = \count($keys); $i < $s; $i++) { - $filtered[$this->compileValue($keys[$i], $quote)] = $this->compileValue($values[$i], $quote); + for ($i = 0, $s = count($keys); $i < $s; $i++) { + $filtered[$this->compileValue($keys[$i])] = $this->compileValue($values[$i]); } array_walk($filtered, function (&$value, $key) { @@ -4690,9 +3761,8 @@ EOL; $delim .= ' '; } - $left = \count($left[2]) > 0 - ? $this->compileValue($left, $quote) . $delim . $whiteLeft - : ''; + $left = count($left[2]) > 0 ? + $this->compileValue($left) . $delim . $whiteLeft: ''; $delim = $right[1]; @@ -4700,19 +3770,15 @@ EOL; $delim .= ' '; } - $right = \count($right[2]) > 0 ? - $whiteRight . $delim . $this->compileValue($right, $quote) : ''; + $right = count($right[2]) > 0 ? + $whiteRight . $delim . $this->compileValue($right) : ''; - return $left . $this->compileValue($interpolate, $quote) . $right; + return $left . $this->compileValue($interpolate) . $right; case Type::T_INTERPOLATE: // strip quotes if it's a string $reduced = $this->reduce($value[1]); - if ($reduced instanceof Number) { - return $this->compileValue($reduced, $quote); - } - switch ($reduced[0]) { case Type::T_LIST: $reduced = $this->extractInterpolation($reduced); @@ -4734,12 +3800,14 @@ EOL; continue; } - if ($item[0] === Type::T_STRING) { - $filtered[] = $this->compileStringContent($item, $quote); - } elseif ($item[0] === Type::T_KEYWORD) { - $filtered[] = $item[1]; + $temp = $this->compileValue([Type::T_KEYWORD, $item]); + + if ($temp[0] === Type::T_STRING) { + $filtered[] = $this->compileStringContent($temp); + } elseif ($temp[0] === Type::T_KEYWORD) { + $filtered[] = $temp[1]; } else { - $filtered[] = $this->compileValue($item, $quote); + $filtered[] = $this->compileValue($temp); } } @@ -4747,14 +3815,14 @@ EOL; break; case Type::T_STRING: - $reduced = [Type::T_STRING, '', [$this->compileStringContent($reduced)]]; + $reduced = [Type::T_KEYWORD, $this->compileStringContent($reduced)]; break; case Type::T_NULL: $reduced = [Type::T_KEYWORD, '']; } - return $this->compileValue($reduced, $quote); + return $this->compileValue($reduced); case Type::T_NULL: return 'null'; @@ -4763,29 +3831,7 @@ EOL; return $this->compileCommentValue($value); default: - throw $this->error('unknown value type: ' . json_encode($value)); - } - } - - /** - * @param array|Number $value - * - * @return string - */ - protected function compileDebugValue($value) - { - $value = $this->reduce($value, true); - - if ($value instanceof Number) { - return $this->compileValue($value); - } - - switch ($value[0]) { - case Type::T_STRING: - return $this->compileStringContent($value); - - default: - return $this->compileValue($value); + $this->throwError("unknown value type: ".json_encode($value)); } } @@ -4795,50 +3841,26 @@ EOL; * @param array $list * * @return string - * - * @deprecated */ protected function flattenList($list) { - @trigger_error(sprintf('The "%s" method is deprecated.', __METHOD__), E_USER_DEPRECATED); - return $this->compileValue($list); } - /** - * Gets the text of a Sass string - * - * Calling this method on anything else than a SassString is unsupported. Use {@see assertString} first - * to ensure that the value is indeed a string. - * - * @param array $value - * - * @return string - */ - public function getStringText(array $value) - { - if ($value[0] !== Type::T_STRING) { - throw new \InvalidArgumentException('The argument is not a sass string. Did you forgot to use "assertString"?'); - } - - return $this->compileStringContent($value); - } - /** * Compile string content * * @param array $string - * @param bool $quote * * @return string */ - protected function compileStringContent($string, $quote = true) + protected function compileStringContent($string) { $parts = []; foreach ($string[2] as $part) { - if (\is_array($part) || $part instanceof Number) { - $parts[] = $this->compileValue($part, $quote); + if (is_array($part) || $part instanceof \ArrayAccess) { + $parts[] = $this->compileValue($part); } else { $parts[] = $part; } @@ -4860,8 +3882,8 @@ EOL; 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]; } @@ -4886,7 +3908,7 @@ EOL; $selfParentSelectors = null; - if (! \is_null($selfParent) && $selfParent->selectors) { + if (! is_null($selfParent) && $selfParent->selectors) { $selfParentSelectors = $this->evalSelectors($selfParent->selectors); } @@ -4902,8 +3924,8 @@ EOL; $prevSelectors = $selectors; $selectors = []; - foreach ($parentSelectors as $parent) { - foreach ($prevSelectors as $selector) { + foreach ($prevSelectors as $selector) { + foreach ($parentSelectors as $parent) { if ($selfParentSelectors) { foreach ($selfParentSelectors as $selfParent) { // if no '&' in the selector, each call will give same result, only add once @@ -4923,21 +3945,16 @@ EOL; $selectors = array_values($selectors); - // case we are just starting a at-root : nothing to multiply but parentSelectors - if (! $selectors && $selfParentSelectors) { - $selectors = $selfParentSelectors; - } - return $selectors; } /** * Join selectors; looks for & to replace, or append parent before child * - * @param array $parent - * @param array $child - * @param bool $stillHasSelf - * @param array $selfParentSelectors + * @param array $parent + * @param array $child + * @param boolean &$stillHasSelf + * @param array $selfParentSelectors * @return array */ @@ -4958,7 +3975,7 @@ EOL; if ($p === static::$selfSelector && ! $setSelf) { $setSelf = true; - if (\is_null($selfParentSelectors)) { + if (is_null($selfParentSelectors)) { $selfParentSelectors = $parent; } @@ -4969,7 +3986,7 @@ EOL; } foreach ($parentPart as $pp) { - if (\is_array($pp)) { + if (is_array($pp)) { $flatten = []; array_walk_recursive($pp, function ($a) use (&$flatten) { @@ -5003,8 +4020,7 @@ EOL; */ 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; @@ -5015,8 +4031,6 @@ EOL; return $this->multiplyMedia($env->parent, $childQueries); } - assert($env->block instanceof MediaBlock); - $parentQueries = isset($env->block->queryList) ? $env->block->queryList : [[[Type::T_MEDIA_VALUE, $env->block->value]]]; @@ -5029,7 +4043,7 @@ EOL; list($this->env, $this->storeEnv) = $store; - if (\is_null($childQueries)) { + if (is_null($childQueries)) { $childQueries = $parentQueries; } else { $originalQueries = $childQueries; @@ -5052,11 +4066,9 @@ EOL; /** * Convert env linked list to stack * - * @param Environment $env + * @param \ScssPhp\ScssPhp\Compiler\Environment $env * - * @return Environment[] - * - * @phpstan-return non-empty-array + * @return array */ protected function compactEnv(Environment $env) { @@ -5070,11 +4082,9 @@ EOL; /** * Convert env stack to singly linked list * - * @param Environment[] $envs + * @param array $envs * - * @return Environment - * - * @phpstan-param non-empty-array $envs + * @return \ScssPhp\ScssPhp\Compiler\Environment */ protected function extractEnv($envs) { @@ -5095,47 +4105,25 @@ EOL; */ 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 * @@ -5151,11 +4139,9 @@ EOL; * * @param string $name * @param mixed $value - * @param bool $shadow + * @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) { @@ -5179,50 +4165,29 @@ EOL; * @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 ($maxDepth-- <= 0) { - break; - } - - if (\array_key_exists($name, $env->store)) { + if (array_key_exists($name, $env->store)) { break; } 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; } + + if (! isset($env->parent)) { + $env = $storeEnv; + break; + } + + $env = $env->parent; } $env->store[$name] = $value; @@ -5239,8 +4204,6 @@ EOL; * @param mixed $value * @param \ScssPhp\ScssPhp\Compiler\Environment $env * @param mixed $valueUnreduced - * - * @return void */ protected function setRaw($name, $value, Environment $env, $valueUnreduced = null) { @@ -5254,12 +4217,12 @@ EOL; /** * Get variable * - * @internal + * @api * * @param string $name - * @param bool $shouldThrow + * @param boolean $shouldThrow * @param \ScssPhp\ScssPhp\Compiler\Environment $env - * @param bool $unreduced + * @param boolean $unreduced * * @return mixed|null */ @@ -5281,7 +4244,7 @@ EOL; 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]; } @@ -5303,17 +4266,15 @@ EOL; continue; } - if (isset($env->parentStore)) { - $env = $env->parentStore; - } elseif (isset($env->parent)) { - $env = $env->parent; - } else { + if (! isset($env->parent)) { break; } + + $env = $env->parent; } if ($shouldThrow) { - throw $this->error("Undefined variable \$$name" . ($maxDepth <= 0 ? ' (infinite recursion)' : '')); + $this->throwError("Undefined variable \$$name" . ($maxDepth <= 0 ? " (infinite recursion)" : "")); } // found nothing @@ -5326,19 +4287,17 @@ EOL; * @param string $name * @param \ScssPhp\ScssPhp\Compiler\Environment $env * - * @return bool + * @return boolean */ protected function has($name, Environment $env = null) { - return ! \is_null($this->get($name, false, $env)); + return ! is_null($this->get($name, false, $env)); } /** * Inject variables * * @param array $args - * - * @return void */ protected function injectVariables(array $args) { @@ -5353,7 +4312,7 @@ EOL; $name = substr($name, 1); } - if (!\is_string($strValue) || ! $parser->parseValue($strValue, $value)) { + if (! $parser->parseValue($strValue, $value)) { $value = $this->coerceValue($strValue); } @@ -5361,59 +4320,16 @@ EOL; } } - /** - * Replaces variables. - * - * @param array $variables - * - * @return void - */ - public function replaceVariables(array $variables) - { - $this->registeredVars = []; - $this->addVariables($variables); - } - - /** - * Replaces variables. - * - * @param array $variables - * - * @return void - */ - public function addVariables(array $variables) - { - $triggerWarning = false; - - foreach ($variables as $name => $value) { - if (!$value instanceof Number && !\is_array($value)) { - $triggerWarning = true; - } - - $this->registeredVars[$name] = $value; - } - - if ($triggerWarning) { - @trigger_error('Passing raw values to as custom variables to the Compiler is deprecated. Use "\ScssPhp\ScssPhp\ValueConverter::parseValue" or "\ScssPhp\ScssPhp\ValueConverter::fromPhp" to convert them instead.', E_USER_DEPRECATED); - } - } - /** * Set variables * * @api * * @param array $variables - * - * @return void - * - * @deprecated Use "addVariables" or "replaceVariables" instead. */ public function setVariables(array $variables) { - @trigger_error('The method "setVariables" of the Compiler is deprecated. Use the "addVariables" method for the equivalent behavior or "replaceVariables" if merging with previous variables was not desired.'); - - $this->addVariables($variables); + $this->registeredVars = array_merge($this->registeredVars, $variables); } /** @@ -5422,8 +4338,6 @@ EOL; * @api * * @param string $name - * - * @return void */ public function unsetVariable($name) { @@ -5445,15 +4359,13 @@ EOL; /** * Adds to list of parsed files * - * @internal + * @api * - * @param string|null $path - * - * @return void + * @param string $path */ public function addParsedFile($path) { - if (! \is_null($path) && is_file($path)) { + if (isset($path) && is_file($path)) { $this->parsedFiles[realpath($path)] = filemtime($path); } } @@ -5461,12 +4373,12 @@ EOL; /** * Returns list of parsed files * - * @deprecated - * @return array + * @api + * + * @return array */ public function getParsedFiles() { - @trigger_error('The method "getParsedFiles" of the Compiler is deprecated. Use the "getIncludedFiles" method on the CompilationResult instance returned by compileString() instead. Be careful that the signature of the method is different.', E_USER_DEPRECATED); return $this->parsedFiles; } @@ -5476,12 +4388,10 @@ EOL; * @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; } } @@ -5491,24 +4401,11 @@ EOL; * * @api * - * @param string|array $path - * - * @return void + * @param string|array $path */ public function setImportPaths($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 "compileString()" instead.', E_USER_DEPRECATED); - } - - $this->importPaths = $actualImportPaths; + $this->importPaths = (array) $path; } /** @@ -5516,43 +4413,11 @@ EOL; * * @api * - * @param int $numberPrecision - * - * @return void - * - * @deprecated The number precision is not configurable anymore. The default is enough for all browsers. + * @param integer $numberPrecision */ public function setNumberPrecision($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->configuredFormatter = Expanded::class; - break; - - case OutputStyle::COMPRESSED: - $this->configuredFormatter = Compressed::class; - break; - - default: - throw new \InvalidArgumentException(sprintf('Invalid output style "%s".', $style)); - } + Node\Number::$precision = $numberPrecision; } /** @@ -5561,21 +4426,10 @@ EOL; * @api * * @param string $formatterName - * - * @return void - * - * @deprecated Use {@see setOutputStyle} instead. - * - * @phpstan-param class-string $formatterName */ 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->configuredFormatter = $formatterName; + $this->formatter = $formatterName; } /** @@ -5584,34 +4438,10 @@ EOL; * @api * * @param string $lineNumberStyle - * - * @return void - * - * @deprecated The line number output is not supported anymore. Use source maps instead. */ public function setLineNumberStyle($lineNumberStyle) { - @trigger_error('The line number output is not supported anymore. ' - . 'Use source maps instead.', E_USER_DEPRECATED); - } - - /** - * Configures the handling of non-ASCII outputs. - * - * If $charset is `true`, this will include a `@charset` declaration or a - * UTF-8 [byte-order mark][] if the stylesheet contains any non-ASCII - * characters. Otherwise, it will never include a `@charset` declaration or a - * byte-order mark. - * - * [byte-order mark]: https://en.wikipedia.org/wiki/Byte_order_mark#UTF-8 - * - * @param bool $charset - * - * @return void - */ - public function setCharset($charset) - { - $this->charset = $charset; + $this->lineNumberStyle = $lineNumberStyle; } /** @@ -5619,11 +4449,7 @@ EOL; * * @api * - * @param int $sourceMap - * - * @return void - * - * @phpstan-param self::SOURCE_MAP_* $sourceMap + * @param integer $sourceMap */ public function setSourceMap($sourceMap) { @@ -5636,10 +4462,6 @@ EOL; * @api * * @param array $sourceMapOptions - * - * @phpstan-param array{sourceRoot?: string, sourceMapFilename?: string|null, sourceMapURL?: string|null, sourceMapWriteTo?: string|null, outputSourceFiles?: bool, sourceMapRootpath?: string, sourceMapBasepath?: string} $sourceMapOptions - * - * @return void */ public function setSourceMapOptions($sourceMapOptions) { @@ -5651,23 +4473,13 @@ EOL; * * @api * - * @param string $name - * @param callable $callback - * @param string[]|null $argumentDeclaration - * - * @return void + * @param string $name + * @param callable $func + * @param array $prototype */ - public function registerFunction($name, $callback, $argumentDeclaration = null) + public function registerFunction($name, $func, $prototype = null) { - if (self::isNativeFunction($name)) { - @trigger_error(sprintf('The "%s" function is a core sass function. Overriding it with a custom implementation through "%s" is deprecated and won\'t be supported in ScssPhp 2.0 anymore.', $name, __METHOD__), E_USER_DEPRECATED); - } - - if ($argumentDeclaration === null) { - @trigger_error('Omitting the argument declaration when registering custom function is deprecated and won\'t be supported in ScssPhp 2.0 anymore.', E_USER_DEPRECATED); - } - - $this->userFunctions[$this->normalizeName($name)] = [$callback, $argumentDeclaration]; + $this->userFunctions[$this->normalizeName($name)] = [$func, $prototype]; } /** @@ -5676,8 +4488,6 @@ EOL; * @api * * @param string $name - * - * @return void */ public function unregisterFunction($name) { @@ -5690,15 +4500,9 @@ EOL; * @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; } @@ -5707,28 +4511,12 @@ EOL; * * @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); - if ($realPath === false) { - $realPath = $path; - } - - if (substr($path, -5) === '.sass') { - $this->sourceIndex = \count($this->sourceNames); - $this->sourceNames[] = $path; - $this->sourceLine = 1; - $this->sourceColumn = 1; - - throw $this->error('The Sass indented syntax is not implemented.'); - } - if (isset($this->importCache[$realPath])) { $this->handleImportLoop($realPath); @@ -5741,261 +4529,62 @@ EOL; $this->importCache[$realPath] = $tree; } - $currentDirectory = $this->currentDirectory; - $this->currentDirectory = dirname($path); + $pi = pathinfo($path); + array_unshift($this->importPaths, $pi['dirname']); $this->compileChildrenNoReturn($tree->children, $out); - $this->currentDirectory = $currentDirectory; - $this->popCallStack(); - } - - /** - * Save the imported files with their resolving path context - * - * @param string|null $currentDirectory - * @param string $path - * @param string $filePath - * - * @return void - */ - private function registerImport($currentDirectory, $path, $filePath) - { - $this->resolvedImports[] = ['currentDir' => $currentDirectory, 'path' => $path, 'filePath' => $filePath]; - } - - /** - * Detects whether the import is a CSS import. - * - * For legacy reasons, custom importers are called for those, allowing them - * to replace them with an actual Sass import. However this behavior is - * deprecated. Custom importers are expected to return null when they receive - * a CSS import. - * - * @param string $url - * - * @return bool - */ - public static function isCssImport($url) - { - return 1 === preg_match('~\.css$|^https?://|^//~', $url); + array_shift($this->importPaths); } /** * Return the file path for an import url if it exists * - * @internal + * @api * - * @param string $url - * @param string|null $currentDir + * @param string $url * * @return string|null */ - public function findImport($url, $currentDir = null) + public function findImport($url) { - // Vanilla css and external requests. These are not meant to be Sass imports. - // Callback importers are still called for BC. - if (self::isCssImport($url)) { - foreach ($this->importPaths as $dir) { - if (\is_string($dir)) { - continue; - } + $urls = []; - if (\is_callable($dir)) { - // check custom callback for import path - $file = \call_user_func($dir, $url); + // 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)]; + } - if (! \is_null($file)) { - if (\is_array($dir)) { - $callableDescription = (\is_object($dir[0]) ? \get_class($dir[0]) : $dir[0]).'::'.$dir[1]; - } elseif ($dir instanceof \Closure) { - $r = new \ReflectionFunction($dir); - if (false !== strpos($r->name, '{closure}')) { - $callableDescription = sprintf('closure{%s:%s}', $r->getFileName(), $r->getStartLine()); - } elseif ($class = $r->getClosureScopeClass()) { - $callableDescription = $class->name.'::'.$r->name; - } else { - $callableDescription = $r->name; - } - } elseif (\is_object($dir)) { - $callableDescription = \get_class($dir) . '::__invoke'; - } else { - $callableDescription = 'callable'; // Fallback if we don't have a dedicated description - } - @trigger_error(sprintf('Returning a file to import for CSS or external references in custom importer callables is deprecated and will not be supported anymore in ScssPhp 2.0. This behavior is not compliant with the Sass specification. Update your "%s" importer.', $callableDescription), E_USER_DEPRECATED); + $hasExtension = preg_match('/[.]s?css$/', $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 (is_file($file = $full . '.scss') || + ($hasExtension && is_file($file = $full)) + ) { return $file; } } - } - return null; - } - - if (!\is_null($currentDir)) { - $relativePath = $this->resolveImportPath($url, $currentDir); - - 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)) { + } elseif (is_callable($dir)) { // check custom callback for import path - $file = \call_user_func($dir, $url); + $file = call_user_func($dir, $url); - if (! \is_null($file)) { + if (! is_null($file)) { return $file; } } } - 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 "compileString()" 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('/.s[ac]ss$/', $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 = array_merge( - $this->tryImportPath($path.'.sass'), - $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|null $path - * - * @return string - */ - private function getPrettyPath($path) - { - if ($path === null) { - return '(unknown file)'; - } - - $normalizedPath = $path; - $normalizedRootDirectory = $this->rootDirectory.'/'; - - if (\DIRECTORY_SEPARATOR === '\\') { - $normalizedRootDirectory = str_replace('\\', '/', $normalizedRootDirectory); - $normalizedPath = str_replace('\\', '/', $path); - } - - if (0 === strpos($normalizedPath, $normalizedRootDirectory)) { - return substr($path, \strlen($normalizedRootDirectory)); - } - - return $path; + return null; } /** @@ -6003,20 +4592,10 @@ EOL; * * @api * - * @param string|null $encoding - * - * @return void - * - * @deprecated Non-compliant support for other encodings than UTF-8 is deprecated. + * @param string $encoding */ public function setEncoding($encoding) { - if (!$encoding || strtolower($encoding) === 'utf-8') { - @trigger_error(sprintf('The "%s" method is deprecated.', __METHOD__), E_USER_DEPRECATED); - } else { - @trigger_error(sprintf('The "%s" method is deprecated. Parsing will only support UTF-8 in ScssPhp 2.0. The non-UTF-8 parsing of ScssPhp 1.x is not spec compliant.', __METHOD__), E_USER_DEPRECATED); - } - $this->encoding = $encoding; } @@ -6025,37 +4604,17 @@ EOL; * * @api * - * @param bool $ignoreErrors + * @param boolean $ignoreErrors * * @return \ScssPhp\ScssPhp\Compiler - * - * @deprecated Ignoring Sass errors is not longer supported. */ public function setIgnoreErrors($ignoreErrors) { - @trigger_error('Ignoring Sass errors is not longer supported.', E_USER_DEPRECATED); + $this->ignoreErrors = $ignoreErrors; return $this; } - /** - * Get source position - * - * @api - * - * @return array - * - * @deprecated - */ - public function getSourcePosition() - { - @trigger_error(sprintf('The "%s" method is deprecated.', __METHOD__), E_USER_DEPRECATED); - - $sourceFile = isset($this->sourceNames[$this->sourceIndex]) ? $this->sourceNames[$this->sourceIndex] : ''; - - return [$sourceFile, $this->sourceLine, $this->sourceColumn]; - } - /** * Throw error (exception) * @@ -6063,58 +4622,24 @@ EOL; * * @param string $msg Message with optional sprintf()-style vararg parameters * - * @return never - * * @throws \ScssPhp\ScssPhp\Exception\CompilerException - * - * @deprecated use "error" and throw the exception in the caller instead. */ public function throwError($msg) { - @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) - * - * @internal - * - * @param string $msg Message with optional sprintf()-style vararg parameters - * @param bool|float|int|string|null ...$args - * - * @return CompilerException - */ - public function error($msg, ...$args) - { - if ($args) { - $msg = sprintf($msg, ...$args); + if ($this->ignoreErrors) { + return; } - if (! $this->ignoreCallStackMessage) { - $msg = $this->addLocationToMessage($msg); - } - - return new CompilerException($msg); - } - - /** - * @param string $msg - * - * @return string - */ - private function addLocationToMessage($msg) - { $line = $this->sourceLine; $column = $this->sourceColumn; $loc = isset($this->sourceNames[$this->sourceIndex]) - ? $this->getPrettyPath($this->sourceNames[$this->sourceIndex]) . " on line $line, at column $column" - : "line: $line, column: $column"; + ? $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"; @@ -6124,51 +4649,14 @@ EOL; $msg .= "\nCall Stack:\n" . $callStackMsg; } - return $msg; - } - - /** - * @param string $functionName - * @param array $ExpectedArgs - * @param int $nbActual - * @return CompilerException - * - * @deprecated - */ - public function errorArgsNumber($functionName, $ExpectedArgs, $nbActual) - { - @trigger_error(sprintf('The "%s" method is deprecated.', __METHOD__), E_USER_DEPRECATED); - - $nbExpected = \count($ExpectedArgs); - - if ($nbActual > $nbExpected) { - 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)); - } - - return $this->error( - 'Error: %s() argument%s %s missing.', - $functionName, - count($missing) > 1 ? 's' : '', - implode(', ', $missing) - ); - } + throw new CompilerException($msg); } /** * Beautify call stack for output * - * @param bool $all - * @param int|null $limit + * @param boolean $all + * @param null $limit * * @return string */ @@ -6180,15 +4668,15 @@ EOL; 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->getPrettyPath($this->sourceNames[$call[Parser::SOURCE_INDEX]]) + ? $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; } } @@ -6203,8 +4691,6 @@ EOL; * * @param string $name * - * @return void - * * @throws \Exception */ protected function handleImportLoop($name) @@ -6216,12 +4702,9 @@ EOL; $file = $this->sourceNames[$env->block->sourceIndex]; - if ($file === null) { - continue; - } - if (realpath($file) === $name) { - throw $this->error('An @import loop has been found: %s imports %s', $file, basename($file)); + $this->throwError('An @import loop has been found: %s imports %s', $file, basename($file)); + break; } } } @@ -6229,86 +4712,100 @@ EOL; /** * Call SCSS @function * - * @param CallableBlock|null $func - * @param array $argValues + * @param string $name + * @param array $argValues + * @param array $returnValue * - * @return array|Number + * @return boolean Returns true if returnValue is set; otherwise, false */ - protected function callScssFunction($func, $argValues) + protected function callScssFunction($name, $argValues, &$returnValue) { + $func = $this->get(static::$namespaces['function'] . $name, false); + if (! $func) { - return static::$defaultValue; + return false; } - $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'; - if (! empty($func->parentEnv)) { $this->env->declarationScopeParent = $func->parentEnv; } else { - throw $this->error("@function $name() without parentEnv"); + $this->throwError("@function $name() without parentEnv"); } - $ret = $this->compileChildren($func->children, $tmp, $this->env->marker . ' ' . $name); + $ret = $this->compileChildren($func->children, $tmp, $this->env->marker . " " . $name); + + $this->storeEnv = $storeEnv; $this->popEnv(); - return ! isset($ret) ? static::$defaultValue : $ret; + $returnValue = ! isset($ret) ? static::$defaultValue : $ret; + + return true; } /** * Call built-in and registered (PHP) functions * * @param string $name - * @param callable $function - * @param array $prototype * @param array $args + * @param array $returnValue * - * @return array|Number|null + * @return boolean Returns true if returnValue is set; otherwise, false */ - protected function callNativeFunction($name, $function, $prototype, $args) + protected function callNativeFunction($name, $args, &$returnValue) { - $libName = (is_array($function) ? end($function) : null); - $sorted_kwargs = $this->sortNativeFunctionArgs($libName, $prototype, $args); + // try a lib function + $name = $this->normalizeName($name); - if (\is_null($sorted_kwargs)) { - return null; + 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; } - @list($sorted, $kwargs) = $sorted_kwargs; - if ($name !== 'if') { + @list($sorted, $kwargs) = $this->sortNativeFunctionArgs($libName, $prototype, $args); + + if ($name !== 'if' && $name !== 'call') { + $inExp = true; + + if ($name === 'join') { + $inExp = false; + } + foreach ($sorted as &$val) { - if ($val !== null) { - $val = $this->reduce($val, true); - } + $val = $this->reduce($val, $inExp); } } - $returnValue = \call_user_func($function, $sorted, $kwargs); + $returnValue = call_user_func($f, $sorted, $kwargs); if (! isset($returnValue)) { - return null; + return false; } - if (\is_array($returnValue) || $returnValue instanceof Number) { - return $returnValue; - } + $returnValue = $this->coerceValue($returnValue); - @trigger_error(sprintf('Returning a PHP value from the "%s" custom function is deprecated. A sass value must be returned instead.', $name), E_USER_DEPRECATED); - - return $this->coerceValue($returnValue); + return true; } /** @@ -6320,22 +4817,6 @@ EOL; */ protected function getBuiltinFunction($name) { - $libName = self::normalizeNativeFunctionName($name); - return [$this, $libName]; - } - - /** - * Normalize native function name - * - * @internal - * - * @param string $name - * - * @return string - */ - public static function normalizeNativeFunctionName($name) - { - $name = str_replace("-", "_", $name); $libName = 'lib' . preg_replace_callback( '/_(.)/', function ($m) { @@ -6343,31 +4824,18 @@ EOL; }, ucfirst($name) ); - return $libName; - } - /** - * Check if a function is a native built-in scss function, for css parsing - * - * @internal - * - * @param string $name - * - * @return bool - */ - public static function isNativeFunction($name) - { - return method_exists(Compiler::class, self::normalizeNativeFunctionName($name)); + return [$this, $libName]; } /** * Sorts keyword arguments * * @param string $functionName - * @param array|null $prototypes + * @param array $prototypes * @param array $args * - * @return array|null + * @return array */ protected function sortNativeFunctionArgs($functionName, $prototypes, $args) { @@ -6377,18 +4845,16 @@ EOL; $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; - if (empty($key) or empty($key[1])) { + $key = $key[1]; + + if (empty($key)) { $posArgs[] = empty($arg[2]) ? $value : $arg; } else { - $keyArgs[$key[1]] = $value; + $keyArgs[$key] = $value; } } @@ -6396,359 +4862,209 @@ EOL; } // specific cases ? - if (\in_array($functionName, ['libRgb', 'libRgba', 'libHsl', 'libHsla'])) { + 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 (!isset($arg[1])) { - continue; // This happens when using a trailing comma - } - if ($arg[1][0] === Type::T_LIST && \count($arg[1][2]) === 3) { - $args[$k][1][2] = $this->extractSlashAlphaInColorFunction($arg[1][2]); + 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; + } } } } - list($positionalArgs, $namedArgs, $names, $separator, $hasSplat) = $this->evaluateArguments($args, false); + $finalArgs = []; - if (! \is_array(reset($prototypes))) { + if (! is_array(reset($prototypes))) { $prototypes = [$prototypes]; } - $parsedPrototypes = array_map([$this, 'parseFunctionPrototype'], $prototypes); - assert(!empty($parsedPrototypes)); - $matchedPrototype = $this->selectFunctionPrototype($parsedPrototypes, \count($positionalArgs), $names); - - $this->verifyPrototype($matchedPrototype, \count($positionalArgs), $names, $hasSplat); - - $vars = $this->applyArgumentsToDeclaration($matchedPrototype, $positionalArgs, $namedArgs, $separator); - - $finalArgs = []; $keyArgs = []; - foreach ($matchedPrototype['arguments'] as $argument) { - list($normalizedName, $originalName, $default) = $argument; + // trying each prototypes + $prototypeHasMatch = false; + $exceptionMessage = ''; - if (isset($vars[$normalizedName])) { - $value = $vars[$normalizedName]; - } else { - $value = $default; + foreach ($prototypes as $prototype) { + $argDef = []; + + foreach ($prototype as $i => $p) { + $default = null; + $p = explode(':', $p, 2); + $name = array_shift($p); + + if (count($p)) { + $p = trim(reset($p)); + + if ($p === 'null') { + // differentiate this null from the static::$null + $default = [Type::T_KEYWORD, 'null']; + } else { + if (is_null($parser)) { + $parser = $this->parserFactory(__METHOD__); + } + + $parser->parseValue($p, $default); + } + } + + $isVariable = false; + + if (substr($name, -3) === '...') { + $isVariable = true; + $name = substr($name, 0, -3); + } + + $argDef[] = [$name, $default, $isVariable]; } - // special null value as default: translate to real null here - if ($value === [Type::T_KEYWORD, 'null']) { - $value = null; - } + try { + $vars = $this->applyArguments($argDef, $args, false, false); - $finalArgs[] = $value; - $keyArgs[$originalName] = $value; + // ensure all args are populated + foreach ($prototype as $i => $p) { + $name = explode(':', $p)[0]; + + if (! isset($finalArgs[$i])) { + $finalArgs[$i] = null; + } + } + + // apply positional args + foreach (array_values($vars) as $i => $val) { + $finalArgs[$i] = $val; + } + + $keyArgs = array_merge($keyArgs, $vars); + $prototypeHasMatch = true; + + // overwrite positional args with keyword args + foreach ($prototype as $i => $p) { + $name = explode(':', $p)[0]; + + if (isset($keyArgs[$name])) { + $finalArgs[$i] = $keyArgs[$name]; + } + + // special null value as default: translate to real null here + if ($finalArgs[$i] === [Type::T_KEYWORD, 'null']) { + $finalArgs[$i] = null; + } + } + // should we break if this prototype seems fulfilled? + } catch (CompilerException $e) { + $exceptionMessage = $e->getMessage(); + } } - if ($matchedPrototype['rest_argument'] !== null) { - $value = $vars[$matchedPrototype['rest_argument']]; - - $finalArgs[] = $value; - $keyArgs[$matchedPrototype['rest_argument']] = $value; + if ($exceptionMessage && ! $prototypeHasMatch) { + $this->throwError($exceptionMessage); } return [$finalArgs, $keyArgs]; } /** - * Parses a function prototype to the internal representation of arguments. + * Apply argument values per definition * - * The input is an array of strings describing each argument, as supported - * in {@see registerFunction}. Argument names don't include the `$`. - * The output contains the list of positional argument, with their normalized - * name (underscores are replaced by dashes), their original name (to be used - * in case of error reporting) and their default value. The output also contains - * the normalized name of the rest argument, or null if the function prototype - * is not variadic. - * - * @param string[] $prototype - * - * @return array - * @phpstan-return array{arguments: list, rest_argument: string|null} - */ - private function parseFunctionPrototype(array $prototype) - { - static $parser = null; - - $arguments = []; - $restArgument = null; - - foreach ($prototype as $p) { - if (null !== $restArgument) { - throw new \InvalidArgumentException('The argument declaration is invalid. The rest argument must be the last one.'); - } - - $default = null; - $p = explode(':', $p, 2); - $name = str_replace('_', '-', $p[0]); - - if (isset($p[1])) { - $defaultSource = trim($p[1]); - - if ($defaultSource === 'null') { - // differentiate this null from the static::$null - $default = [Type::T_KEYWORD, 'null']; - } else { - if (\is_null($parser)) { - $parser = $this->parserFactory(__METHOD__); - } - - $parser->parseValue($defaultSource, $default); - } - } - - if (substr($name, -3) === '...') { - $restArgument = substr($name, 0, -3); - } else { - $arguments[] = [$name, $p[0], $default]; - } - } - - return [ - 'arguments' => $arguments, - 'rest_argument' => $restArgument, - ]; - } - - /** - * Returns the function prototype for the given positional and named arguments. - * - * If no exact match is found, finds the closest approximation. Note that this - * doesn't guarantee that $positional and $names are valid for the returned - * prototype. - * - * @param array[] $prototypes - * @param int $positional - * @param array $names A set of names, as both keys and values + * @param array $argDef + * @param array $argValues + * @param boolean $storeInEnv + * @param boolean $reduce + * only used if $storeInEnv = false * * @return array * - * @phpstan-param non-empty-list, rest_argument: string|null}> $prototypes - * @phpstan-return array{arguments: list, rest_argument: string|null} + * @throws \Exception */ - private function selectFunctionPrototype(array $prototypes, $positional, array $names) + protected function applyArguments($argDef, $argValues, $storeInEnv = true, $reduce = true) { - $fuzzyMatch = null; - $minMismatchDistance = null; - - foreach ($prototypes as $prototype) { - // Ideally, find an exact match. - if ($this->checkPrototypeMatches($prototype, $positional, $names)) { - return $prototype; - } - - $mismatchDistance = \count($prototype['arguments']) - $positional; - - if ($minMismatchDistance !== null) { - if (abs($mismatchDistance) > abs($minMismatchDistance)) { - continue; - } - - // If two overloads have the same mismatch distance, favor the overload - // that has more arguments. - if (abs($mismatchDistance) === abs($minMismatchDistance) && $mismatchDistance < 0) { - continue; - } - } - - $minMismatchDistance = $mismatchDistance; - $fuzzyMatch = $prototype; + $output = []; + if (is_array($argValues) && count($argValues) && end($argValues) === static::$null) { + array_pop($argValues); } - return $fuzzyMatch; - } + if ($storeInEnv) { + $storeEnv = $this->getStoreEnv(); - /** - * Checks whether the argument invocation matches the callable prototype. - * - * The rules are similar to {@see verifyPrototype}. The boolean return value - * avoids the overhead of building and catching exceptions when the reason of - * not matching the prototype does not need to be known. - * - * @param array $prototype - * @param int $positional - * @param array $names - * - * @return bool - * - * @phpstan-param array{arguments: list, rest_argument: string|null} $prototype - */ - private function checkPrototypeMatches(array $prototype, $positional, array $names) - { - $nameUsed = 0; - - foreach ($prototype['arguments'] as $i => $argument) { - list ($name, $originalName, $default) = $argument; - - if ($i < $positional) { - if (isset($names[$name])) { - return false; - } - } elseif (isset($names[$name])) { - $nameUsed++; - } elseif ($default === null) { - return false; - } + $env = new Environment; + $env->store = $storeEnv->store; } - if ($prototype['rest_argument'] !== null) { - return true; + $hasVariable = false; + $args = []; + + foreach ($argDef as $i => $arg) { + list($name, $default, $isVariable) = $argDef[$i]; + + $args[$name] = [$i, $name, $default, $isVariable]; + $hasVariable |= $isVariable; } - if ($positional > \count($prototype['arguments'])) { - return false; - } + $splatSeparator = null; + $keywordArgs = []; + $deferredKeywordArgs = []; + $remaining = []; + $hasKeywordArgument = false; - if ($nameUsed < \count($names)) { - return false; - } - - return true; - } - - /** - * Verifies that the argument invocation is valid for the callable prototype. - * - * @param array $prototype - * @param int $positional - * @param array $names - * @param bool $hasSplat - * - * @return void - * - * @throws SassScriptException - * - * @phpstan-param array{arguments: list, rest_argument: string|null} $prototype - */ - private function verifyPrototype(array $prototype, $positional, array $names, $hasSplat) - { - $nameUsed = 0; - - foreach ($prototype['arguments'] as $i => $argument) { - list ($name, $originalName, $default) = $argument; - - if ($i < $positional) { - if (isset($names[$name])) { - throw new SassScriptException(sprintf('Argument $%s was passed both by position and by name.', $originalName)); - } - } elseif (isset($names[$name])) { - $nameUsed++; - } elseif ($default === null) { - throw new SassScriptException(sprintf('Missing argument $%s', $originalName)); - } - } - - if ($prototype['rest_argument'] !== null) { - return; - } - - if ($positional > \count($prototype['arguments'])) { - $message = sprintf( - 'Only %d %sargument%s allowed, but %d %s passed.', - \count($prototype['arguments']), - empty($names) ? '' : 'positional ', - \count($prototype['arguments']) === 1 ? '' : 's', - $positional, - $positional === 1 ? 'was' : 'were' - ); - if (!$hasSplat) { - throw new SassScriptException($message); - } - - $message = $this->addLocationToMessage($message); - $message .= "\nThis will be an error in future versions of Sass."; - $this->logger->warn($message, true); - } - - if ($nameUsed < \count($names)) { - $unknownNames = array_values(array_diff($names, array_column($prototype['arguments'], 0))); - $lastName = array_pop($unknownNames); - $message = sprintf( - 'No argument%s named $%s%s.', - $unknownNames ? 's' : '', - $unknownNames ? implode(', $', $unknownNames) . ' or $' : '', - $lastName - ); - throw new SassScriptException($message); - } - } - - /** - * Evaluates the argument from the invocation. - * - * This returns several things about this invocation: - * - the list of positional arguments - * - the map of named arguments, indexed by normalized names - * - the set of names used in the arguments (that's an array using the normalized names as keys for O(1) access) - * - the separator used by the list using the splat operator, if any - * - a boolean indicator whether any splat argument (list or map) was used, to support the incomplete error reporting. - * - * @param array[] $args - * @param bool $reduce Whether arguments should be reduced to their value - * - * @return array - * - * @throws SassScriptException - * - * @phpstan-return array{0: list, 1: array, 2: array, 3: string|null, 4: bool} - */ - private function evaluateArguments(array $args, $reduce = true) - { - // this represents trailing commas - if (\count($args) && end($args) === static::$null) { - array_pop($args); - } - - $splatSeparator = null; - $keywordArgs = []; - $names = []; - $positionalArgs = []; - $hasKeywordArgument = false; - $hasSplat = false; - - foreach ($args as $arg) { - if (!empty($arg[0])) { + // assign the keyword args + foreach ((array) $argValues as $arg) { + if (! empty($arg[0])) { $hasKeywordArgument = true; - assert(\is_string($arg[0][1])); - $name = str_replace('_', '-', $arg[0][1]); - - if (isset($keywordArgs[$name])) { - throw new SassScriptException(sprintf('Duplicate named argument $%s.', $arg[0][1])); + $name = $arg[0][1]; + if (! isset($args[$name])) { + foreach (array_keys($args) as $an) { + if (str_replace("_", "-", $an) === str_replace("_", "-", $name)) { + $name = $an; + break; + } + } } - - $keywordArgs[$name] = $this->maybeReduce($reduce, $arg[1]); - $names[$name] = $name; - } elseif (! empty($arg[2])) { - // $arg[2] means a var followed by ... in the arg ($list... ) + if (! isset($args[$name]) || $args[$name][3]) { + if ($hasVariable) { + $deferredKeywordArgs[$name] = $arg[1]; + } else { + $this->throwError("Mixin or function doesn't have an argument named $%s.", $arg[0][1]); + break; + } + } elseif ($args[$name][0] < count($remaining)) { + $this->throwError("The argument $%s was passed both by position and by name.", $arg[0][1]); + break; + } else { + $keywordArgs[$name] = $arg[1]; + } + } elseif ($arg[2] === true) { $val = $this->reduce($arg[1], true); - $hasSplat = true; if ($val[0] === Type::T_LIST) { - foreach ($val[2] as $item) { - if (\is_null($splatSeparator)) { - $splatSeparator = $val[1]; - } - - $positionalArgs[] = $this->maybeReduce($reduce, $item); - } - - if (isset($val[3]) && \is_array($val[3])) { - foreach ($val[3] as $name => $item) { - assert(\is_string($name)); - - $normalizedName = str_replace('_', '-', $name); - - if (isset($keywordArgs[$normalizedName])) { - throw new SassScriptException(sprintf('Duplicate named argument $%s.', $name)); + foreach ($val[2] as $name => $item) { + if (! is_numeric($name)) { + if (! isset($args[$name])) { + foreach (array_keys($args) as $an) { + if (str_replace("_", "-", $an) === str_replace("_", "-", $name)) { + $name = $an; + break; + } + } } - $keywordArgs[$normalizedName] = $this->maybeReduce($reduce, $item); - $names[$normalizedName] = $normalizedName; - $hasKeywordArgument = true; + if ($hasVariable) { + $deferredKeywordArgs[$name] = $item; + } else { + $keywordArgs[$name] = $item; + } + } else { + if (is_null($splatSeparator)) { + $splatSeparator = $val[1]; + } + + $remaining[] = $item; } } } elseif ($val[0] === Type::T_MAP) { @@ -6757,121 +5073,62 @@ EOL; $item = $val[2][$i]; if (! is_numeric($name)) { - $normalizedName = str_replace('_', '-', $name); - - if (isset($keywordArgs[$normalizedName])) { - throw new SassScriptException(sprintf('Duplicate named argument $%s.', $name)); + if (! isset($args[$name])) { + foreach (array_keys($args) as $an) { + if (str_replace("_", "-", $an) === str_replace("_", "-", $name)) { + $name = $an; + break; + } + } } - $keywordArgs[$normalizedName] = $this->maybeReduce($reduce, $item); - $names[$normalizedName] = $normalizedName; - $hasKeywordArgument = true; + if ($hasVariable) { + $deferredKeywordArgs[$name] = $item; + } else { + $keywordArgs[$name] = $item; + } } else { - if (\is_null($splatSeparator)) { + if (is_null($splatSeparator)) { $splatSeparator = $val[1]; } - $positionalArgs[] = $this->maybeReduce($reduce, $item); + $remaining[] = $item; } } - } elseif ($val[0] !== Type::T_NULL) { // values other than null are treated a single-element lists, while null is the empty list - $positionalArgs[] = $this->maybeReduce($reduce, $val); + } else { + $remaining[] = $val; } } elseif ($hasKeywordArgument) { - throw new SassScriptException('Positional arguments must come before keyword arguments.'); + $this->throwError('Positional arguments must come before keyword arguments.'); + break; } else { - $positionalArgs[] = $this->maybeReduce($reduce, $arg[1]); + $remaining[] = $arg[1]; } } - return [$positionalArgs, $keywordArgs, $names, $splatSeparator, $hasSplat]; - } - - /** - * @param bool $reduce - * @param array|Number $value - * - * @return array|Number - */ - private function maybeReduce($reduce, $value) - { - if ($reduce) { - return $this->reduce($value, true); - } - - return $value; - } - - /** - * Apply argument values per definition - * - * @param array[] $argDef - * @param array|null $argValues - * @param bool $storeInEnv - * @param bool $reduce only used if $storeInEnv = false - * - * @return array - * - * @phpstan-param list $argDef - * - * @throws \Exception - */ - protected function applyArguments($argDef, $argValues, $storeInEnv = true, $reduce = true) - { - $output = []; - - if (\is_null($argValues)) { - $argValues = []; - } - - if ($storeInEnv) { - $storeEnv = $this->getStoreEnv(); - - $env = new Environment(); - $env->store = $storeEnv->store; - } - - $prototype = ['arguments' => [], 'rest_argument' => null]; - $originalRestArgumentName = null; - - foreach ($argDef as $arg) { - list($name, $default, $isVariable) = $arg; - $normalizedName = str_replace('_', '-', $name); + foreach ($args as $arg) { + list($i, $name, $default, $isVariable) = $arg; if ($isVariable) { - $originalRestArgumentName = $name; - $prototype['rest_argument'] = $normalizedName; - } else { - $prototype['arguments'][] = [$normalizedName, $name, !empty($default) ? $default : null]; - } - } + $val = [Type::T_LIST, is_null($splatSeparator) ? ',' : $splatSeparator , [], $isVariable]; - list($positionalArgs, $namedArgs, $names, $splatSeparator, $hasSplat) = $this->evaluateArguments($argValues, $reduce); + for ($count = count($remaining); $i < $count; $i++) { + $val[2][] = $remaining[$i]; + } - $this->verifyPrototype($prototype, \count($positionalArgs), $names, $hasSplat); - - $vars = $this->applyArgumentsToDeclaration($prototype, $positionalArgs, $namedArgs, $splatSeparator); - - foreach ($prototype['arguments'] as $argument) { - list($normalizedName, $name) = $argument; - - if (!isset($vars[$normalizedName])) { + foreach ($deferredKeywordArgs as $itemName => $item) { + $val[2][$itemName] = $item; + } + } elseif (isset($remaining[$i])) { + $val = $remaining[$i]; + } elseif (isset($keywordArgs[$name])) { + $val = $keywordArgs[$name]; + } elseif (! empty($default)) { continue; - } - - $val = $vars[$normalizedName]; - - if ($storeInEnv) { - $this->set($name, $this->reduce($val, true), true, $env); } else { - $output[$name] = ($reduce ? $this->reduce($val, true) : $val); + $this->throwError("Missing argument $name"); + break; } - } - - if ($prototype['rest_argument'] !== null) { - assert($originalRestArgumentName !== null); - $name = $originalRestArgumentName; - $val = $vars[$prototype['rest_argument']]; if ($storeInEnv) { $this->set($name, $this->reduce($val, true), true, $env); @@ -6884,13 +5141,12 @@ EOL; $storeEnv->store = $env->store; } - foreach ($prototype['arguments'] as $argument) { - list($normalizedName, $name, $default) = $argument; + foreach ($args as $arg) { + list($i, $name, $default, $isVariable) = $arg; - if (isset($vars[$normalizedName])) { + if ($isVariable || isset($remaining[$i]) || isset($keywordArgs[$name]) || empty($default)) { continue; } - assert($default !== null); if ($storeInEnv) { $this->set($name, $this->reduce($default, true), true); @@ -6902,90 +5158,29 @@ EOL; return $output; } - /** - * Apply argument values per definition. - * - * This method assumes that the arguments are valid for the provided prototype. - * The validation with {@see verifyPrototype} must have been run before calling - * it. - * Arguments are returned as a map from the normalized argument names to the - * value. Additional arguments are collected in a sass argument list available - * under the name of the rest argument in the result. - * - * Defaults are not applied as they are resolved in a different environment. - * - * @param array $prototype - * @param array $positionalArgs - * @param array $namedArgs - * @param string|null $splatSeparator - * - * @return array - * - * @phpstan-param array{arguments: list, rest_argument: string|null} $prototype - */ - private function applyArgumentsToDeclaration(array $prototype, array $positionalArgs, array $namedArgs, $splatSeparator) - { - $output = []; - $minLength = min(\count($positionalArgs), \count($prototype['arguments'])); - - for ($i = 0; $i < $minLength; $i++) { - list($name) = $prototype['arguments'][$i]; - $val = $positionalArgs[$i]; - - $output[$name] = $val; - } - - $restNamed = $namedArgs; - - for ($i = \count($positionalArgs); $i < \count($prototype['arguments']); $i++) { - $argument = $prototype['arguments'][$i]; - list($name) = $argument; - - if (isset($namedArgs[$name])) { - $val = $namedArgs[$name]; - unset($restNamed[$name]); - } else { - continue; - } - - $output[$name] = $val; - } - - if ($prototype['rest_argument'] !== null) { - $name = $prototype['rest_argument']; - $rest = array_values(array_slice($positionalArgs, \count($prototype['arguments']))); - - $val = [Type::T_LIST, \is_null($splatSeparator) ? ',' : $splatSeparator , $rest, $restNamed]; - - $output[$name] = $val; - } - - return $output; - } - /** * Coerce a php value into a scss one * * @param mixed $value * - * @return array|Number + * @return array|\ScssPhp\ScssPhp\Node\Number */ protected function coerceValue($value) { - if (\is_array($value) || $value instanceof Number) { + if (is_array($value) || $value instanceof \ArrayAccess) { return $value; } - if (\is_bool($value)) { + if (is_bool($value)) { return $this->toBool($value); } - if (\is_null($value)) { + if (is_null($value)) { return static::$null; } if (is_numeric($value)) { - return new Number($value, ''); + return new Node\Number($value, ''); } if ($value === '') { @@ -7003,102 +5198,80 @@ EOL; } /** - * Tries to convert an item to a Sass map + * Coerce something to map * - * @param Number|array $item + * @param array $item * - * @return array|null + * @return array */ - private function tryMap($item) + protected function coerceMap($item) { - if ($item instanceof Number) { - return null; - } - if ($item[0] === Type::T_MAP) { return $item; } - if ( - $item[0] === Type::T_LIST && - $item[2] === [] - ) { + if ($item[0] === static::$emptyList[0] + && $item[1] === static::$emptyList[1] + && $item[2] === static::$emptyList[2]) { return static::$emptyMap; } - return null; - } - - /** - * Coerce something to map - * - * @param array|Number $item - * - * @return array|Number - */ - protected function coerceMap($item) - { - $map = $this->tryMap($item); - - if ($map !== null) { - return $map; - } - - return $item; + return [Type::T_MAP, [$item], [static::$null]]; } /** * Coerce something to list * - * @param array|Number $item - * @param string $delim - * @param bool $removeTrailingNull + * @param array $item + * @param string $delim * * @return array */ - protected function coerceList($item, $delim = ',', $removeTrailingNull = false) + protected function coerceList($item, $delim = ',') { - if ($item instanceof Number) { - return [Type::T_LIST, '', [$item]]; - } - - if ($item[0] === Type::T_LIST) { - // remove trailing null from the list - if ($removeTrailingNull && end($item[2]) === static::$null) { - array_pop($item[2]); - } - + if (isset($item) && $item[0] === Type::T_LIST) { return $item; } - if ($item[0] === Type::T_MAP) { + if (isset($item) && $item[0] === Type::T_MAP) { $keys = $item[1]; $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: + break; + + default: + $key = [Type::T_KEYWORD, $this->compileStringContent($this->coerceString($key))]; + break; + } + $list[] = [ Type::T_LIST, - ' ', + '', [$key, $value] ]; } - return [Type::T_LIST, $list ? ',' : '', $list]; + return [Type::T_LIST, ',', $list]; } - return [Type::T_LIST, '', [$item]]; + return [Type::T_LIST, $delim, ! isset($item) ? []: [$item]]; } /** * Coerce color for expression * - * @param array|Number $value + * @param array $value * - * @return array|Number + * @return array|null */ protected function coerceForExpression($value) { @@ -7112,17 +5285,12 @@ EOL; /** * Coerce value to color * - * @param array|Number $value - * @param bool $inRGBFunction + * @param array $value * * @return array|null */ protected function coerceColor($value, $inRGBFunction = false) { - if ($value instanceof Number) { - return null; - } - switch ($value[0]) { case Type::T_COLOR: for ($i = 1; $i <= 3; $i++) { @@ -7153,7 +5321,7 @@ EOL; case Type::T_LIST: if ($inRGBFunction) { - if (\count($value[2]) == 3 || \count($value[2]) == 4) { + if (count($value[2]) == 3 || count($value[2]) == 4) { $color = $value[2]; array_unshift($color, Type::T_COLOR); @@ -7164,17 +5332,16 @@ EOL; return null; case Type::T_KEYWORD: - if (! \is_string($value[1])) { + if (! is_string($value[1])) { return null; } $name = strtolower($value[1]); - // hexa color? if (preg_match('/^#([0-9a-f]+)$/i', $name, $m)) { - $nofValues = \strlen($m[1]); + $nofValues = strlen($m[1]); - if (\in_array($nofValues, [3, 4, 6, 8])) { + if (in_array($nofValues, [3, 4, 6, 8])) { $nbChannels = 3; $color = []; $num = hexdec($m[1]); @@ -7208,7 +5375,7 @@ EOL; if ($color[3] === 255) { $color[3] = 1; // fully opaque } else { - $color[3] = round($color[3] / 255, Number::PRECISION); + $color[3] = round($color[3] / 255, 3); } } @@ -7231,10 +5398,10 @@ EOL; } /** - * @param int|Number $value - * @param bool $isAlpha + * @param integer|\ScssPhp\ScssPhp\Node\Number $value + * @param boolean $isAlpha * - * @return int|mixed + * @return integer|mixed */ protected function compileRGBAValue($value, $isAlpha = false) { @@ -7246,35 +5413,44 @@ EOL; } /** - * @param mixed $value - * @param int|float $min - * @param int|float $max - * @param bool $isInt + * @param mixed $value + * @param integer|float $min + * @param integer|float $max + * @param boolean $isInt + * @param boolean $clamp + * @param boolean $modulo * - * @return int|mixed + * @return integer|mixed */ - protected function compileColorPartValue($value, $min, $max, $isInt = true) + protected function compileColorPartValue($value, $min, $max, $isInt = true, $clamp = true, $modulo = false) { if (! is_numeric($value)) { - if (\is_array($value)) { + if (is_array($value)) { $reduced = $this->reduce($value); - if ($reduced instanceof Number) { + if (is_object($reduced) && $value->type === Type::T_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); + if (is_object($value) && $value->type === Type::T_NUMBER) { + $num = $value->dimension; + + if (count($value->units)) { + $unit = array_keys($value->units); + $unit = reset($unit); + + switch ($unit) { + case '%': + $num *= $max / 100; + break; + default: + break; + } } $value = $num; - } elseif (\is_array($value)) { + } elseif (is_array($value)) { $value = $this->compileValue($value); } } @@ -7284,7 +5460,18 @@ EOL; $value = round($value); } - $value = min($max, max($min, $value)); + if ($clamp) { + $value = min($max, max($min, $value)); + } + + if ($modulo) { + $value = $value % $max; + + // still negative? + while ($value < $min) { + $value += $max; + } + } return $value; } @@ -7295,72 +5482,34 @@ EOL; /** * Coerce value to string * - * @param array|Number $value + * @param array $value * - * @return array + * @return array|null */ protected function coerceString($value) { if ($value[0] === Type::T_STRING) { - assert(\is_array($value)); - return $value; } return [Type::T_STRING, '', [$this->compileValue($value)]]; } - /** - * Assert value is a string - * - * This method deals with internal implementation details of the value - * representation where unquoted strings can sometimes be stored under - * other types. - * The returned value is always using the T_STRING type. - * - * @api - * - * @param array|Number $value - * @param string|null $varName - * - * @return array - * - * @throws SassScriptException - */ - 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); - throw SassScriptException::forArgument("$value is not a string.", $varName); - } - - return $this->coerceString($value); - } - /** * Coerce value to a percentage * - * @param array|Number $value + * @param array $value * - * @return int|float - * - * @deprecated + * @return integer|float */ protected function coercePercent($value) { - @trigger_error(sprintf('"%s" is deprecated since 1.7.0.', __METHOD__), E_USER_DEPRECATED); - - if ($value instanceof Number) { - if ($value->hasUnit('%')) { - return $value->getDimension() / 100; + if ($value[0] === Type::T_NUMBER) { + if (! empty($value[2]['%'])) { + return $value[1] / 100; } - return $value->getDimension(); + return $value[1]; } return 0; @@ -7371,24 +5520,21 @@ EOL; * * @api * - * @param array|Number $value - * @param string|null $varName + * @param array $value * * @return array * - * @throws SassScriptException + * @throws \Exception */ - public function assertMap($value, $varName = null) + public function assertMap($value) { - $map = $this->tryMap($value); + $value = $this->coerceMap($value); - if ($map === null) { - $value = $this->compileValue($value); - - throw SassScriptException::forArgument("$value is not a map.", $varName); + if ($value[0] !== Type::T_MAP) { + $this->throwError('expecting map, %s received', $value[0]); } - return $map; + return $value; } /** @@ -7396,7 +5542,7 @@ EOL; * * @api * - * @param array|Number $value + * @param array $value * * @return array * @@ -7405,55 +5551,30 @@ EOL; public function assertList($value) { if ($value[0] !== Type::T_LIST) { - throw $this->error('expecting list, %s received', $value[0]); + $this->throwError('expecting list, %s received', $value[0]); } - assert(\is_array($value)); return $value; } - /** - * Gets the keywords of an argument list. - * - * Keys in the returned array are normalized names (underscores are replaced with dashes) - * without the leading `$`. - * Calling this helper with anything that an argument list received for a rest argument - * of the function argument declaration is not supported. - * - * @param array|Number $value - * - * @return array - */ - public function getArgumentListKeywords($value) - { - if ($value[0] !== Type::T_LIST || !isset($value[3]) || !\is_array($value[3])) { - throw new \InvalidArgumentException('The argument is not a sass argument list.'); - } - - return $value[3]; - } - /** * Assert value is a color * * @api * - * @param array|Number $value - * @param string|null $varName + * @param array $value * * @return array * - * @throws SassScriptException + * @throws \Exception */ - public function assertColor($value, $varName = null) + public function assertColor($value) { if ($color = $this->coerceColor($value)) { return $color; } - $value = $this->compileValue($value); - - throw SassScriptException::forArgument("$value is not a color.", $varName); + $this->throwError('expecting color, %s received', $value[0]); } /** @@ -7461,64 +5582,21 @@ EOL; * * @api * - * @param array|Number $value - * @param string|null $varName + * @param array $value * - * @return Number + * @return integer|float * - * @throws SassScriptException + * @throws \Exception */ - public function assertNumber($value, $varName = null) + public function assertNumber($value) { - if (!$value instanceof Number) { - $value = $this->compileValue($value); - throw SassScriptException::forArgument("$value is not a number.", $varName); + if ($value[0] !== Type::T_NUMBER) { + $this->throwError('expecting number, %s received', $value[0]); } - return $value; + return $value[1]; } - /** - * Assert value is a integer - * - * @api - * - * @param array|Number $value - * @param string|null $varName - * - * @return int - * - * @throws SassScriptException - */ - public function assertInteger($value, $varName = null) - { - $value = $this->assertNumber($value, $varName)->getDimension(); - if (round($value - \intval($value), Number::PRECISION) > 0) { - throw SassScriptException::forArgument("$value is not an integer.", $varName); - } - - return intval($value); - } - - /** - * Extract the ... / alpha on the last argument of channel arg - * in color functions - * - * @param array $args - * @return array - */ - private function extractSlashAlphaInColorFunction($args) - { - $last = end($args); - if (\count($args) === 3 && $last[0] === Type::T_EXPRESSION && $last[1] === '/') { - array_pop($args); - $args[] = $last[2]; - $args[] = $last[3]; - } - return $args; - } - - /** * Make sure a color's components don't go out of bounds * @@ -7536,10 +5614,6 @@ EOL; if ($c[$i] > 255) { $c[$i] = 255; } - - if (!\is_int($c[$i])) { - $c[$i] = round($c[$i]); - } } return $c; @@ -7548,11 +5622,11 @@ EOL; /** * Convert RGB to HSL * - * @internal + * @api * - * @param int $red - * @param int $green - * @param int $blue + * @param integer $red + * @param integer $green + * @param integer $blue * * @return array */ @@ -7577,12 +5651,12 @@ EOL; $h = 60 * ($green - $blue) / $d; } elseif ($green == $max) { $h = 60 * ($blue - $red) / $d + 120; - } else { + } elseif ($blue == $max) { $h = 60 * ($red - $green) / $d + 240; } } - return [Type::T_HSL, fmod($h + 360, 360), $s * 100, $l / 5.1]; + return [Type::T_HSL, fmod($h, 360), $s * 100, $l / 5.1]; } /** @@ -7611,7 +5685,7 @@ EOL; } if ($h * 3 < 2) { - return $m1 + ($m2 - $m1) * (2 / 3 - $h) * 6; + return $m1 + ($m2 - $m1) * (2/3 - $h) * 6; } return $m1; @@ -7620,11 +5694,11 @@ EOL; /** * Convert HSL to RGB * - * @internal + * @api * - * @param int|float $hue H from 0 to 360 - * @param int|float $saturation S from 0 to 100 - * @param int|float $lightness L from 0 to 100 + * @param integer $hue H from 0 to 360 + * @param integer $saturation S from 0 to 100 + * @param integer $lightness L from 0 to 100 * * @return array */ @@ -7641,133 +5715,35 @@ EOL; $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]; return $out; } - /** - * Convert HWB to RGB - * https://www.w3.org/TR/css-color-4/#hwb-to-rgb - * - * @api - * - * @param int|float $hue H from 0 to 360 - * @param int|float $whiteness W from 0 to 100 - * @param int|float $blackness B from 0 to 100 - * - * @return array - */ - private function HWBtoRGB($hue, $whiteness, $blackness) - { - $w = min(100, max(0, $whiteness)) / 100; - $b = min(100, max(0, $blackness)) / 100; - - $sum = $w + $b; - if ($sum > 1.0) { - $w = $w / $sum; - $b = $b / $sum; - } - $b = min(1.0 - $w, $b); - - $rgb = $this->toRGB($hue, 100, 50); - for($i = 1; $i < 4; $i++) { - $rgb[$i] *= (1.0 - $w - $b); - $rgb[$i] = round($rgb[$i] + 255 * $w + 0.0001); - } - - return $rgb; - } - - /** - * Convert RGB to HWB - * - * @api - * - * @param int $red - * @param int $green - * @param int $blue - * - * @return array - */ - private function RGBtoHWB($red, $green, $blue) - { - $min = min($red, $green, $blue); - $max = max($red, $green, $blue); - - $d = $max - $min; - - if ((int) $d === 0) { - $h = 0; - } else { - - if ($red == $max) { - $h = 60 * ($green - $blue) / $d; - } elseif ($green == $max) { - $h = 60 * ($blue - $red) / $d + 120; - } else { - $h = 60 * ($red - $green) / $d + 240; - } - } - - return [Type::T_HWB, fmod($h, 360), $min / 255 * 100, 100 - $max / 255 *100]; - } - - // Built in functions - protected static $libCall = ['function', 'args...']; - protected function libCall($args) + protected static $libCall = ['name', 'args...']; + protected function libCall($args, $kwargs) { - $functionReference = $args[0]; + $name = $this->compileStringContent($this->coerceString($this->reduce(array_shift($args), true))); + $callArgs = []; - if (in_array($functionReference[0], [Type::T_STRING, Type::T_KEYWORD])) { - $name = $this->compileStringContent($this->coerceString($functionReference)); - $warning = "Passing a string to call() is deprecated and will be illegal\n" - . "in Sass 4.0. Use call(function-reference($name)) instead."; - Warn::deprecation($warning); - $functionReference = $this->libGetFunction([$this->assertString($functionReference, 'function')]); + // $kwargs['args'] is [Type::T_LIST, ',', [..]] + foreach ($kwargs['args'][2] as $varname => $arg) { + if (is_numeric($varname)) { + $varname = null; + } else { + $varname = [ 'var', $varname]; + } + + $callArgs[] = [$varname, $arg, false]; } - 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 = [ - [null, $args[1], true] - ]; - - return $this->reduce([Type::T_FUNCTION_CALL, $functionReference, $callArgs]); - } - - - protected static $libGetFunction = [ - ['name'], - ['name', 'css'] - ]; - protected function libGetFunction($args) - { - $name = $this->compileStringContent($this->assertString(array_shift($args), 'name')); - $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); + return $this->reduce([Type::T_FUNCTION_CALL, $name, $callArgs]); } protected static $libIf = ['condition', 'if-true', 'if-false:']; @@ -7787,8 +5763,11 @@ EOL; { list($list, $value) = $args; - if ( - $list[0] === Type::T_MAP || + if ($value[0] === Type::T_MAP) { + return static::$null; + } + + if ($list[0] === Type::T_MAP || $list[0] === Type::T_STRING || $list[0] === Type::T_KEYWORD || $list[0] === Type::T_INTERPOLATE @@ -7800,22 +5779,6 @@ EOL; 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) { @@ -7824,7 +5787,7 @@ EOL; $key = array_search($this->normalizeValue($value), $values); - return false === $key ? static::$null : new Number($key + 1, ''); + return false === $key ? static::$null : $key + 1; } protected static $libRgb = [ @@ -7833,17 +5796,9 @@ EOL; ['channels'], ['red', 'green', 'blue'], ['red', 'green', 'blue', 'alpha'] ]; - - /** - * @param array $args - * @param array $kwargs - * @param string $funcName - * - * @return array - */ protected function libRgb($args, $kwargs, $funcName = 'rgb') { - switch (\count($args)) { + switch (count($args)) { case 1: if (! $color = $this->coerceColor($args[0], true)) { $color = [Type::T_STRING, '', [$funcName . '(', $args[0], ')']]; @@ -7854,7 +5809,7 @@ EOL; $color = [Type::T_COLOR, $args[0], $args[1], $args[2]]; if (! $color = $this->coerceColor($color)) { - $color = [Type::T_STRING, '', [$funcName . '(', $args[0], ', ', $args[1], ', ', $args[2], ')']]; + $color = [Type::T_STRING, '', [$funcName .'(', $args[0], ', ', $args[1], ', ', $args[2], ')']]; } return $color; @@ -7870,7 +5825,7 @@ EOL; [$funcName . '(', $color[1], ', ', $color[2], ', ', $color[3], ', ', $alpha, ')']]; } } else { - $color = [Type::T_STRING, '', [$funcName . '(', $args[0], ', ', $args[1], ')']]; + $color = [Type::T_STRING, '', [$funcName . '(', $args[0], ')']]; } break; @@ -7899,125 +5854,32 @@ EOL; return $this->libRgb($args, $kwargs, 'rgba'); } - /** - * Helper function for adjust_color, change_color, and scale_color - * - * @param array $args - * @param string $operation - * @param callable $fn - * - * @return array - * - * @phpstan-param callable(float|int, float|int|null, float|int): (float|int) $fn - */ - protected function alterColor(array $args, $operation, $fn) + // helper function for adjust_color, change_color, and scale_color + protected function alterColor($args, $fn) { - $color = $this->assertColor($args[0], 'color'); + $color = $this->assertColor($args[0]); - if ($args[1][2]) { - throw new SassScriptException('Only one positional argument is allowed. All other arguments must be passed by name.'); - } + foreach ([1 => 1, 2 => 2, 3 => 3, 7 => 4] as $iarg => $irgba) { + if (isset($args[$iarg])) { + $val = $this->assertNumber($args[$iarg]); - $kwargs = $this->getArgumentListKeywords($args[1]); - - $scale = $operation === 'scale'; - $change = $operation === 'change'; - - /** @phpstan-var callable(string, float|int, bool=, bool=): (float|int|null) $getParam */ - $getParam = function ($name, $max, $checkPercent = false, $assertPercent = false) use (&$kwargs, $scale, $change) { - if (!isset($kwargs[$name])) { - return null; - } - - $number = $this->assertNumber($kwargs[$name], $name); - unset($kwargs[$name]); - - if (!$scale && $checkPercent) { - if (!$number->hasUnit('%')) { - $warning = $this->error("{$name} Passing a number `$number` without unit % is deprecated."); - $this->logger->warn($warning->getMessage(), true); + if (! isset($color[$irgba])) { + $color[$irgba] = (($irgba < 4) ? 0 : 1); } + + $color[$irgba] = call_user_func($fn, $color[$irgba], $val, $iarg); } - - if ($scale || $assertPercent) { - $number->assertUnit('%', $name); - } - - if ($scale) { - $max = 100; - } - - return $number->valueInRange($change ? 0 : -$max, $max, $name); - }; - - $alpha = $getParam('alpha', 1); - $red = $getParam('red', 255); - $green = $getParam('green', 255); - $blue = $getParam('blue', 255); - - if ($scale || !isset($kwargs['hue'])) { - $hue = null; - } else { - $hueNumber = $this->assertNumber($kwargs['hue'], 'hue'); - unset($kwargs['hue']); - $hue = $hueNumber->getDimension(); - } - $saturation = $getParam('saturation', 100, true); - $lightness = $getParam('lightness', 100, true); - $whiteness = $getParam('whiteness', 100, false, true); - $blackness = $getParam('blackness', 100, false, true); - - if (!empty($kwargs)) { - $unknownNames = array_keys($kwargs); - $lastName = array_pop($unknownNames); - $message = sprintf( - 'No argument%s named $%s%s.', - $unknownNames ? 's' : '', - $unknownNames ? implode(', $', $unknownNames) . ' or $' : '', - $lastName - ); - throw new SassScriptException($message); } - $hasRgb = $red !== null || $green !== null || $blue !== null; - $hasSL = $saturation !== null || $lightness !== null; - $hasWB = $whiteness !== null || $blackness !== null; - - if ($hasRgb && ($hasSL || $hasWB || $hue !== null)) { - throw new SassScriptException(sprintf('RGB parameters may not be passed along with %s parameters.', $hasWB ? 'HWB' : 'HSL')); - } - - if ($hasWB && $hasSL) { - throw new SassScriptException('HSL parameters may not be passed along with HWB parameters.'); - } - - if ($hasRgb) { - $color[1] = round($fn($color[1], $red, 255)); - $color[2] = round($fn($color[2], $green, 255)); - $color[3] = round($fn($color[3], $blue, 255)); - } elseif ($hasWB) { - $hwb = $this->RGBtoHWB($color[1], $color[2], $color[3]); - if ($hue !== null) { - $hwb[1] = $change ? $hue : $hwb[1] + $hue; - } - $hwb[2] = $fn($hwb[2], $whiteness, 100); - $hwb[3] = $fn($hwb[3], $blackness, 100); - - $rgb = $this->HWBtoRGB($hwb[1], $hwb[2], $hwb[3]); - - if (isset($color[4])) { - $rgb[4] = $color[4]; - } - - $color = $rgb; - } elseif ($hue !== null || $hasSL) { + if (! empty($args[4]) || ! empty($args[5]) || ! empty($args[6])) { $hsl = $this->toHSL($color[1], $color[2], $color[3]); - if ($hue !== null) { - $hsl[1] = $change ? $hue : $hsl[1] + $hue; + foreach ([4 => 1, 5 => 2, 6 => 3] as $iarg => $ihsl) { + if (! empty($args[$iarg])) { + $val = $this->assertNumber($args[$iarg]); + $hsl[$ihsl] = call_user_func($fn, $hsl[$ihsl], $val, $iarg); + } } - $hsl[2] = $fn($hsl[2], $saturation, 100); - $hsl[3] = $fn($hsl[3], $lightness, 100); $rgb = $this->toRGB($hsl[1], $hsl[2], $hsl[3]); @@ -8028,54 +5890,58 @@ EOL; $color = $rgb; } - if ($alpha !== null) { - $existingAlpha = isset($color[4]) ? $color[4] : 1; - $color[4] = $fn($existingAlpha, $alpha, 1); - } - return $color; } - protected static $libAdjustColor = ['color', 'kwargs...']; + protected static $libAdjustColor = [ + 'color', 'red:null', 'green:null', 'blue:null', + 'hue:null', 'saturation:null', 'lightness:null', 'alpha:null' + ]; protected function libAdjustColor($args) { - return $this->alterColor($args, 'adjust', function ($base, $alter, $max) { - if ($alter === null) { - return $base; - } - - $new = $base + $alter; - - if ($new < 0) { - return 0; - } - - if ($new > $max) { - return $max; - } - - return $new; + return $this->alterColor($args, function ($base, $alter, $i) { + return $base + $alter; }); } - protected static $libChangeColor = ['color', 'kwargs...']; + protected static $libChangeColor = [ + 'color', 'red:null', 'green:null', 'blue:null', + 'hue:null', 'saturation:null', 'lightness:null', 'alpha:null' + ]; protected function libChangeColor($args) { - return $this->alterColor($args,'change', function ($base, $alter, $max) { - if ($alter === null) { - return $base; - } - + return $this->alterColor($args, function ($base, $alter, $i) { return $alter; }); } - protected static $libScaleColor = ['color', 'kwargs...']; + protected static $libScaleColor = [ + 'color', 'red:null', 'green:null', 'blue:null', + 'hue:null', 'saturation:null', 'lightness:null', 'alpha:null' + ]; protected function libScaleColor($args) { - return $this->alterColor($args, 'scale', function ($base, $scale, $max) { - if ($scale === null) { - return $base; + return $this->alterColor($args, function ($base, $scale, $i) { + // 1, 2, 3 - rgb + // 4, 5, 6 - hsl + // 7 - a + switch ($i) { + case 1: + case 2: + case 3: + $max = 255; + break; + + case 4: + $max = 360; + break; + + case 7: + $max = 1; + break; + + default: + $max = 100; } $scale = $scale / 100; @@ -8092,11 +5958,6 @@ EOL; 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 [Type::T_STRING, '', [sprintf('#%02X%02X%02X%02X', $color[4], $color[1], $color[2], $color[3])]]; @@ -8107,11 +5968,7 @@ EOL; { $color = $this->coerceColor($args[0]); - if (\is_null($color)) { - throw $this->error('Error: argument `$color` of `red($color)` must be a color'); - } - - return new Number((int) $color[1], ''); + return $color[1]; } protected static $libGreen = ['color']; @@ -8119,11 +5976,7 @@ EOL; { $color = $this->coerceColor($args[0]); - if (\is_null($color)) { - throw $this->error('Error: argument `$color` of `green($color)` must be a color'); - } - - return new Number((int) $color[2], ''); + return $color[2]; } protected static $libBlue = ['color']; @@ -8131,18 +5984,14 @@ EOL; { $color = $this->coerceColor($args[0]); - if (\is_null($color)) { - throw $this->error('Error: argument `$color` of `blue($color)` must be a color'); - } - - return new Number((int) $color[3], ''); + return $color[3]; } protected static $libAlpha = ['color']; protected function libAlpha($args) { if ($color = $this->coerceColor($args[0])) { - return new Number(isset($color[4]) ? $color[4] : 1, ''); + return isset($color[4]) ? $color[4] : 1; } // this might be the IE function, so return value unchanged @@ -8154,7 +6003,7 @@ EOL; { $value = $args[0]; - if ($value instanceof Number) { + if ($value[0] === Type::T_NUMBER) { return null; } @@ -8162,129 +6011,78 @@ EOL; } // mix two colors - protected static $libMix = [ - ['color1', 'color2', 'weight:50%'], - ['color-1', 'color-2', 'weight:50%'] - ]; + protected static $libMix = ['color-1', 'color-2', 'weight:0.5']; protected function libMix($args) { list($first, $second, $weight) = $args; - $first = $this->assertColor($first, 'color1'); - $second = $this->assertColor($second, 'color2'); - $weightScale = $this->assertNumber($weight, 'weight')->valueInRange(0, 100, 'weight') / 100; + $first = $this->assertColor($first); + $second = $this->assertColor($second); + + if (! isset($weight)) { + $weight = 0.5; + } else { + $weight = $this->coercePercent($weight); + } $firstAlpha = isset($first[4]) ? $first[4] : 1; $secondAlpha = isset($second[4]) ? $second[4] : 1; - $normalizedWeight = $weightScale * 2 - 1; - $alphaDistance = $firstAlpha - $secondAlpha; + $w = $weight * 2 - 1; + $a = $firstAlpha - $secondAlpha; - $combinedWeight = $normalizedWeight * $alphaDistance == -1 ? $normalizedWeight : ($normalizedWeight + $alphaDistance) / (1 + $normalizedWeight * $alphaDistance); - $weight1 = ($combinedWeight + 1) / 2.0; - $weight2 = 1.0 - $weight1; + $w1 = (($w * $a === -1 ? $w : ($w + $a) / (1 + $w * $a)) + 1) / 2.0; + $w2 = 1.0 - $w1; $new = [Type::T_COLOR, - $weight1 * $first[1] + $weight2 * $second[1], - $weight1 * $first[2] + $weight2 * $second[2], - $weight1 * $first[3] + $weight2 * $second[3], + $w1 * $first[1] + $w2 * $second[1], + $w1 * $first[2] + $w2 * $second[2], + $w1 * $first[3] + $w2 * $second[3], ]; if ($firstAlpha != 1.0 || $secondAlpha != 1.0) { - $new[] = $firstAlpha * $weightScale + $secondAlpha * (1 - $weightScale); + $new[] = $firstAlpha * $weight + $secondAlpha * (1 - $weight); } return $this->fixColor($new); } - protected static $libHsl = [ + protected static $libHsl =[ ['channels'], - ['hue', 'saturation'], ['hue', 'saturation', 'lightness'], ['hue', 'saturation', 'lightness', 'alpha'] ]; - - /** - * @param array $args - * @param array $kwargs - * @param string $funcName - * - * @return array|null - */ protected function libHsl($args, $kwargs, $funcName = 'hsl') { - $args_to_check = $args; - - if (\count($args) == 1) { - if ($args[0][0] !== Type::T_LIST || \count($args[0][2]) < 3 || \count($args[0][2]) > 4) { + 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], ')']]; } $args = $args[0][2]; - $args_to_check = $kwargs['channels'][2]; } - if (\count($args) === 2) { - // if var() is used as an argument, return as a css function - foreach ($args as $arg) { - if ($arg[0] === Type::T_FUNCTION && in_array($arg[1], ['var'])) { - return null; - } - } + $hue = $this->compileColorPartValue($args[0], 0, 360, false, false, true); + $saturation = $this->compileColorPartValue($args[1], 0, 100, false); + $lightness = $this->compileColorPartValue($args[2], 0, 100, false); - throw new SassScriptException('Missing argument $lightness.'); - } - - foreach ($kwargs as $arg) { - if (in_array($arg[0], [Type::T_FUNCTION_CALL, Type::T_FUNCTION]) && in_array($arg[1], ['min', 'max'])) { - return null; - } - } - - foreach ($args_to_check as $k => $arg) { - if (in_array($arg[0], [Type::T_FUNCTION_CALL, Type::T_FUNCTION]) && 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) { + 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)) { + if (! is_numeric($hue) || ! is_numeric($saturation) || ! is_numeric($lightness) || ! 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) { + if (! is_numeric($hue) || ! is_numeric($saturation) || ! is_numeric($lightness)) { return [Type::T_STRING, '', [$funcName . '(', $args[0], ', ', $args[1], ', ', $args[2], ')']]; } } - $hueValue = fmod($hue->getDimension(), 360); + $color = $this->toRGB($hue, $saturation, $lightness); - while ($hueValue < 0) { - $hueValue += 360; - } - - $color = $this->toRGB($hueValue, max(0, min($saturation->getDimension(), 100)), max(0, min($lightness->getDimension(), 100))); - - if (! \is_null($alpha)) { + if (! is_null($alpha)) { $color[4] = $alpha; } @@ -8293,9 +6091,7 @@ EOL; protected static $libHsla = [ ['channels'], - ['hue', 'saturation'], - ['hue', 'saturation', 'lightness'], - ['hue', 'saturation', 'lightness', 'alpha']]; + ['hue', 'saturation', 'lightness', 'alpha:1'] ]; protected function libHsla($args, $kwargs) { return $this->libHsl($args, $kwargs, 'hsla'); @@ -8304,173 +6100,34 @@ EOL; protected static $libHue = ['color']; protected function libHue($args) { - $color = $this->assertColor($args[0], 'color'); + $color = $this->assertColor($args[0]); $hsl = $this->toHSL($color[1], $color[2], $color[3]); - return new Number($hsl[1], 'deg'); + return new Node\Number($hsl[1], 'deg'); } protected static $libSaturation = ['color']; protected function libSaturation($args) { - $color = $this->assertColor($args[0], 'color'); + $color = $this->assertColor($args[0]); $hsl = $this->toHSL($color[1], $color[2], $color[3]); - return new Number($hsl[2], '%'); + return new Node\Number($hsl[2], '%'); } protected static $libLightness = ['color']; protected function libLightness($args) { - $color = $this->assertColor($args[0], 'color'); + $color = $this->assertColor($args[0]); $hsl = $this->toHSL($color[1], $color[2], $color[3]); - return new Number($hsl[3], '%'); + return new Node\Number($hsl[3], '%'); } - /* - * Todo : a integrer dans le futur module color - protected static $libHwb = [ - ['channels'], - ['hue', 'whiteness', 'blackness'], - ['hue', 'whiteness', 'blackness', 'alpha'] ]; - protected function libHwb($args, $kwargs, $funcName = 'hwb') - { - $args_to_check = $args; - - if (\count($args) == 1) { - if ($args[0][0] !== Type::T_LIST) { - throw $this->error("Missing elements \$whiteness and \$blackness"); - } - - if (\trim($args[0][1])) { - throw $this->error("\$channels must be a space-separated list."); - } - - if (! empty($args[0]['enclosing'])) { - throw $this->error("\$channels must be an unbracketed list."); - } - - $args = $args[0][2]; - if (\count($args) > 3) { - throw $this->error("hwb() : Only 3 elements are allowed but ". \count($args) . "were passed"); - } - - $args_to_check = $this->extractSlashAlphaInColorFunction($kwargs['channels'][2]); - if (\count($args_to_check) !== \count($kwargs['channels'][2])) { - $args = $args_to_check; - } - } - - if (\count($args_to_check) < 2) { - throw $this->error("Missing elements \$whiteness and \$blackness"); - } - if (\count($args_to_check) < 3) { - throw $this->error("Missing element \$blackness"); - } - if (\count($args_to_check) > 4) { - throw $this->error("hwb() : Only 4 elements are allowed but ". \count($args) . "were passed"); - } - - foreach ($kwargs as $k => $arg) { - if (in_array($arg[0], [Type::T_FUNCTION_CALL]) && in_array($arg[1], ['min', 'max'])) { - return null; - } - } - - foreach ($args_to_check as $k => $arg) { - if (in_array($arg[0], [Type::T_FUNCTION_CALL]) && in_array($arg[1], ['min', 'max'])) { - if (count($kwargs) > 1 || ($k >= 2 && count($args) === 4)) { - return null; - } - - $args[$k] = $this->stringifyFncallArgs($arg); - } - - if ( - $k >= 2 && count($args) === 4 && - in_array($arg[0], [Type::T_FUNCTION_CALL, Type::T_FUNCTION]) && - in_array($arg[1], ['calc','env']) - ) { - return null; - } - } - - $hue = $this->reduce($args[0]); - $whiteness = $this->reduce($args[1]); - $blackness = $this->reduce($args[2]); - $alpha = null; - - if (\count($args) === 4) { - $alpha = $this->compileColorPartValue($args[3], 0, 1, false); - - if (! \is_numeric($alpha)) { - $val = $this->compileValue($args[3]); - throw $this->error("\$alpha: $val is not a number"); - } - } - - $this->assertNumber($hue, 'hue'); - $this->assertUnit($whiteness, ['%'], 'whiteness'); - $this->assertUnit($blackness, ['%'], 'blackness'); - - $this->assertRange($whiteness, 0, 100, "0% and 100%", "whiteness"); - $this->assertRange($blackness, 0, 100, "0% and 100%", "blackness"); - - $w = $whiteness->getDimension(); - $b = $blackness->getDimension(); - - $hueValue = $hue->getDimension() % 360; - - while ($hueValue < 0) { - $hueValue += 360; - } - - $color = $this->HWBtoRGB($hueValue, $w, $b); - - if (! \is_null($alpha)) { - $color[4] = $alpha; - } - - return $color; - } - - protected static $libWhiteness = ['color']; - protected function libWhiteness($args, $kwargs, $funcName = 'whiteness') { - - $color = $this->assertColor($args[0]); - $hwb = $this->RGBtoHWB($color[1], $color[2], $color[3]); - - return new Number($hwb[2], '%'); - } - - protected static $libBlackness = ['color']; - protected function libBlackness($args, $kwargs, $funcName = 'blackness') { - - $color = $this->assertColor($args[0]); - $hwb = $this->RGBtoHWB($color[1], $color[2], $color[3]); - - return new Number($hwb[3], '%'); - } - */ - - /** - * @param array $color - * @param int $idx - * @param int|float $amount - * - * @return array - */ protected function adjustHsl($color, $idx, $amount) { $hsl = $this->toHSL($color[1], $color[2], $color[3]); $hsl[$idx] += $amount; - - if ($idx !== 1) { - // Clamp the saturation and lightness - $hsl[$idx] = min(max(0, $hsl[$idx]), 100); - } - $out = $this->toRGB($hsl[1], $hsl[2], $hsl[3]); if (isset($color[4])) { @@ -8483,8 +6140,8 @@ EOL; protected static $libAdjustHue = ['color', 'degrees']; protected function libAdjustHue($args) { - $color = $this->assertColor($args[0], 'color'); - $degrees = $this->assertNumber($args[1], 'degrees')->getDimension(); + $color = $this->assertColor($args[0]); + $degrees = $this->assertNumber($args[1]); return $this->adjustHsl($color, 1, $degrees); } @@ -8492,7 +6149,7 @@ EOL; protected static $libLighten = ['color', 'amount']; protected function libLighten($args) { - $color = $this->assertColor($args[0], 'color'); + $color = $this->assertColor($args[0]); $amount = Util::checkRange('amount', new Range(0, 100), $args[1], '%'); return $this->adjustHsl($color, 3, $amount); @@ -8501,36 +6158,34 @@ EOL; protected static $libDarken = ['color', 'amount']; protected function libDarken($args) { - $color = $this->assertColor($args[0], 'color'); + $color = $this->assertColor($args[0]); $amount = Util::checkRange('amount', new Range(0, 100), $args[1], '%'); return $this->adjustHsl($color, 3, -$amount); } - protected static $libSaturate = [['color', 'amount'], ['amount']]; + protected static $libSaturate = [['color', 'amount'], ['number']]; protected function libSaturate($args) { $value = $args[0]; - if (count($args) === 1) { - $this->assertNumber($args[0], 'amount'); - + if ($value[0] === Type::T_NUMBER) { return null; } - $color = $this->assertColor($args[0], 'color'); - $amount = $this->assertNumber($args[1], 'amount'); + $color = $this->assertColor($value); + $amount = 100 * $this->coercePercent($args[1]); - return $this->adjustHsl($color, 2, $amount->valueInRange(0, 100, 'amount')); + return $this->adjustHsl($color, 2, $amount); } protected static $libDesaturate = ['color', 'amount']; protected function libDesaturate($args) { - $color = $this->assertColor($args[0], 'color'); - $amount = $this->assertNumber($args[1], 'amount'); + $color = $this->assertColor($args[0]); + $amount = 100 * $this->coercePercent($args[1]); - return $this->adjustHsl($color, 2, -$amount->valueInRange(0, 100, 'amount')); + return $this->adjustHsl($color, 2, -$amount); } protected static $libGrayscale = ['color']; @@ -8538,51 +6193,55 @@ EOL; { $value = $args[0]; - if ($value instanceof Number) { + if ($value[0] === Type::T_NUMBER) { return null; } - return $this->adjustHsl($this->assertColor($value, 'color'), 2, -100); + return $this->adjustHsl($this->assertColor($value), 2, -100); } protected static $libComplement = ['color']; protected function libComplement($args) { - return $this->adjustHsl($this->assertColor($args[0], 'color'), 1, 180); + return $this->adjustHsl($this->assertColor($args[0]), 1, 180); } - protected static $libInvert = ['color', 'weight:100%']; + protected static $libInvert = ['color', 'weight:1']; protected function libInvert($args) { - $value = $args[0]; + list($value, $weight) = $args; - $weight = $this->assertNumber($args[1], 'weight'); - - if ($value instanceof Number) { - if ($weight->getDimension() != 100 || !$weight->hasUnit('%')) { - throw new SassScriptException('Only one argument may be passed to the plain-CSS invert() function.'); - } + if (! isset($weight)) { + $weight = 1; + } else { + $weight = $this->coercePercent($weight); + } + if ($value[0] === Type::T_NUMBER) { return null; } - $color = $this->assertColor($value, 'color'); + $color = $this->assertColor($value); $inverted = $color; $inverted[1] = 255 - $inverted[1]; $inverted[2] = 255 - $inverted[2]; $inverted[3] = 255 - $inverted[3]; - return $this->libMix([$inverted, $color, $weight]); + if ($weight < 1) { + return $this->libMix([$inverted, $color, [Type::T_NUMBER, $weight]]); + } + + return $inverted; } // increases opacity by amount protected static $libOpacify = ['color', 'amount']; protected function libOpacify($args) { - $color = $this->assertColor($args[0], 'color'); - $amount = $this->assertNumber($args[1], 'amount'); + $color = $this->assertColor($args[0]); + $amount = $this->coercePercent($args[1]); - $color[4] = (isset($color[4]) ? $color[4] : 1) + $amount->valueInRange(0, 1, 'amount'); + $color[4] = (isset($color[4]) ? $color[4] : 1) + $amount; $color[4] = min(1, max(0, $color[4])); return $color; @@ -8598,10 +6257,10 @@ EOL; protected static $libTransparentize = ['color', 'amount']; protected function libTransparentize($args) { - $color = $this->assertColor($args[0], 'color'); - $amount = $this->assertNumber($args[1], 'amount'); + $color = $this->assertColor($args[0]); + $amount = $this->coercePercent($args[1]); - $color[4] = (isset($color[4]) ? $color[4] : 1) - $amount->valueInRange(0, 1, 'amount'); + $color[4] = (isset($color[4]) ? $color[4] : 1) - $amount; $color[4] = min(1, max(0, $color[4])); return $color; @@ -8616,165 +6275,166 @@ EOL; protected static $libUnquote = ['string']; protected function libUnquote($args) { - try { - $str = $this->assertString($args[0], 'string'); - } catch (SassScriptException $e) { - $value = $this->compileValue($args[0]); - $fname = $this->getPrettyPath($this->sourceNames[$this->sourceIndex]); - $line = $this->sourceLine; + $str = $args[0]; - $message = "Passing $value, a non-string value, to unquote() -will be an error in future versions of Sass.\n on line $line of $fname"; - - $this->logger->warn($message, true); - - return $args[0]; + if ($str[0] === Type::T_STRING) { + $str[1] = ''; } - $str[1] = ''; - return $str; } protected static $libQuote = ['string']; protected function libQuote($args) { - $value = $this->assertString($args[0], 'string'); + $value = $args[0]; - $value[1] = '"'; + if ($value[0] === Type::T_STRING && ! empty($value[1])) { + return $value; + } - return $value; + return [Type::T_STRING, '"', [$value]]; } - protected static $libPercentage = ['number']; + protected static $libPercentage = ['value']; protected function libPercentage($args) { - $num = $this->assertNumber($args[0], 'number'); - $num->assertNoUnits('number'); - - return new Number($num->getDimension() * 100, '%'); + return new Node\Number($this->coercePercent($args[0]) * 100, '%'); } - protected static $libRound = ['number']; + protected static $libRound = ['value']; protected function libRound($args) { - $num = $this->assertNumber($args[0], 'number'); + $num = $args[0]; - return new Number(round($num->getDimension()), $num->getNumeratorUnits(), $num->getDenominatorUnits()); + return new Node\Number(round($num[1]), $num[2]); } - protected static $libFloor = ['number']; + protected static $libFloor = ['value']; protected function libFloor($args) { - $num = $this->assertNumber($args[0], 'number'); + $num = $args[0]; - return new Number(floor($num->getDimension()), $num->getNumeratorUnits(), $num->getDenominatorUnits()); + return new Node\Number(floor($num[1]), $num[2]); } - protected static $libCeil = ['number']; + protected static $libCeil = ['value']; protected function libCeil($args) { - $num = $this->assertNumber($args[0], 'number'); + $num = $args[0]; - return new Number(ceil($num->getDimension()), $num->getNumeratorUnits(), $num->getDenominatorUnits()); + return new Node\Number(ceil($num[1]), $num[2]); } - protected static $libAbs = ['number']; + protected static $libAbs = ['value']; protected function libAbs($args) { - $num = $this->assertNumber($args[0], 'number'); + $num = $args[0]; - return new Number(abs($num->getDimension()), $num->getNumeratorUnits(), $num->getDenominatorUnits()); + return new Node\Number(abs($num[1]), $num[2]); } - protected static $libMin = ['numbers...']; protected function libMin($args) { - /** - * @var Number|null - */ + $numbers = $this->getNormalizedNumbers($args); $min = null; - foreach ($args[0][2] as $arg) { - $number = $this->assertNumber($arg); - - if (\is_null($min) || $min->greaterThan($number)) { - $min = $number; + foreach ($numbers as $key => $number) { + if (is_null($min) || $number[1] <= $min[1]) { + $min = [$key, $number[1]]; } } - if (!\is_null($min)) { - return $min; - } - - throw $this->error('At least one argument must be passed.'); + return $args[$min[0]]; } - protected static $libMax = ['numbers...']; protected function libMax($args) { - /** - * @var Number|null - */ + $numbers = $this->getNormalizedNumbers($args); $max = null; - foreach ($args[0][2] as $arg) { - $number = $this->assertNumber($arg); - - if (\is_null($max) || $max->lessThan($number)) { - $max = $number; + foreach ($numbers as $key => $number) { + if (is_null($max) || $number[1] >= $max[1]) { + $max = [$key, $number[1]]; } } - if (!\is_null($max)) { - return $max; + return $args[$max[0]]; + } + + /** + * Helper to normalize args containing numbers + * + * @param array $args + * + * @return array + */ + protected function getNormalizedNumbers($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; + } + + $number = $item->normalize(); + + if (is_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; + } + + $numbers[$key] = $number; } - throw $this->error('At least one argument must be passed.'); + return $numbers; } protected static $libLength = ['list']; protected function libLength($args) { - $list = $this->coerceList($args[0], ',', true); + $list = $this->coerceList($args[0]); - return new Number(\count($list[2]), ''); + return count($list[2]); } - protected static $libListSeparator = ['list']; + //protected static $libListSeparator = ['list...']; protected function libListSeparator($args) { - if (! \in_array($args[0][0], [Type::T_LIST, Type::T_MAP])) { - return [Type::T_KEYWORD, 'space']; + if (count($args) > 1) { + return 'comma'; } $list = $this->coerceList($args[0]); - if ($list[1] === '' && \count($list[2]) <= 1 && empty($list['enclosing'])) { - return [Type::T_KEYWORD, 'space']; + if (count($list[2]) <= 1) { + return 'space'; } if ($list[1] === ',') { - return [Type::T_KEYWORD, 'comma']; + return 'comma'; } - if ($list[1] === '/') { - return [Type::T_KEYWORD, 'slash']; - } - - return [Type::T_KEYWORD, 'space']; + return 'space'; } protected static $libNth = ['list', 'n']; protected function libNth($args) { - $list = $this->coerceList($args[0], ',', false); - $n = $this->assertNumber($args[1])->getDimension(); + $list = $this->coerceList($args[0]); + $n = $this->assertNumber($args[1]); 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; @@ -8784,16 +6444,18 @@ will be an error in future versions of Sass.\n on line $line of $fname"; protected function libSetNth($args) { $list = $this->coerceList($args[0]); - $n = $this->assertNumber($args[1])->getDimension(); + $n = $this->assertNumber($args[1]); if ($n > 0) { $n--; } elseif ($n < 0) { - $n += \count($list[2]); + $n += count($list[2]); } if (! isset($list[2][$n])) { - throw $this->error('Invalid argument for "n"'); + $this->throwError('Invalid argument for "n"'); + + return null; } $list[2][$n] = $args[2]; @@ -8801,78 +6463,29 @@ will be an error in future versions of Sass.\n on line $line of $fname"; return $list; } - protected static $libMapGet = ['map', 'key', 'keys...']; + protected static $libMapGet = ['map', 'key']; protected function libMapGet($args) { - $map = $this->assertMap($args[0], 'map'); - if (!isset($args[2])) { - // BC layer for usages of the function from PHP code rather than from the Sass function - $args[2] = self::$emptyArgumentList; - } - $keys = array_merge([$args[1]], $args[2][2]); - $value = static::$null; + $map = $this->assertMap($args[0]); + $key = $args[1]; - foreach ($keys as $key) { - if (!\is_array($map) || $map[0] !== Type::T_MAP) { - return static::$null; - } + if (! is_null($key)) { + $key = $this->compileStringContent($this->coerceString($key)); - $map = $this->mapGet($map, $key); - - if ($map === null) { - return static::$null; - } - - $value = $map; - } - - return $value; - } - - /** - * Gets the value corresponding to that key in the map - * - * @param array $map - * @param Number|array $key - * - * @return Number|array|null - */ - private function mapGet(array $map, $key) - { - $index = $this->mapGetEntryIndex($map, $key); - - if ($index !== null) { - return $map[2][$index]; - } - - return null; - } - - /** - * Gets the index corresponding to that key in the map entries - * - * @param array $map - * @param Number|array $key - * - * @return int|null - */ - private function mapGetEntryIndex(array $map, $key) - { - $key = $this->compileStringContent($this->coerceString($key)); - - for ($i = \count($map[1]) - 1; $i >= 0; $i--) { - if ($key === $this->compileStringContent($this->coerceString($map[1][$i]))) { - return $i; + for ($i = count($map[1]) - 1; $i >= 0; $i--) { + if ($key === $this->compileStringContent($this->coerceString($map[1][$i]))) { + return $map[2][$i]; + } } } - return null; + return static::$null; } protected static $libMapKeys = ['map']; protected function libMapKeys($args) { - $map = $this->assertMap($args[0], 'map'); + $map = $this->assertMap($args[0]); $keys = $map[1]; return [Type::T_LIST, ',', $keys]; @@ -8881,33 +6494,20 @@ will be an error in future versions of Sass.\n on line $line of $fname"; protected static $libMapValues = ['map']; protected function libMapValues($args) { - $map = $this->assertMap($args[0], 'map'); + $map = $this->assertMap($args[0]); $values = $map[2]; return [Type::T_LIST, ',', $values]; } - protected static $libMapRemove = [ - ['map'], - ['map', 'key', 'keys...'], - ]; + protected static $libMapRemove = ['map', 'key']; protected function libMapRemove($args) { - $map = $this->assertMap($args[0], 'map'); + $map = $this->assertMap($args[0]); + $key = $this->compileStringContent($this->coerceString($args[1])); - if (\count($args) === 1) { - return $map; - } - - $keys = []; - $keys[] = $this->compileStringContent($this->coerceString($args[1])); - - foreach ($args[2][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)) { + for ($i = count($map[1]) - 1; $i >= 0; $i--) { + if ($key === $this->compileStringContent($this->coerceString($map[1][$i]))) { array_splice($map[1], $i, 1); array_splice($map[2], $i, 1); } @@ -8916,40 +6516,13 @@ will be an error in future versions of Sass.\n on line $line of $fname"; return $map; } - protected static $libMapHasKey = ['map', 'key', 'keys...']; + protected static $libMapHasKey = ['map', 'key']; protected function libMapHasKey($args) { - $map = $this->assertMap($args[0], 'map'); - if (!isset($args[2])) { - // BC layer for usages of the function from PHP code rather than from the Sass function - $args[2] = self::$emptyArgumentList; - } - $keys = array_merge([$args[1]], $args[2][2]); - $lastKey = array_pop($keys); + $map = $this->assertMap($args[0]); + $key = $this->compileStringContent($this->coerceString($args[1])); - foreach ($keys as $key) { - $value = $this->mapGet($map, $key); - - if ($value === null || $value instanceof Number || $value[0] !== Type::T_MAP) { - return self::$false; - } - - $map = $value; - } - - return $this->toBool($this->mapHasKey($map, $lastKey)); - } - - /** - * @param array|Number $keyValue - * - * @return bool - */ - private function mapHasKey(array $map, $keyValue) - { - $key = $this->compileStringContent($this->coerceString($keyValue)); - - for ($i = \count($map[1]) - 1; $i >= 0; $i--) { + for ($i = count($map[1]) - 1; $i >= 0; $i--) { if ($key === $this->compileStringContent($this->coerceString($map[1][$i]))) { return true; } @@ -8958,129 +6531,23 @@ will be an error in future versions of Sass.\n on line $line of $fname"; return false; } - protected static $libMapMerge = [ - ['map1', 'map2'], - ['map-1', 'map-2'], - ['map1', 'args...'] - ]; + protected static $libMapMerge = ['map-1', 'map-2']; protected function libMapMerge($args) { - $map1 = $this->assertMap($args[0], 'map1'); - $map2 = $args[1]; - $keys = []; - if ($map2[0] === Type::T_LIST && isset($map2[3]) && \is_array($map2[3])) { - // This is an argument list for the variadic signature - if (\count($map2[2]) === 0) { - throw new SassScriptException('Expected $args to contain a key.'); - } - if (\count($map2[2]) === 1) { - throw new SassScriptException('Expected $args to contain a value.'); - } - $keys = $map2[2]; - $map2 = array_pop($keys); - } - $map2 = $this->assertMap($map2, 'map2'); + $map1 = $this->assertMap($args[0]); + $map2 = $this->assertMap($args[1]); - return $this->modifyMap($map1, $keys, function ($oldValue) use ($map2) { - $nestedMap = $this->tryMap($oldValue); - - if ($nestedMap === null) { - return $map2; - } - - return $this->mergeMaps($nestedMap, $map2); - }); - } - - /** - * @param array $map - * @param array $keys - * @param callable $modify - * @param bool $addNesting - * - * @return Number|array - * - * @phpstan-param array $keys - * @phpstan-param callable(Number|array): (Number|array) $modify - */ - private function modifyMap(array $map, array $keys, callable $modify, $addNesting = true) - { - if ($keys === []) { - return $modify($map); - } - - return $this->modifyNestedMap($map, $keys, $modify, $addNesting); - } - - /** - * @param array $map - * @param array $keys - * @param callable $modify - * @param bool $addNesting - * - * @return array - * - * @phpstan-param non-empty-array $keys - * @phpstan-param callable(Number|array): (Number|array) $modify - */ - private function modifyNestedMap(array $map, array $keys, callable $modify, $addNesting) - { - $key = array_shift($keys); - - $nestedValueIndex = $this->mapGetEntryIndex($map, $key); - - if ($keys === []) { - if ($nestedValueIndex !== null) { - $map[2][$nestedValueIndex] = $modify($map[2][$nestedValueIndex]); - } else { - $map[1][] = $key; - $map[2][] = $modify(self::$null); - } - - return $map; - } - - $nestedMap = $nestedValueIndex !== null ? $this->tryMap($map[2][$nestedValueIndex]) : null; - - if ($nestedMap === null && !$addNesting) { - return $map; - } - - if ($nestedMap === null) { - $nestedMap = self::$emptyMap; - } - - $newNestedMap = $this->modifyNestedMap($nestedMap, $keys, $modify, $addNesting); - - if ($nestedValueIndex !== null) { - $map[2][$nestedValueIndex] = $newNestedMap; - } else { - $map[1][] = $key; - $map[2][] = $newNestedMap; - } - - return $map; - } - - /** - * Merges 2 Sass maps together - * - * @param array $map1 - * @param array $map2 - * - * @return array - */ - private function mergeMaps(array $map1, array $map2) - { foreach ($map2[1] as $i2 => $key2) { - $map1EntryIndex = $this->mapGetEntryIndex($map1, $key2); + $key = $this->compileStringContent($this->coerceString($key2)); - if ($map1EntryIndex !== null) { - $map1[2][$map1EntryIndex] = $map2[2][$i2]; - continue; + foreach ($map1[1] as $i1 => $key1) { + if ($key === $this->compileStringContent($this->coerceString($key1))) { + $map1[2][$i1] = $map2[2][$i2]; + continue 2; + } } - $map1[1][] = $key2; + $map1[1][] = $map2[1][$i2]; $map1[2][] = $map2[2][$i2]; } @@ -9090,18 +6557,12 @@ will be an error in future versions of Sass.\n on line $line of $fname"; protected static $libKeywords = ['args']; protected function libKeywords($args) { - $value = $args[0]; - - if ($value[0] !== Type::T_LIST || !isset($value[3]) || !\is_array($value[3])) { - $compiledValue = $this->compileValue($value); - - throw SassScriptException::forArgument($compiledValue . ' is not an argument list.', 'args'); - } + $this->assertList($args[0]); $keys = []; $values = []; - foreach ($this->getArgumentListKeywords($value) as $name => $arg) { + foreach ($args[0][2] as $name => $arg) { $keys[] = [Type::T_KEYWORD, $name]; $values[] = $arg; } @@ -9114,27 +6575,15 @@ will be an error in future versions of Sass.\n on line $line of $fname"; { $list = $args[0]; $this->coerceList($list, ' '); - if (! empty($list['enclosing']) && $list['enclosing'] === 'bracket') { - return self::$true; + return true; } - - return self::$false; + return false; } - /** - * @param array $list1 - * @param array|Number|null $sep - * - * @return string - * @throws CompilerException - * - * @deprecated - */ + protected function listSeparatorForJoin($list1, $sep) { - @trigger_error(sprintf('The "%s" method is deprecated.', __METHOD__), E_USER_DEPRECATED); - if (! isset($sep)) { return $list1[1]; } @@ -9151,40 +6600,14 @@ will be an error in future versions of Sass.\n on line $line of $fname"; } } - protected static $libJoin = ['list1', 'list2', 'separator:auto', 'bracketed:auto']; + protected static $libJoin = ['list1', 'list2', 'separator:null', 'bracketed:auto']; protected function libJoin($args) { list($list1, $list2, $sep, $bracketed) = $args; - $list1 = $this->coerceList($list1, ' ', true); - $list2 = $this->coerceList($list2, ' ', true); - - switch ($this->compileStringContent($this->assertString($sep, 'separator'))) { - case 'comma': - $separator = ','; - break; - - case 'space': - $separator = ' '; - break; - - case 'slash': - $separator = '/'; - break; - - case 'auto': - if ($list1[1] !== '' || count($list1[2]) > 1 || !empty($list1['enclosing']) && $list1['enclosing'] !== 'parent') { - $separator = $list1[1] ?: ' '; - } elseif ($list2[1] !== '' || count($list2[2]) > 1 || !empty($list2['enclosing']) && $list2['enclosing'] !== 'parent') { - $separator = $list2[1] ?: ' '; - } else { - $separator = ' '; - } - break; - - default: - throw SassScriptException::forArgument('Must be "space", "comma", "slash", or "auto".', 'separator'); - } + $list1 = $this->coerceList($list1, ' '); + $list2 = $this->coerceList($list2, ' '); + $sep = $this->listSeparatorForJoin($list1, $sep); if ($bracketed === static::$true) { $bracketed = true; @@ -9197,7 +6620,6 @@ will be an error in future versions of Sass.\n on line $line of $fname"; } else { $bracketed = $this->compileValue($bracketed); $bracketed = ! ! $bracketed; - if ($bracketed === true) { $bracketed = true; } @@ -9205,91 +6627,60 @@ will be an error in future versions of Sass.\n on line $line of $fname"; if ($bracketed === 'auto') { $bracketed = false; - if (! empty($list1['enclosing']) && $list1['enclosing'] === 'bracket') { $bracketed = true; } } - $res = [Type::T_LIST, $separator, array_merge($list1[2], $list2[2])]; - + $res = [Type::T_LIST, $sep, array_merge($list1[2], $list2[2])]; + if (isset($list1['enclosing'])) { + $res['enlcosing'] = $list1['enclosing']; + } if ($bracketed) { $res['enclosing'] = 'bracket'; } - return $res; } - protected static $libAppend = ['list', 'val', 'separator:auto']; + protected static $libAppend = ['list', 'val', 'separator:null']; protected function libAppend($args) { list($list1, $value, $sep) = $args; - $list1 = $this->coerceList($list1, ' ', true); - - switch ($this->compileStringContent($this->assertString($sep, 'separator'))) { - case 'comma': - $separator = ','; - break; - - case 'space': - $separator = ' '; - break; - - case 'slash': - $separator = '/'; - break; - - case 'auto': - $separator = $list1[1] === '' && \count($list1[2]) <= 1 && (empty($list1['enclosing']) || $list1['enclosing'] === 'parent') ? ' ' : $list1[1]; - break; - - default: - throw SassScriptException::forArgument('Must be "space", "comma", "slash", or "auto".', 'separator'); - } - - $res = [Type::T_LIST, $separator, array_merge($list1[2], [$value])]; + $list1 = $this->coerceList($list1, ' '); + $sep = $this->listSeparatorForJoin($list1, $sep); + $res = [Type::T_LIST, $sep, array_merge($list1[2], [$value])]; if (isset($list1['enclosing'])) { $res['enclosing'] = $list1['enclosing']; } - return $res; } - protected static $libZip = ['lists...']; protected function libZip($args) { - $argLists = []; - foreach ($args[0][2] as $arg) { - $argLists[] = $this->coerceList($arg); + foreach ($args as $key => $arg) { + $args[$key] = $this->coerceList($arg); } $lists = []; - $firstList = array_shift($argLists); + $firstList = array_shift($args); - $result = [Type::T_LIST, ',', $lists]; - if (! \is_null($firstList)) { - foreach ($firstList[2] as $key => $item) { - $list = [Type::T_LIST, ' ', [$item]]; + foreach ($firstList[2] as $key => $item) { + $list = [Type::T_LIST, '', [$item]]; - foreach ($argLists 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; } - $result[2] = $lists; - } else { - $result['enclosing'] = 'parent'; + $lists[] = $list; } - return $result; + return [Type::T_LIST, ',', $lists]; } protected static $libTypeOf = ['value']; @@ -9297,16 +6688,6 @@ will be an error in future versions of Sass.\n on line $line of $fname"; { $value = $args[0]; - return [Type::T_KEYWORD, $this->getTypeOf($value)]; - } - - /** - * @param array|Number $value - * - * @return string - */ - private function getTypeOf($value) - { switch ($value[0]) { case Type::T_KEYWORD: if ($value === static::$true || $value === static::$false) { @@ -9321,11 +6702,8 @@ will be an error in future versions of Sass.\n on line $line of $fname"; case Type::T_FUNCTION: return 'string'; - case Type::T_FUNCTION_REFERENCE: - return 'function'; - case Type::T_LIST: - if (isset($value[3]) && \is_array($value[3])) { + if (isset($value[3]) && $value[3]) { return 'arglist'; } @@ -9338,77 +6716,68 @@ will be an error in future versions of Sass.\n on line $line of $fname"; protected static $libUnit = ['number']; protected function libUnit($args) { - $num = $this->assertNumber($args[0], 'number'); + $num = $args[0]; - return [Type::T_STRING, '"', [$num->unitStr()]]; + if ($num[0] === Type::T_NUMBER) { + return [Type::T_STRING, '"', [$num->unitStr()]]; + } + + return ''; } protected static $libUnitless = ['number']; protected function libUnitless($args) { - $value = $this->assertNumber($args[0], 'number'); + $value = $args[0]; - return $this->toBool($value->unitless()); + return $value[0] === Type::T_NUMBER && $value->unitless(); } - protected static $libComparable = [ - ['number1', 'number2'], - ['number-1', 'number-2'] - ]; + protected static $libComparable = ['number-1', 'number-2']; protected function libComparable($args) { list($number1, $number2) = $args; - if ( - ! $number1 instanceof Number || - ! $number2 instanceof Number + if (! isset($number1[0]) || $number1[0] !== Type::T_NUMBER || + ! isset($number2[0]) || $number2[0] !== Type::T_NUMBER ) { - throw $this->error('Invalid argument(s) for "comparable"'); + $this->throwError('Invalid argument(s) for "comparable"'); + + return null; } - return $this->toBool($number1->isComparableTo($number2)); + $number1 = $number1->normalize(); + $number2 = $number2->normalize(); + + return $number1[2] === $number2[2] || $number1->unitless() || $number2->unitless(); } protected static $libStrIndex = ['string', 'substring']; protected function libStrIndex($args) { - $string = $this->assertString($args[0], 'string'); + $string = $this->coerceString($args[0]); $stringContent = $this->compileStringContent($string); - $substring = $this->assertString($args[1], 'substring'); + $substring = $this->coerceString($args[1]); $substringContent = $this->compileStringContent($substring); - if (! \strlen($substringContent)) { - $result = 0; - } else { - $result = Util::mbStrpos($stringContent, $substringContent); - } + $result = strpos($stringContent, $substringContent); - return $result === false ? static::$null : new Number($result + 1, ''); + return $result === false ? static::$null : new Node\Number($result + 1, ''); } protected static $libStrInsert = ['string', 'insert', 'index']; protected function libStrInsert($args) { - $string = $this->assertString($args[0], 'string'); + $string = $this->coerceString($args[0]); $stringContent = $this->compileStringContent($string); - $insert = $this->assertString($args[1], 'insert'); + $insert = $this->coerceString($args[1]); $insertContent = $this->compileStringContent($insert); - $index = $this->assertInteger($args[2], 'index'); - if ($index > 0) { - $index = $index - 1; - } - if ($index < 0) { - $index = max(Util::mbStrlen($stringContent) + 1 + $index, 0); - } + list(, $index) = $args[2]; - $string[2] = [ - Util::mbSubstr($stringContent, 0, $index), - $insertContent, - Util::mbSubstr($stringContent, $index) - ]; + $string[2] = [substr_replace($stringContent, $insertContent, $index - 1, 0)]; return $string; } @@ -9416,46 +6785,34 @@ will be an error in future versions of Sass.\n on line $line of $fname"; protected static $libStrLength = ['string']; protected function libStrLength($args) { - $string = $this->assertString($args[0], 'string'); + $string = $this->coerceString($args[0]); $stringContent = $this->compileStringContent($string); - return new Number(Util::mbStrlen($stringContent), ''); + return new Node\Number(strlen($stringContent), ''); } protected static $libStrSlice = ['string', 'start-at', 'end-at:-1']; protected function libStrSlice($args) { - $string = $this->assertString($args[0], 'string'); + if (isset($args[2]) && ! $args[2][1]) { + return static::$nullString; + } + + $string = $this->coerceString($args[0]); $stringContent = $this->compileStringContent($string); - $start = $this->assertNumber($args[1], 'start-at'); - $start->assertNoUnits('start-at'); - $startInt = $this->assertInteger($start, 'start-at'); - $end = $this->assertNumber($args[2], 'end-at'); - $end->assertNoUnits('end-at'); - $endInt = $this->assertInteger($end, 'end-at'); + $start = (int) $args[1][1]; - if ($endInt === 0) { - return [Type::T_STRING, $string[1], []]; + if ($start > 0) { + $start--; } - if ($startInt > 0) { - $startInt--; - } + $end = isset($args[2]) ? (int) $args[2][1] : -1; + $length = $end < 0 ? $end + 1 : ($end > 0 ? $end - $start : $end); - if ($endInt < 0) { - $endInt = Util::mbStrlen($stringContent) + $endInt; - } else { - $endInt--; - } - - if ($endInt < $startInt) { - return [Type::T_STRING, $string[1], []]; - } - - $length = $endInt - $startInt + 1; // The end of the slice is inclusive - - $string[2] = [Util::mbSubstr($stringContent, $startInt, $length)]; + $string[2] = $length + ? [substr($stringContent, $start, $length)] + : [substr($stringContent, $start)]; return $string; } @@ -9463,10 +6820,10 @@ will be an error in future versions of Sass.\n on line $line of $fname"; protected static $libToLowerCase = ['string']; protected function libToLowerCase($args) { - $string = $this->assertString($args[0], 'string'); + $string = $this->coerceString($args[0]); $stringContent = $this->compileStringContent($string); - $string[2] = [$this->stringTransformAsciiOnly($stringContent, 'strtolower')]; + $string[2] = [function_exists('mb_strtolower') ? mb_strtolower($stringContent) : strtolower($stringContent)]; return $string; } @@ -9474,103 +6831,75 @@ will be an error in future versions of Sass.\n on line $line of $fname"; protected static $libToUpperCase = ['string']; protected function libToUpperCase($args) { - $string = $this->assertString($args[0], 'string'); + $string = $this->coerceString($args[0]); $stringContent = $this->compileStringContent($string); - $string[2] = [$this->stringTransformAsciiOnly($stringContent, 'strtoupper')]; + $string[2] = [function_exists('mb_strtoupper') ? mb_strtoupper($stringContent) : strtoupper($stringContent)]; 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) { - $string = $this->assertString($args[0], 'feature'); + $string = $this->coerceString($args[0]); $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 ); } protected static $libFunctionExists = ['name']; protected function libFunctionExists($args) { - $string = $this->assertString($args[0], 'name'); + $string = $this->coerceString($args[0]); $name = $this->compileStringContent($string); // user defined functions if ($this->has(static::$namespaces['function'] . $name)) { - return self::$true; + return true; } $name = $this->normalizeName($name); if (isset($this->userFunctions[$name])) { - return self::$true; + return true; } // built-in functions $f = $this->getBuiltinFunction($name); - return $this->toBool(\is_callable($f)); + return $this->toBool(is_callable($f)); } protected static $libGlobalVariableExists = ['name']; protected function libGlobalVariableExists($args) { - $string = $this->assertString($args[0], 'name'); + $string = $this->coerceString($args[0]); $name = $this->compileStringContent($string); - return $this->toBool($this->has($name, $this->rootEnv)); + return $this->has($name, $this->rootEnv); } protected static $libMixinExists = ['name']; protected function libMixinExists($args) { - $string = $this->assertString($args[0], 'name'); + $string = $this->coerceString($args[0]); $name = $this->compileStringContent($string); - return $this->toBool($this->has(static::$namespaces['mixin'] . $name)); + return $this->has(static::$namespaces['mixin'] . $name); } protected static $libVariableExists = ['name']; protected function libVariableExists($args) { - $string = $this->assertString($args[0], 'name'); + $string = $this->coerceString($args[0]); $name = $this->compileStringContent($string); - return $this->toBool($this->has($name)); + return $this->has($name); } - protected static $libCounter = ['args...']; /** * Workaround IE7's content counter bug. * @@ -9580,37 +6909,35 @@ will be an error in future versions of Sass.\n on line $line of $fname"; */ protected function libCounter($args) { - $list = array_map([$this, 'compileValue'], $args[0][2]); + $list = array_map([$this, 'compileValue'], $args); return [Type::T_STRING, '', ['counter(' . implode(',', $list) . ')']]; } - protected static $libRandom = ['limit:null']; + protected static $libRandom = ['limit']; protected function libRandom($args) { - if (isset($args[0]) && $args[0] !== static::$null) { - $n = $this->assertInteger($args[0], 'limit'); + if (isset($args[0])) { + $n = $this->assertNumber($args[0]); if ($n < 1) { - throw new SassScriptException("\$limit: Must be greater than 0, was $n."); + $this->throwError("limit must be greater than or equal to 1"); + + return null; } - return new Number(mt_rand(1, $n), ''); + return new Node\Number(mt_rand(1, $n), ''); } - $max = mt_getrandmax(); - return new Number(mt_rand(0, $max - 1) / $max, ''); + return new Node\Number(mt_rand(1, mt_getrandmax()), ''); } - protected static $libUniqueId = []; protected function libUniqueId() { static $id; if (! isset($id)) { - $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, pow(36, 8)); } $id += mt_rand(0, 10) + 1; @@ -9618,47 +6945,29 @@ will be an error in future versions of Sass.\n on line $line of $fname"; return [Type::T_STRING, '', ['u' . str_pad(base_convert($id, 10, 36), 8, '0', STR_PAD_LEFT)]]; } - /** - * @param array|Number $value - * @param bool $force_enclosing_display - * - * @return array - */ protected function inspectFormatValue($value, $force_enclosing_display = false) { if ($value === static::$null) { $value = [Type::T_KEYWORD, 'null']; } - $stringValue = [$value]; - - if ($value instanceof Number) { - return [Type::T_STRING, '', $stringValue]; - } - if ($value[0] === Type::T_LIST) { if (end($value[2]) === static::$null) { array_pop($value[2]); $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; - } elseif (! \count($value[2])) { - $value['enclosing'] = 'forced_parent'; + if (! empty($value['enclosing'])) { + if ($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]; } @@ -9669,80 +6978,37 @@ will be an error in future versions of Sass.\n on line $line of $fname"; protected function libInspect($args) { $value = $args[0]; - return $this->inspectFormatValue($value); } /** * Preprocess selector args * - * @param array $arg - * @param string|null $varname - * @param bool $allowParent + * @param array $arg * - * @return array + * @return array|boolean */ - protected function getSelectorArg($arg, $varname = null, $allowParent = false) + protected function getSelectorArg($arg) { static $parser = null; - if (\is_null($parser)) { + if (is_null($parser)) { $parser = $this->parserFactory(__METHOD__); } - if (! $this->checkSelectorArgType($arg)) { - $var_value = $this->compileValue($arg); - throw SassScriptException::forArgument("$var_value is not a valid selector: it must be a string, a list of strings, or a list of lists of strings", $varname); - } - - - if ($arg[0] === Type::T_STRING) { - $arg[1] = ''; - } + $arg = $this->libUnquote([$arg]); $arg = $this->compileValue($arg); $parsedSelector = []; - if ($parser->parseSelector($arg, $parsedSelector, true)) { + if ($parser->parseSelector($arg, $parsedSelector)) { $selector = $this->evalSelectors($parsedSelector); $gluedSelector = $this->glueFunctionSelectors($selector); - if (! $allowParent) { - foreach ($gluedSelector as $selector) { - foreach ($selector as $s) { - if (in_array(static::$selfSelector, $s)) { - throw SassScriptException::forArgument("Parent selectors aren't allowed here.", $varname); - } - } - } - } - return $gluedSelector; } - throw SassScriptException::forArgument("expected more input, invalid selector.", $varname); - } - - /** - * 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; + return false; } /** @@ -9750,11 +7016,11 @@ will be an error in future versions of Sass.\n on line $line of $fname"; * * @param array $selectors * - * @return array + * @return string */ protected function formatOutputSelector($selectors) { - $selectors = $this->collapseSelectorsAsList($selectors); + $selectors = $this->collapseSelectors($selectors, true); return $selectors; } @@ -9764,10 +7030,10 @@ will be an error in future versions of Sass.\n on line $line of $fname"; { list($super, $sub) = $args; - $super = $this->getSelectorArg($super, 'super'); - $sub = $this->getSelectorArg($sub, 'sub'); + $super = $this->getSelectorArg($super); + $sub = $this->getSelectorArg($sub); - return $this->toBool($this->isSuperSelector($super, $sub)); + return $this->isSuperSelector($super, $sub); } /** @@ -9776,35 +7042,17 @@ will be an error in future versions of Sass.\n on line $line of $fname"; * @param array $super * @param array $sub * - * @return bool + * @return boolean */ protected function isSuperSelector($super, $sub) { // one and only one selector for each arg - if (! $super) { - throw $this->error('Invalid super selector for isSuperSelector()'); + if (! $super || count($super) !== 1) { + $this->throwError("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 (count($super) > 1) { - foreach ($super as $s) { - if ($this->isSuperSelector([$s], $sub)) { - return true; - } - } - return false; + if (! $sub || count($sub) !== 1) { + $this->throwError("Invalid sub selector for isSuperSelector()"); } $super = reset($super); @@ -9831,7 +7079,7 @@ will be an error in future versions of Sass.\n on line $line of $fname"; $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; } @@ -9839,7 +7087,7 @@ will be an error in future versions of Sass.\n on line $line of $fname"; $i++; } - if ($i >= \count($sub)) { + if ($i >= count($sub)) { return false; } @@ -9857,18 +7105,18 @@ will be an error in future versions of Sass.\n on line $line of $fname"; * @param array $superParts * @param array $subParts * - * @return bool + * @return boolean */ 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; } @@ -9885,14 +7133,11 @@ will be an error in future versions of Sass.\n on line $line of $fname"; $args = reset($args); $args = $args[2]; - if (\count($args) < 1) { - throw $this->error('selector-append() needs at least 1 argument'); + if (count($args) < 1) { + $this->throwError("selector-append() needs at least 1 argument"); } - $selectors = []; - foreach ($args as $arg) { - $selectors[] = $this->getSelectorArg($arg, 'selector'); - } + $selectors = array_map([$this, 'getSelectorArg'], $args); return $this->formatOutputSelector($this->selectorAppend($selectors)); } @@ -9911,31 +7156,34 @@ will be an error in future versions of Sass.\n on line $line of $fname"; $lastSelectors = array_pop($selectors); if (! $lastSelectors) { - throw $this->error('Invalid selector list in selector-append()'); + $this->throwError("Invalid selector list in selector-append()"); } - while (\count($selectors)) { + while (count($selectors)) { $previousSelectors = array_pop($selectors); if (! $previousSelectors) { - throw $this->error('Invalid selector list in selector-append()'); + $this->throwError("Invalid selector list in selector-append()"); } // do the trick, happening $lastSelector to $previousSelector $appended = []; - foreach ($previousSelectors as $previousSelector) { - foreach ($lastSelectors as $lastSelector) { - $previous = $previousSelector; - foreach ($previousSelector as $j => $previousSelectorParts) { - foreach ($lastSelector as $lastSelectorParts) { - foreach ($lastSelectorParts as $lastSelectorPart) { - $previous[$j][] = $lastSelectorPart; + foreach ($lastSelectors as $lastSelector) { + $previous = $previousSelectors; + + foreach ($lastSelector as $lastSelectorParts) { + foreach ($lastSelectorParts as $lastSelectorPart) { + foreach ($previous as $i => $previousSelector) { + foreach ($previousSelector as $j => $previousSelectorParts) { + $previous[$i][$j][] = $lastSelectorPart; } } } + } - $appended[] = $previous; + foreach ($previous as $ps) { + $appended[] = $ps; } } @@ -9945,20 +7193,17 @@ will be an error in future versions of Sass.\n on line $line of $fname"; return $lastSelectors; } - protected static $libSelectorExtend = [ - ['selector', 'extendee', 'extender'], - ['selectors', 'extendee', 'extender'] - ]; + protected static $libSelectorExtend = ['selectors', 'extendee', 'extender']; protected function libSelectorExtend($args) { list($selectors, $extendee, $extender) = $args; - $selectors = $this->getSelectorArg($selectors, 'selector'); - $extendee = $this->getSelectorArg($extendee, 'extendee'); - $extender = $this->getSelectorArg($extender, 'extender'); + $selectors = $this->getSelectorArg($selectors); + $extendee = $this->getSelectorArg($extendee); + $extender = $this->getSelectorArg($extender); if (! $selectors || ! $extendee || ! $extender) { - throw $this->error('selector-extend() invalid arguments'); + $this->throwError("selector-extend() invalid arguments"); } $extended = $this->extendOrReplaceSelectors($selectors, $extendee, $extender); @@ -9966,20 +7211,17 @@ will be an error in future versions of Sass.\n on line $line of $fname"; return $this->formatOutputSelector($extended); } - protected static $libSelectorReplace = [ - ['selector', 'original', 'replacement'], - ['selectors', 'original', 'replacement'] - ]; + protected static $libSelectorReplace = ['selectors', 'original', 'replacement']; protected function libSelectorReplace($args) { list($selectors, $original, $replacement) = $args; - $selectors = $this->getSelectorArg($selectors, 'selector'); - $original = $this->getSelectorArg($original, 'original'); - $replacement = $this->getSelectorArg($replacement, 'replacement'); + $selectors = $this->getSelectorArg($selectors); + $original = $this->getSelectorArg($original); + $replacement = $this->getSelectorArg($replacement); if (! $selectors || ! $original || ! $replacement) { - throw $this->error('selector-replace() invalid arguments'); + $this->throwError("selector-replace() invalid arguments"); } $replaced = $this->extendOrReplaceSelectors($selectors, $original, $replacement, true); @@ -9991,10 +7233,10 @@ will be an error in future versions of Sass.\n on line $line of $fname"; * Extend/replace in selectors * used by selector-extend and selector-replace that use the same logic * - * @param array $selectors - * @param array $extendee - * @param array $extender - * @param bool $replace + * @param array $selectors + * @param array $extendee + * @param array $extender + * @param boolean $replace * * @return array */ @@ -10007,10 +7249,6 @@ will be an error in future versions of Sass.\n on line $line of $fname"; $this->extendsMap = []; foreach ($extendee as $es) { - if (\count($es) !== 1) { - throw $this->error('Can\'t extend complex selector.'); - } - // only use the first one $this->pushExtends(reset($es), $extender, null); } @@ -10022,12 +7260,12 @@ will be an error in future versions of Sass.\n on line $line of $fname"; $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 && \count($extended) === $n) { + if ($replace and count($extended) === $n) { $extended[] = $selector; } } @@ -10045,17 +7283,11 @@ will be an error in future versions of Sass.\n on line $line of $fname"; $args = reset($args); $args = $args[2]; - if (\count($args) < 1) { - throw $this->error('selector-nest() needs at least 1 argument'); + if (count($args) < 1) { + $this->throwError("selector-nest() needs at least 1 argument"); } - $selectorsMap = []; - foreach ($args as $arg) { - $selectorsMap[] = $this->getSelectorArg($arg, 'selector', true); - } - - assert(!empty($selectorsMap)); - + $selectorsMap = array_map([$this, 'getSelectorArg'], $args); $envs = []; foreach ($selectorsMap as $selectors) { @@ -10072,14 +7304,11 @@ will be an error in future versions of Sass.\n on line $line of $fname"; return $this->formatOutputSelector($outputSelectors); } - protected static $libSelectorParse = [ - ['selector'], - ['selectors'] - ]; + protected static $libSelectorParse = ['selectors']; protected function libSelectorParse($args) { $selectors = reset($args); - $selectors = $this->getSelectorArg($selectors, 'selector'); + $selectors = $this->getSelectorArg($selectors); return $this->formatOutputSelector($selectors); } @@ -10089,11 +7318,11 @@ will be an error in future versions of Sass.\n on line $line of $fname"; { list($selectors1, $selectors2) = $args; - $selectors1 = $this->getSelectorArg($selectors1, 'selectors1'); - $selectors2 = $this->getSelectorArg($selectors2, 'selectors2'); + $selectors1 = $this->getSelectorArg($selectors1); + $selectors2 = $this->getSelectorArg($selectors2); if (! $selectors1 || ! $selectors2) { - throw $this->error('selector-unify() invalid arguments'); + $this->throwError("selector-unify() invalid arguments"); } // only consider the first compound of each @@ -10113,15 +7342,15 @@ will be an error in future versions of Sass.\n on line $line of $fname"; * @param array $compound1 * @param array $compound2 * - * @return array + * @return array|mixed */ protected function unifyCompoundSelectors($compound1, $compound2) { - if (! \count($compound1)) { + if (! count($compound1)) { return $compound2; } - if (! \count($compound2)) { + if (! count($compound2)) { return $compound1; } @@ -10138,7 +7367,7 @@ will be an error in future versions of Sass.\n on line $line of $fname"; $unifiedSelectors = [$unifiedCompound]; // do the rest - while (\count($compound1) || \count($compound2)) { + while (count($compound1) || count($compound2)) { $part1 = end($compound1); $part2 = end($compound2); @@ -10229,7 +7458,7 @@ will be an error in future versions of Sass.\n on line $line of $fname"; * @param array $part * @param array $compound * - * @return array|false + * @return array|boolean */ protected function matchPartInCompound($part, $compound) { @@ -10238,7 +7467,7 @@ will be an error in future versions of Sass.\n on line $line of $fname"; $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)) { @@ -10252,11 +7481,11 @@ will be an error in future versions of Sass.\n on line $line of $fname"; $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]; } } @@ -10324,7 +7553,7 @@ will be an error in future versions of Sass.\n on line $line of $fname"; * @param string $tag1 * @param string $tag2 * - * @return array|false + * @return array|boolean */ protected function checkCompatibleTags($tag1, $tag2) { @@ -10332,12 +7561,12 @@ will be an error in future versions of Sass.\n on line $line of $fname"; $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; } @@ -10347,9 +7576,9 @@ will be an error in future versions of Sass.\n on line $line of $fname"; /** * Find the html tag name in a selector parts list * - * @param string[] $parts + * @param array $parts * - * @return string + * @return mixed|string */ protected function findTagName($parts) { @@ -10366,7 +7595,7 @@ will be an error in future versions of Sass.\n on line $line of $fname"; protected function libSimpleSelectors($args) { $selector = reset($args); - $selector = $this->getSelectorArg($selector, 'selector'); + $selector = $this->getSelectorArg($selector); // remove selectors list layer, keeping the first one $selector = reset($selector); @@ -10382,27 +7611,4 @@ will be an error in future versions of Sass.\n on line $line of $fname"; return [Type::T_LIST, ',', $listParts]; } - - protected static $libScssphpGlob = ['pattern']; - protected function libScssphpGlob($args) - { - @trigger_error(sprintf('The "scssphp-glob" function is deprecated an will be removed in ScssPhp 2.0. Register your own alternative through "%s::registerFunction', __CLASS__), E_USER_DEPRECATED); - - $this->logger->warn('The "scssphp-glob" function is deprecated an will be removed in ScssPhp 2.0.', true); - - $string = $this->assertString($args[0], 'pattern'); - $pattern = $this->compileStringContent($string); - $matches = glob($pattern); - $listParts = []; - - foreach ($matches as $match) { - if (! is_file($match)) { - continue; - } - - $listParts[] = [Type::T_STRING, '"', [$match]]; - } - - return [Type::T_LIST, ',', $listParts]; - } } diff --git a/lib/scssphp/scssphp/src/Compiler/CachedResult.php b/lib/scssphp/scssphp/src/Compiler/CachedResult.php deleted file mode 100644 index a66291996..000000000 --- a/lib/scssphp/scssphp/src/Compiler/CachedResult.php +++ /dev/null @@ -1,77 +0,0 @@ - - */ - private $parsedFiles; - - /** - * @var array - * @phpstan-var list - */ - private $resolvedImports; - - /** - * @param CompilationResult $result - * @param array $parsedFiles - * @param array $resolvedImports - * - * @phpstan-param list $resolvedImports - */ - public function __construct(CompilationResult $result, array $parsedFiles, array $resolvedImports) - { - $this->result = $result; - $this->parsedFiles = $parsedFiles; - $this->resolvedImports = $resolvedImports; - } - - /** - * @return CompilationResult - */ - public function getResult() - { - return $this->result; - } - - /** - * @return array - */ - public function getParsedFiles() - { - return $this->parsedFiles; - } - - /** - * @return array - * - * @phpstan-return list - */ - public function getResolvedImports() - { - return $this->resolvedImports; - } -} diff --git a/lib/scssphp/scssphp/src/Compiler/Environment.php b/lib/scssphp/scssphp/src/Compiler/Environment.php index b205a077f..03eb86a5d 100644 --- a/lib/scssphp/scssphp/src/Compiler/Environment.php +++ b/lib/scssphp/scssphp/src/Compiler/Environment.php @@ -1,9 +1,8 @@ - * - * @internal */ class Environment { /** - * @var \ScssPhp\ScssPhp\Block|null + * @var \ScssPhp\ScssPhp\Block */ public $block; /** - * @var \ScssPhp\ScssPhp\Compiler\Environment|null + * @var \ScssPhp\ScssPhp\Compiler\Environment */ public $parent; - /** - * @var Environment|null - */ - public $declarationScopeParent; - - /** - * @var Environment|null - */ - public $parentStore; - - /** - * @var array|null - */ - public $selectors; - - /** - * @var string|null - */ - public $marker; - /** * @var array */ @@ -62,7 +39,7 @@ class Environment public $storeUnreduced; /** - * @var int + * @var integer */ public $depth; } diff --git a/lib/scssphp/scssphp/src/Exception/CompilerException.php b/lib/scssphp/scssphp/src/Exception/CompilerException.php index 0b00cf525..a9d134fca 100644 --- a/lib/scssphp/scssphp/src/Exception/CompilerException.php +++ b/lib/scssphp/scssphp/src/Exception/CompilerException.php @@ -1,9 +1,8 @@ - * - * @internal */ -class CompilerException extends \Exception implements SassException +class CompilerException extends \Exception { } diff --git a/lib/scssphp/scssphp/src/Exception/ParserException.php b/lib/scssphp/scssphp/src/Exception/ParserException.php index f0726698f..2fa12dd7a 100644 --- a/lib/scssphp/scssphp/src/Exception/ParserException.php +++ b/lib/scssphp/scssphp/src/Exception/ParserException.php @@ -1,9 +1,8 @@ - * - * @internal */ -class ParserException extends \Exception implements SassException +class ParserException extends \Exception { - /** - * @var array|null - * @phpstan-var array{string, int, int}|null - */ - private $sourcePosition; - - /** - * Get source position - * - * @api - * - * @return array|null - * @phpstan-return array{string, int, int}|null - */ - public function getSourcePosition() - { - return $this->sourcePosition; - } - - /** - * Set source position - * - * @api - * - * @param array $sourcePosition - * - * @return void - * - * @phpstan-param array{string, int, int} $sourcePosition - */ - public function setSourcePosition($sourcePosition) - { - $this->sourcePosition = $sourcePosition; - } } diff --git a/lib/scssphp/scssphp/src/Exception/RangeException.php b/lib/scssphp/scssphp/src/Exception/RangeException.php index 4be4dee70..ee36c97e1 100644 --- a/lib/scssphp/scssphp/src/Exception/RangeException.php +++ b/lib/scssphp/scssphp/src/Exception/RangeException.php @@ -1,9 +1,8 @@ - * - * @internal */ -class RangeException extends \Exception implements SassException +class RangeException extends \Exception { } diff --git a/lib/scssphp/scssphp/src/Exception/SassException.php b/lib/scssphp/scssphp/src/Exception/SassException.php deleted file mode 100644 index 9f62b3cd2..000000000 --- a/lib/scssphp/scssphp/src/Exception/SassException.php +++ /dev/null @@ -1,7 +0,0 @@ - - * - * @deprecated The Scssphp server should define its own exception instead. */ -class ServerException extends \Exception implements SassException +class ServerException extends \Exception { } diff --git a/lib/scssphp/scssphp/src/Formatter.php b/lib/scssphp/scssphp/src/Formatter.php index 6137dc650..e17770458 100644 --- a/lib/scssphp/scssphp/src/Formatter.php +++ b/lib/scssphp/scssphp/src/Formatter.php @@ -1,9 +1,8 @@ - * - * @internal */ abstract class Formatter { /** - * @var int + * @var integer */ public $indentLevel; @@ -60,7 +57,7 @@ abstract class Formatter public $assignSeparator; /** - * @var bool + * @var boolean */ public $keepSemicolons; @@ -70,17 +67,17 @@ abstract class Formatter protected $currentBlock; /** - * @var int + * @var integer */ protected $currentLine; /** - * @var int + * @var integer */ protected $currentColumn; /** - * @var \ScssPhp\ScssPhp\SourceMap\SourceMapGenerator|null + * @var \ScssPhp\ScssPhp\SourceMap\SourceMapGenerator */ protected $sourceMapGenerator; @@ -121,33 +118,16 @@ abstract class Formatter return rtrim($name) . $this->assignSeparator . $value . ';'; } - /** - * Return custom property assignment - * differs in that you have to keep spaces in the value as is - * - * @api - * - * @param string $name - * @param mixed $value - * - * @return string - */ - public function customProperty($name, $value) - { - return rtrim($name) . trim($this->assignSeparator) . $value . ';'; - } - /** * Output lines inside a block * * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $block - * - * @return void */ protected function blockLines(OutputBlock $block) { $inner = $this->indentStr(); - $glue = $this->break . $inner; + + $glue = $this->break . $inner; $this->write($inner . implode($glue, $block->lines)); @@ -160,13 +140,9 @@ abstract class Formatter * Output block selectors * * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $block - * - * @return void */ protected function blockSelectors(OutputBlock $block) { - assert(! empty($block->selectors)); - $inner = $this->indentStr(); $this->write($inner @@ -178,8 +154,6 @@ abstract class Formatter * Output block children * * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $block - * - * @return void */ protected function blockChildren(OutputBlock $block) { @@ -192,8 +166,6 @@ abstract class Formatter * Output non-empty block * * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $block - * - * @return void */ protected function block(OutputBlock $block) { @@ -239,7 +211,7 @@ abstract class Formatter * * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $block * - * @return bool + * @return boolean */ protected function testEmptyChildren($block) { @@ -286,18 +258,9 @@ abstract class Formatter ob_start(); - try { - $this->block($block); - } catch (\Exception $e) { - ob_end_clean(); - throw $e; - } catch (\Throwable $e) { - ob_end_clean(); - throw $e; - } + $this->block($block); $out = ob_get_clean(); - assert($out !== false); return $out; } @@ -306,8 +269,6 @@ abstract class Formatter * Output content * * @param string $str - * - * @return void */ protected function write($str) { @@ -321,8 +282,7 @@ abstract class Formatter * 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 && + if (! $this->keepSemicolons && $str && (strpos($str, ';') !== false) && (substr($str, -1) === ';') @@ -333,43 +293,22 @@ abstract class Formatter } 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 + ); + $lines = explode("\n", $str); + $lineCount = count($lines); + $this->currentLine += $lineCount-1; + $lastLine = array_pop($lines); - 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 !== '') { - assert($this->currentBlock->sourceLine !== null); - assert($this->currentBlock->sourceName !== null); - $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 !== '') { - assert($this->currentBlock->sourceLine !== null); - assert($this->currentBlock->sourceName !== null); - $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); + $this->currentColumn = ($lineCount === 1 ? $this->currentColumn : 0) + strlen($lastLine); } echo $str; diff --git a/lib/scssphp/scssphp/src/Formatter/Compact.php b/lib/scssphp/scssphp/src/Formatter/Compact.php index 22f226889..591f0c92e 100644 --- a/lib/scssphp/scssphp/src/Formatter/Compact.php +++ b/lib/scssphp/scssphp/src/Formatter/Compact.php @@ -1,9 +1,8 @@ - * - * @deprecated since 1.4.0. Use the Compressed formatter instead. - * - * @internal */ class Compact extends Formatter { @@ -30,8 +25,6 @@ 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/lib/scssphp/scssphp/src/Formatter/Compressed.php b/lib/scssphp/scssphp/src/Formatter/Compressed.php index 58ebe3f11..ec4722eaf 100644 --- a/lib/scssphp/scssphp/src/Formatter/Compressed.php +++ b/lib/scssphp/scssphp/src/Formatter/Compressed.php @@ -1,9 +1,8 @@ - * - * @internal */ class Compressed extends Formatter { @@ -50,6 +48,8 @@ class Compressed extends Formatter foreach ($block->lines as $index => $line) { if (substr($line, 0, 2) === '/*' && substr($line, 2, 1) !== '!') { unset($block->lines[$index]); + } elseif (substr($line, 0, 3) === '/*!') { + $block->lines[$index] = '/*' . substr($line, 3); } } @@ -67,8 +67,6 @@ class Compressed extends Formatter */ protected function blockSelectors(OutputBlock $block) { - assert(! empty($block->selectors)); - $inner = $this->indentStr(); $this->write( diff --git a/lib/scssphp/scssphp/src/Formatter/Crunched.php b/lib/scssphp/scssphp/src/Formatter/Crunched.php index 2bc1e9299..51ccb516d 100644 --- a/lib/scssphp/scssphp/src/Formatter/Crunched.php +++ b/lib/scssphp/scssphp/src/Formatter/Crunched.php @@ -1,9 +1,8 @@ - * - * @deprecated since 1.4.0. Use the Compressed formatter instead. - * - * @internal */ class Crunched extends Formatter { @@ -30,8 +26,6 @@ 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 = ''; @@ -71,8 +65,6 @@ class Crunched extends Formatter */ protected function blockSelectors(OutputBlock $block) { - assert(! empty($block->selectors)); - $inner = $this->indentStr(); $this->write( diff --git a/lib/scssphp/scssphp/src/Formatter/Debug.php b/lib/scssphp/scssphp/src/Formatter/Debug.php index b3f442253..94e70c815 100644 --- a/lib/scssphp/scssphp/src/Formatter/Debug.php +++ b/lib/scssphp/scssphp/src/Formatter/Debug.php @@ -1,9 +1,8 @@ - * - * @deprecated since 1.4.0. - * - * @internal */ class Debug extends Formatter { @@ -30,8 +26,6 @@ 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/lib/scssphp/scssphp/src/Formatter/Expanded.php b/lib/scssphp/scssphp/src/Formatter/Expanded.php index 6eb4a0cb7..9549c6cfa 100644 --- a/lib/scssphp/scssphp/src/Formatter/Expanded.php +++ b/lib/scssphp/scssphp/src/Formatter/Expanded.php @@ -1,9 +1,8 @@ - * - * @internal */ class Expanded extends Formatter { @@ -57,9 +55,7 @@ class Expanded extends Formatter foreach ($block->lines as $index => $line) { if (substr($line, 0, 2) === '/*') { - $replacedLine = preg_replace('/\r\n?|\n|\f/', $this->break, $line); - assert($replacedLine !== null); - $block->lines[$index] = $replacedLine; + $block->lines[$index] = preg_replace('/[\r\n]+/', $glue, $line); } } diff --git a/lib/scssphp/scssphp/src/Formatter/Nested.php b/lib/scssphp/scssphp/src/Formatter/Nested.php index d5ed85cc2..f9e7f037f 100644 --- a/lib/scssphp/scssphp/src/Formatter/Nested.php +++ b/lib/scssphp/scssphp/src/Formatter/Nested.php @@ -1,9 +1,8 @@ - * - * @deprecated since 1.4.0. Use the Expanded formatter instead. - * - * @internal */ class Nested extends Formatter { /** - * @var int + * @var integer */ private $depth; @@ -36,8 +32,6 @@ 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"; @@ -64,13 +58,12 @@ class Nested extends Formatter 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) === '/*') { - $replacedLine = preg_replace('/\r\n?|\n|\f/', $this->break, $line); - assert($replacedLine !== null); - $block->lines[$index] = $replacedLine; + $block->lines[$index] = preg_replace('/[\r\n]+/', $glue, $line); } } @@ -97,7 +90,7 @@ class Nested extends Formatter $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); @@ -105,8 +98,7 @@ class Nested extends Formatter 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; @@ -127,12 +119,10 @@ class Nested extends Formatter 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; } } @@ -223,7 +213,7 @@ class Nested extends Formatter * * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $block * - * @return bool + * @return boolean */ private function hasFlatChild($block) { diff --git a/lib/scssphp/scssphp/src/Formatter/OutputBlock.php b/lib/scssphp/scssphp/src/Formatter/OutputBlock.php index 2799656a4..3e6fd9289 100644 --- a/lib/scssphp/scssphp/src/Formatter/OutputBlock.php +++ b/lib/scssphp/scssphp/src/Formatter/OutputBlock.php @@ -1,9 +1,8 @@ - * - * @internal */ class OutputBlock { /** - * @var string|null + * @var string */ public $type; /** - * @var int + * @var integer */ public $depth; /** - * @var array|null + * @var array */ public $selectors; /** - * @var string[] + * @var array */ public $lines; /** - * @var OutputBlock[] + * @var array */ public $children; /** - * @var OutputBlock|null + * @var \ScssPhp\ScssPhp\Formatter\OutputBlock */ public $parent; /** - * @var string|null + * @var string */ public $sourceName; /** - * @var int|null + * @var integer */ public $sourceLine; /** - * @var int|null + * @var integer */ public $sourceColumn; } diff --git a/lib/scssphp/scssphp/src/Logger/LoggerInterface.php b/lib/scssphp/scssphp/src/Logger/LoggerInterface.php deleted file mode 100644 index 7c0a2f76e..000000000 --- a/lib/scssphp/scssphp/src/Logger/LoggerInterface.php +++ /dev/null @@ -1,48 +0,0 @@ -stream = $stream; - $this->closeOnDestruct = $closeOnDestruct; - } - - /** - * @internal - */ - public function __destruct() - { - if ($this->closeOnDestruct) { - fclose($this->stream); - } - } - - /** - * @inheritDoc - */ - public function warn($message, $deprecation = false) - { - $prefix = ($deprecation ? 'DEPRECATION ' : '') . 'WARNING: '; - - fwrite($this->stream, $prefix . $message . "\n\n"); - } - - /** - * @inheritDoc - */ - public function debug($message) - { - fwrite($this->stream, $message . "\n"); - } -} diff --git a/lib/scssphp/scssphp/src/Node.php b/lib/scssphp/scssphp/src/Node.php index fcaf8a95f..dab565a63 100644 --- a/lib/scssphp/scssphp/src/Node.php +++ b/lib/scssphp/scssphp/src/Node.php @@ -1,9 +1,8 @@ - * - * @internal */ abstract class Node { @@ -27,17 +24,17 @@ abstract class Node public $type; /** - * @var int + * @var integer */ public $sourceIndex; /** - * @var int|null + * @var integer */ public $sourceLine; /** - * @var int|null + * @var integer */ public $sourceColumn; } diff --git a/lib/scssphp/scssphp/src/Node/Number.php b/lib/scssphp/scssphp/src/Node/Number.php index ca9b5b652..acbabff7f 100644 --- a/lib/scssphp/scssphp/src/Node/Number.php +++ b/lib/scssphp/scssphp/src/Node/Number.php @@ -1,9 +1,8 @@ - * - * @template-implements \ArrayAccess */ class Number extends Node implements \ArrayAccess { - const PRECISION = 10; - /** - * @var int - * @deprecated use {Number::PRECISION} instead to read the precision. Configuring it is not supported anymore. + * @var integer */ - public static $precision = self::PRECISION; + static public $precision = 10; /** * @see http://www.w3.org/TR/2012/WD-css3-values-20120308/ * * @var array - * @phpstan-var array> */ - protected static $unitTable = [ + static protected $unitTable = [ 'in' => [ 'in' => 1, 'pc' => 6, @@ -75,93 +64,91 @@ class Number extends Node implements \ArrayAccess ], 'dpi' => [ 'dpi' => 1, - 'dpcm' => 1 / 2.54, - 'dppx' => 1 / 96, + 'dpcm' => 2.54, + 'dppx' => 96, ], ]; /** - * @var int|float + * @var integer|float */ - private $dimension; + public $dimension; /** - * @var string[] - * @phpstan-var list + * @var array */ - private $numeratorUnits; - - /** - * @var string[] - * @phpstan-var list - */ - private $denominatorUnits; + public $units; /** * Initialize number * - * @param int|float $dimension - * @param string[]|string $numeratorUnits - * @param string[] $denominatorUnits - * - * @phpstan-param list|string $numeratorUnits - * @phpstan-param list $denominatorUnits + * @param mixed $dimension + * @param mixed $initialUnit */ - public function __construct($dimension, $numeratorUnits, array $denominatorUnits = []) + public function __construct($dimension, $initialUnit) { - 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']; + $this->type = Type::T_NUMBER; + $this->dimension = $dimension; + $this->units = is_array($initialUnit) + ? $initialUnit + : ($initialUnit ? [$initialUnit => 1] + : []); + } + + /** + * Coerce number to target units + * + * @param array $units + * + * @return \ScssPhp\ScssPhp\Node\Number + */ + public function coerce($units) + { + if ($this->unitless()) { + return new Number($this->dimension, $units); } - $this->dimension = $dimension; - $this->numeratorUnits = $numeratorUnits; - $this->denominatorUnits = $denominatorUnits; + $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); } /** - * @return float|int + * Normalize number + * + * @return \ScssPhp\ScssPhp\Node\Number */ - public function getDimension() + public function normalize() { - return $this->dimension; + $dimension = $this->dimension; + $units = []; + + $this->normalizeUnits($dimension, $units, 'in'); + + return new Number($dimension, $units); } /** - * @return string[] + * {@inheritdoc} */ - public function getNumeratorUnits() - { - return $this->numeratorUnits; - } - - /** - * @return string[] - */ - public function getDenominatorUnits() - { - return $this->denominatorUnits; - } - - /** - * @return bool - */ - #[\ReturnTypeWillChange] public function offsetExists($offset) { if ($offset === -3) { - return ! \is_null($this->sourceColumn); + return ! is_null($this->sourceColumn); } if ($offset === -2) { - return ! \is_null($this->sourceLine); + return ! is_null($this->sourceLine); } - if ( - $offset === -1 || + if ($offset === -1 || $offset === 0 || $offset === 1 || $offset === 2 @@ -173,9 +160,8 @@ class Number extends Node implements \ArrayAccess } /** - * @return mixed + * {@inheritdoc} */ - #[\ReturnTypeWillChange] public function offsetGet($offset) { switch ($offset) { @@ -189,54 +175,60 @@ class Number extends Node implements \ArrayAccess return $this->sourceIndex; case 0: - return Type::T_NUMBER; + return $this->type; case 1: return $this->dimension; case 2: - return array('numerator_units' => $this->numeratorUnits, 'denominator_units' => $this->denominatorUnits); + return $this->units; } } /** - * @return void + * {@inheritdoc} */ - #[\ReturnTypeWillChange] public function offsetSet($offset, $value) { - throw new \BadMethodCallException('Number is immutable'); + 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; + } } /** - * @return void + * {@inheritdoc} */ - #[\ReturnTypeWillChange] public function offsetUnset($offset) { - throw new \BadMethodCallException('Number is immutable'); + if ($offset === 1) { + $this->dimension = null; + } elseif ($offset === 2) { + $this->units = null; + } elseif ($offset === -1) { + $this->sourceIndex = null; + } elseif ($offset === -2) { + $this->sourceLine = null; + } elseif ($offset === -3) { + $this->sourceColumn = null; + } } /** * Returns true if the number is unitless * - * @return bool + * @return boolean */ public function unitless() { - 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; + return ! array_sum($this->units); } /** @@ -246,289 +238,22 @@ class Number extends Node implements \ArrayAccess */ public function unitStr() { - if ($this->unitless()) { - return ''; - } + $numerators = []; + $denominators = []; - return self::getUnitString($this->numeratorUnits, $this->denominatorUnits); - } - - /** - * @param float|int $min - * @param float|int $max - * @param string|null $name - * - * @return float|int - * @throws SassScriptException - */ - public function valueInRange($min, $max, $name = null) - { - try { - return Util::checkRange('', new Range($min, $max), $this); - } catch (RangeException $e) { - throw SassScriptException::forArgument(sprintf('Expected %s to be within %s%s and %s%3$s', $this, $min, $this->unitStr(), $max), $name); - } - } - - /** - * @param string|null $varName - * - * @return void - */ - public function assertNoUnits($varName = null) - { - if ($this->unitless()) { - return; - } - - throw SassScriptException::forArgument(sprintf('Expected %s to have no units.', $this), $varName); - } - - /** - * @param string $unit - * @param string|null $varName - * - * @return void - */ - public function assertUnit($unit, $varName = null) - { - if ($this->hasUnit($unit)) { - return; - } - - throw SassScriptException::forArgument(sprintf('Expected %s to have unit "%s".', $this, $unit), $varName); - } - - /** - * @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) - )); - } - - /** - * Returns a copy of this number, converted to the units represented by $newNumeratorUnits and $newDenominatorUnits. - * - * This does not throw an error if this number is unitless and - * $newNumeratorUnits/$newDenominatorUnits are not empty, or vice versa. Instead, - * it treats all unitless numbers as convertible to and from all units without - * changing the value. - * - * @param string[] $newNumeratorUnits - * @param string[] $newDenominatorUnits - * - * @return Number - * - * @phpstan-param list $newNumeratorUnits - * @phpstan-param list $newDenominatorUnits - * - * @throws SassScriptException if this number's units are not compatible with $newNumeratorUnits and $newDenominatorUnits - */ - public function coerce(array $newNumeratorUnits, array $newDenominatorUnits) - { - return new Number($this->valueInUnits($newNumeratorUnits, $newDenominatorUnits), $newNumeratorUnits, $newDenominatorUnits); - } - - /** - * @param Number $other - * - * @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; + foreach ($this->units as $unit => $unitSize) { + if ($unitSize > 0) { + $numerators = array_pad($numerators, count($numerators) + $unitSize, $unit); + continue; } - $result = fmod($num1, $num2); - - 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 $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; - } + return implode('*', $numerators) . (count($denominators) ? '/' . implode('*', $denominators) : ''); } /** @@ -540,31 +265,35 @@ class Number extends Node implements \ArrayAccess */ public function output(Compiler $compiler = null) { - $dimension = round($this->dimension, self::PRECISION); + $dimension = round($this->dimension, static::$precision); - if (is_nan($dimension)) { - return 'NaN'; + $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'); + + $dimension = round($dimension, static::$precision); + $units = array_filter($units, function ($unitSize) { + return $unitSize; + }); } - if ($dimension === INF) { - return 'Infinity'; + $unitSize = array_sum($units); + + if ($compiler && ($unitSize > 1 || $unitSize < 0 || count($units) > 1)) { + $compiler->throwError((string) $dimension . $this->unitStr() . " isn't a valid CSS value."); } - if ($dimension === -INF) { - return '-Infinity'; - } + reset($units); + $unit = key($units); + $dimension = number_format($dimension, static::$precision, '.', ''); - if ($compiler) { - $unit = $this->unitStr(); - } elseif (isset($this->numeratorUnits[0])) { - $unit = $this->numeratorUnits[0]; - } else { - $unit = ''; - } - - $dimension = number_format($dimension, self::PRECISION, '.', ''); - - return rtrim(rtrim($dimension, '0'), '.') . $unit; + return (static::$precision ? rtrim(rtrim($dimension, '0'), '.') : $dimension) . $unit; } /** @@ -576,229 +305,26 @@ class Number extends Node implements \ArrayAccess } /** - * @param Number $other - * @param callable $operation + * Normalize units * - * @return Number - * - * @phpstan-param callable(int|float, int|float): (int|float) $operation + * @param integer|float $dimension + * @param array $units + * @param string $baseUnit */ - private function coerceNumber(Number $other, $operation) + private function normalizeUnits(&$dimension, &$units, $baseUnit = 'in') { - $result = $this->coerceUnits($other, $operation); + $dimension = $this->dimension; + $units = []; - if (!$this->unitless()) { - return new Number($result, $this->numeratorUnits, $this->denominatorUnits); - } + foreach ($this->units as $unit => $exp) { + if (isset(static::$unitTable[$baseUnit][$unit])) { + $factor = pow(static::$unitTable[$baseUnit][$unit], $exp); - 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 - * - * @throws SassScriptException if this number's units are not compatible with $numeratorUnits and $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; + $unit = $baseUnit; + $dimension /= $factor; } - throw new SassScriptException(sprintf( - 'Incompatible units %s and %s.', - self::getUnitString($this->numeratorUnits, $this->denominatorUnits), - self::getUnitString($numeratorUnits, $denominatorUnits) - )); + $units[$unit] = $exp + (isset($units[$unit]) ? $units[$unit] : 0); } - - $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]. - * - * Equivalently, `1unit1 * conversionFactor(unit1, unit2) = 1unit2`. - * - * @param string $unit1 - * @param string $unit2 - * - * @return float|int|null - */ - private static function getConversionFactor($unit1, $unit2) - { - if ($unit1 === $unit2) { - return 1; - } - - foreach (static::$unitTable as $unitVariants) { - if (isset($unitVariants[$unit1]) && isset($unitVariants[$unit2])) { - return $unitVariants[$unit1] / $unitVariants[$unit2]; - } - } - - return null; - } - - /** - * 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'; - } - - if (\count($denominators) === 1) { - return $denominators[0] . '^-1'; - } - - return '(' . implode('*', $denominators) . ')^-1'; - } - - return implode('*', $numerators) . (\count($denominators) ? '/' . implode('*', $denominators) : ''); } } diff --git a/lib/scssphp/scssphp/src/OutputStyle.php b/lib/scssphp/scssphp/src/OutputStyle.php deleted file mode 100644 index c284639c1..000000000 --- a/lib/scssphp/scssphp/src/OutputStyle.php +++ /dev/null @@ -1,9 +0,0 @@ - - * - * @internal */ class Parser { @@ -43,7 +30,7 @@ class Parser const SOURCE_COLUMN = -3; /** - * @var array + * @var array */ protected static $precedence = [ '=' => 0, @@ -51,6 +38,7 @@ class Parser 'and' => 2, '==' => 3, '!=' => 3, + '<=>' => 3, '<=' => 4, '>=' => 4, '<' => 4, @@ -62,97 +50,49 @@ 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; - /** - * The current offset in the buffer - * - * @var int - */ + private $charset; private $count; - /** - * @var Block|null - */ 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; - - /** - * @var LoggerInterface - */ - private $logger; - /** * Constructor * * @api * - * @param string|null $sourceName - * @param int $sourceIndex - * @param string|null $encoding - * @param Cache|null $cache - * @param bool $cssOnly - * @param LoggerInterface|null $logger + * @param string $sourceName + * @param integer $sourceIndex + * @param string $encoding + * @param \ScssPhp\ScssPhp\Cache $cache */ - public function __construct($sourceName, $sourceIndex = 0, $encoding = 'utf-8', Cache $cache = null, $cssOnly = false, LoggerInterface $logger = null) + public function __construct($sourceName, $sourceIndex = 0, $encoding = 'utf-8', $cache = null) { $this->sourceName = $sourceName ?: '(stdin)'; $this->sourceIndex = $sourceIndex; + $this->charset = null; $this->utf8 = ! $encoding || strtolower($encoding) === 'utf-8'; $this->patternModifiers = $this->utf8 ? 'Aisu' : 'Ais'; $this->commentsSeen = []; - $this->allowVars = true; - $this->cssOnly = $cssOnly; - $this->logger = $logger ?: new QuietLogger(); + $this->discardComments = false; if (empty(static::$operatorPattern)) { - static::$operatorPattern = '([*\/%+-]|[!=]\=|\>\=?|\<\=?|and|or)'; + static::$operatorPattern = '([*\/%+-]|[!=]\=|\>\=?|\<\=\>|\<\=?|and|or)'; $commentSingle = '\/\/'; $commentMultiLeft = '\/\*'; @@ -164,7 +104,9 @@ class Parser : '/' . $commentSingle . '[^\n]*\s*|(' . static::$commentPattern . ')\s*|\s+/AisS'; } - $this->cache = $cache; + if ($cache) { + $this->cache = $cache; + } } /** @@ -186,32 +128,9 @@ class Parser * * @param string $msg * - * @phpstan-return never-return - * - * @throws ParserException - * - * @deprecated use "parseError" and throw the exception in the caller instead. + * @throws \ScssPhp\ScssPhp\Exception\ParserException */ 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); @@ -219,21 +138,11 @@ class Parser ? "line: $line, column: $column" : "$this->sourceName on line $line, at column $column"; - 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; + if ($this->peek("(.*?)(\n|$)", $m, $this->count)) { + throw new ParserException("$msg: failed at `$m[1]` $loc"); } - $this->restoreEncoding(); - - $e = new ParserException("$msg: $loc"); - $e->setSourcePosition([$this->sourceName, $line, $column]); - - return $e; + throw new ParserException("$msg: $loc"); } /** @@ -243,18 +152,19 @@ class Parser * * @param string $buffer * - * @return Block + * @return \ScssPhp\ScssPhp\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; } } @@ -282,19 +192,22 @@ class Parser ; } - if ($this->count !== \strlen($this->buffer)) { - throw $this->parseError(); + if ($this->count !== strlen($this->buffer)) { + $this->throwParseError(); } if (! empty($this->env->parent)) { - throw $this->parseError('unclosed block'); + $this->throwParseError('unclosed block'); + } + + if ($this->charset) { + array_unshift($this->env->children, $this->charset); } $this->restoreEncoding(); - assert($this->env !== null); if ($this->cache) { - $this->cache->setCache('parse', $cacheKey, $this->env, $parseOptions); + $this->cache->setCache("parse", $cacheKey, $this->env, $parseOptions); } return $this->env; @@ -308,7 +221,7 @@ class Parser * @param string $buffer * @param string|array $out * - * @return bool + * @return boolean */ public function parseValue($buffer, &$out) { @@ -319,7 +232,6 @@ class Parser $this->buffer = (string) $buffer; $this->saveEncoding(); - $this->extractLineNumbers($this->buffer); $list = $this->valueList($out); @@ -335,11 +247,10 @@ class Parser * * @param string $buffer * @param string|array $out - * @param bool $shouldValidate * - * @return bool + * @return boolean */ - public function parseSelector($buffer, &$out, $shouldValidate = true) + public function parseSelector($buffer, &$out) { $this->count = 0; $this->env = null; @@ -348,21 +259,11 @@ class Parser $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; } @@ -371,10 +272,10 @@ class Parser * * @api * - * @param string $buffer - * @param array $out + * @param string $buffer + * @param string|array $out * - * @return bool + * @return boolean */ public function parseMediaQueryList($buffer, &$out) { @@ -385,7 +286,6 @@ class Parser $this->buffer = (string) $buffer; $this->saveEncoding(); - $this->extractLineNumbers($this->buffer); $isMediaQuery = $this->mediaQueryList($out); @@ -431,7 +331,7 @@ class Parser * position into $s. Then if a chain fails, use $this->seek($s) to * go back where we started. * - * @return bool + * @return boolean */ protected function parseChunk() { @@ -439,19 +339,15 @@ class Parser // 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 = new AtRootBlock(); - $this->registerPushedBlock($atRoot, $s); + $atRoot = $this->pushSpecialBlock(Type::T_AT_ROOT, $s); $atRoot->selector = $selector; $atRoot->with = $with; @@ -460,13 +356,8 @@ class Parser $this->seek($s); - if ( - $this->literal('@media', 6) && - $this->mediaQueryList($mediaQueryList) && - $this->matchChar('{', false) - ) { - $media = new MediaBlock(); - $this->registerPushedBlock($media, $s); + if ($this->literal('@media', 6) && $this->mediaQueryList($mediaQueryList) && $this->matchChar('{', false)) { + $media = $this->pushSpecialBlock(Type::T_MEDIA, $s); $media->queryList = $mediaQueryList[2]; return true; @@ -474,16 +365,12 @@ class Parser $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 = new CallableBlock(Type::T_MIXIN); - $this->registerPushedBlock($mixin, $s); + $mixin = $this->pushSpecialBlock(Type::T_MIXIN, $s); $mixin->name = $mixinName; $mixin->args = $args; @@ -492,20 +379,17 @@ class Parser $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) ) { - ! $this->cssOnly || $this->assertPlainCssValid(false, $s); - $child = [ Type::T_INCLUDE, $mixinName, @@ -515,8 +399,7 @@ class Parser ]; if (! empty($hasBlock)) { - $include = new ContentBlock(); - $this->registerPushedBlock($include, $s); + $include = $this->pushSpecialBlock(Type::T_INCLUDE, $s); $include->child = $child; } else { $this->append($child, $s); @@ -527,17 +410,10 @@ class Parser $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); - - list($line, $column) = $this->getSourcePosition($s); - $file = $this->sourceName; - $this->logger->warn("The \"@scssphp-import-once\" directive is deprecated and will be removed in ScssPhp 2.0, in \"$file\", line $line, column $column.", true); - $this->append([Type::T_SCSSPHP_IMPORT_ONCE, $importPath], $s); return true; @@ -545,18 +421,10 @@ class Parser $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; @@ -564,17 +432,10 @@ class Parser $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; @@ -582,13 +443,10 @@ class Parser $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); @@ -598,16 +456,12 @@ class Parser $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 = new CallableBlock(Type::T_FUNCTION); - $this->registerPushedBlock($func, $s); + $func = $this->pushSpecialBlock(Type::T_FUNCTION, $s); $func->name = $fnName; $func->args = $args; @@ -616,13 +470,23 @@ class Parser $this->seek($s); - if ( - $this->literal('@return', 7) && - ($this->valueList($retVal) || true) && - $this->end() - ) { - ! $this->cssOnly || $this->assertPlainCssValid(false, $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->append([Type::T_RETURN, isset($retVal) ? $retVal : [Type::T_NULL]], $s); return true; @@ -630,17 +494,13 @@ class Parser $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 = new EachBlock(); - $this->registerPushedBlock($each, $s); + $each = $this->pushSpecialBlock(Type::T_EACH, $s); foreach ($varNames[2] as $varName) { $each->vars[] = $varName[1]; @@ -653,24 +513,11 @@ class Parser $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 = new WhileBlock(); - $this->registerPushedBlock($while, $s); + $while = $this->pushSpecialBlock(Type::T_WHILE, $s); $while->cond = $cond; return true; @@ -678,8 +525,7 @@ class Parser $this->seek($s); - if ( - $this->literal('@for', 4) && + if ($this->literal('@for', 4) && $this->variable($varName) && $this->literal('from', 4) && $this->expression($start) && @@ -688,10 +534,7 @@ class Parser $this->expression($end) && $this->matchChar('{', false) ) { - ! $this->cssOnly || $this->assertPlainCssValid(false, $s); - - $for = new ForBlock(); - $this->registerPushedBlock($for, $s); + $for = $this->pushSpecialBlock(Type::T_FOR, $s); $for->var = $varName[1]; $for->start = $start; $for->end = $end; @@ -702,24 +545,14 @@ class Parser $this->seek($s); - if ( - $this->literal('@if', 3) && - $this->functionCallArgumentsList($cond, false, '{', false) - ) { - ! $this->cssOnly || $this->assertPlainCssValid(false, $s); - - $if = new IfBlock(); - $this->registerPushedBlock($if, $s); - - while ( - $cond[0] === Type::T_LIST && - ! empty($cond['enclosing']) && - $cond['enclosing'] === 'parent' && - \count($cond[2]) == 1 - ) { + if ($this->literal('@if', 3) && $this->valueList($cond) && $this->matchChar('{', false)) { + $if = $this->pushSpecialBlock(Type::T_IF, $s); + 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 = []; @@ -728,12 +561,10 @@ class Parser $this->seek($s); - if ( - $this->literal('@debug', 6) && - $this->functionCallArgumentsList($value, false) + if ($this->literal('@debug', 6) && + $this->valueList($value) && + $this->end() ) { - ! $this->cssOnly || $this->assertPlainCssValid(false, $s); - $this->append([Type::T_DEBUG, $value], $s); return true; @@ -741,12 +572,10 @@ class Parser $this->seek($s); - if ( - $this->literal('@warn', 5) && - $this->functionCallArgumentsList($value, false) + if ($this->literal('@warn', 5) && + $this->valueList($value) && + $this->end() ) { - ! $this->cssOnly || $this->assertPlainCssValid(false, $s); - $this->append([Type::T_WARN, $value], $s); return true; @@ -754,12 +583,10 @@ class Parser $this->seek($s); - if ( - $this->literal('@error', 6) && - $this->functionCallArgumentsList($value, false) + if ($this->literal('@error', 6) && + $this->valueList($value) && + $this->end() ) { - ! $this->cssOnly || $this->assertPlainCssValid(false, $s); - $this->append([Type::T_ERROR, $value], $s); return true; @@ -767,16 +594,14 @@ class Parser $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->cssOnly || $this->assertPlainCssValid(false, $s); - + $this->argValues($argContent) && + $this->matchChar(')') && + $this->end())) { $this->append([Type::T_MIXIN_CONTENT, isset($argContent) ? $argContent : null], $s); return true; @@ -788,21 +613,17 @@ class Parser if (isset($last) && $last[0] === Type::T_IF) { list(, $if) = $last; - assert($if instanceof IfBlock); if ($this->literal('@else', 5)) { if ($this->matchChar('{', false)) { - $else = new ElseBlock(); - } elseif ( - $this->literal('if', 2) && - $this->functionCallArgumentsList($cond, false, '{', false) - ) { - $else = new ElseifBlock(); + $else = $this->pushSpecialBlock(Type::T_ELSE, $s); + } elseif ($this->literal('if', 2) && $this->valueList($cond) && $this->matchChar('{', false)) { + $else = $this->pushSpecialBlock(Type::T_ELSEIF, $s); $else->cond = $cond; } if (isset($else)) { - $this->registerPushedBlock($else, $s); + $else->dontAppend = true; $if->cases[] = $else; return true; @@ -813,23 +634,32 @@ class Parser } // only retain the first @charset directive encountered - if ( - $this->literal('@charset', 8) && + if ($this->literal('@charset', 8) && $this->valueList($charset) && $this->end() ) { + if (! isset($this->charset)) { + $statement = [Type::T_CHARSET, $charset]; + + list($line, $column) = $this->getSourcePosition($s); + + $statement[static::SOURCE_LINE] = $line; + $statement[static::SOURCE_COLUMN] = $column; + $statement[static::SOURCE_INDEX] = $this->sourceIndex; + + $this->charset = $statement; + } + return true; } $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 = new DirectiveBlock(); - $this->registerPushedBlock($directive, $s); + $directive = $this->pushSpecialBlock(Type::T_DIRECTIVE, $s); $directive->name = 'supports'; $directive->value = $supportQuery; @@ -839,26 +669,19 @@ class Parser $this->seek($s); // doesn't match built in directive, do generic one - if ( - $this->matchChar('@', false) && - $this->mixedKeyword($dirName) && - $this->directiveValue($dirValue, '{') + if ($this->matchChar('@', false) && + $this->keyword($dirName) && + ($this->variable($dirValue) || $this->openString('{', $dirValue) || true) && + $this->matchChar('{', false) ) { - if (count($dirName) === 1 && is_string(reset($dirName))) { - $dirName = reset($dirName); - } else { - $dirName = [Type::T_STRING, '', $dirName]; - } if ($dirName === 'media') { - $directive = new MediaBlock(); + $directive = $this->pushSpecialBlock(Type::T_MEDIA, $s); } else { - $directive = new DirectiveBlock(); + $directive = $this->pushSpecialBlock(Type::T_DIRECTIVE, $s); $directive->name = $dirName; } - $this->registerPushedBlock($directive, $s); if (isset($dirValue)) { - ! $this->cssOnly || ($dirValue = $this->assertPlainCssValid($dirValue)); $directive->value = $dirValue; } @@ -868,38 +691,12 @@ class Parser $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 ($this->matchChar('@', false) && + $this->keyword($dirName) && + $this->valueList($dirValue) && + $this->end() ) { - 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(); + $this->append([Type::T_DIRECTIVE, [$dirName, $dirValue]], $s); return true; } @@ -909,60 +706,9 @@ class Parser 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() @@ -976,14 +722,11 @@ class Parser $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); @@ -993,13 +736,13 @@ class Parser $this->seek($s); - // opening css block - if ( - $this->selectors($selectors) && - $this->matchChar('{', false) - ) { - ! $this->cssOnly || ! $inCssSelector || $this->assertPlainCssValid(false); + // misc + if ($this->literal('-->', 3)) { + return true; + } + // opening css block + if ($this->selectors($selectors) && $this->matchChar('{', false)) { $this->pushBlock($selectors, $s); if ($this->eatWhiteDefault) { @@ -1013,15 +756,12 @@ class Parser $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)) { - throw $this->parseError('expected "{"'); + $this->throwParseError('expected "{"'); } $this->append([Type::T_ASSIGN, $name, $value], $s); @@ -1029,10 +769,7 @@ class Parser } if ($this->matchChar('{', false)) { - ! $this->cssOnly || $this->assertPlainCssValid(false); - - $propBlock = new NestedPropertyBlock(); - $this->registerPushedBlock($propBlock, $s); + $propBlock = $this->pushSpecialBlock(Type::T_NESTED_PROPERTY, $s); $propBlock->prefix = $name; $propBlock->hasValue = $foundSomething; @@ -1053,20 +790,17 @@ class Parser $block = $this->popBlock(); if (! isset($block->type) || $block->type !== Type::T_IF) { - assert($this->env !== null); - if ($this->env->parent) { $this->append(null); // collect comments before next statement if needed } } - if ($block instanceof ContentBlock) { + if (isset($block->type) && $block->type === Type::T_INCLUDE) { $include = $block->child; - assert(\is_array($include)); unset($block->child); $include[3] = $block; $this->append($include, $s); - } elseif (!$block instanceof ElseBlock && !$block instanceof ElseifBlock) { + } elseif (empty($block->dontAppend)) { $type = isset($block->type) ? $block->type : Type::T_BLOCK; $this->append([$type, $block], $s); } @@ -1074,7 +808,6 @@ class Parser // collect comments just after the block closing if needed if ($this->eatWhiteDefault) { $this->whitespace(); - assert($this->env !== null); if ($this->env->comments) { $this->append(null); @@ -1085,7 +818,9 @@ class Parser } // extra stuff - if ($this->matchChar(';')) { + if ($this->matchChar(';') || + $this->literal('