Welcome to mirror list, hosted at ThFree Co, Russian Federation.

github.com/nextcloud/3rdparty.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorCôme Chilliet <come.chilliet@nextcloud.com>2021-11-29 17:02:50 +0300
committerCôme Chilliet <come.chilliet@nextcloud.com>2021-12-02 12:00:08 +0300
commit108a384f119a8bdad65210e7231a287f46ede73e (patch)
tree1dc65d0eb33dda826ab9e8a507e9c38bfb67541f /scssphp
parent1269091e96a2a550912db37c0d2b29ed5c0aa60f (diff)
Bump scssphp/scssphp to 1.8.1
Signed-off-by: Côme Chilliet <come.chilliet@nextcloud.com>
Diffstat (limited to 'scssphp')
-rw-r--r--scssphp/scssphp/README.md24
-rw-r--r--scssphp/scssphp/composer.json55
-rw-r--r--scssphp/scssphp/scss.inc.php46
-rw-r--r--scssphp/scssphp/src/Base/Range.php9
-rw-r--r--scssphp/scssphp/src/Block.php4
-rw-r--r--scssphp/scssphp/src/Cache.php48
-rw-r--r--scssphp/scssphp/src/Colors.php12
-rw-r--r--scssphp/scssphp/src/CompilationResult.php69
-rw-r--r--scssphp/scssphp/src/Compiler.php2513
-rw-r--r--scssphp/scssphp/src/Compiler/CachedResult.php77
-rw-r--r--scssphp/scssphp/src/Compiler/Environment.php6
-rw-r--r--scssphp/scssphp/src/Exception/CompilerException.php2
-rw-r--r--scssphp/scssphp/src/Exception/ParserException.php2
-rw-r--r--scssphp/scssphp/src/Exception/RangeException.php2
-rw-r--r--scssphp/scssphp/src/Exception/ServerException.php4
-rw-r--r--scssphp/scssphp/src/Formatter.php16
-rw-r--r--scssphp/scssphp/src/Formatter/Compact.php2
-rw-r--r--scssphp/scssphp/src/Formatter/Compressed.php4
-rw-r--r--scssphp/scssphp/src/Formatter/Crunched.php4
-rw-r--r--scssphp/scssphp/src/Formatter/Debug.php2
-rw-r--r--scssphp/scssphp/src/Formatter/Expanded.php2
-rw-r--r--scssphp/scssphp/src/Formatter/Nested.php2
-rw-r--r--scssphp/scssphp/src/Formatter/OutputBlock.php14
-rw-r--r--scssphp/scssphp/src/Logger/LoggerInterface.php48
-rw-r--r--scssphp/scssphp/src/Logger/QuietLogger.php27
-rw-r--r--scssphp/scssphp/src/Logger/StreamLogger.php60
-rw-r--r--scssphp/scssphp/src/Node.php6
-rw-r--r--scssphp/scssphp/src/Node/Number.php69
-rw-r--r--scssphp/scssphp/src/Parser.php85
-rw-r--r--scssphp/scssphp/src/SourceMap/Base64.php6
-rw-r--r--scssphp/scssphp/src/SourceMap/Base64VLQ.php2
-rw-r--r--scssphp/scssphp/src/SourceMap/SourceMapGenerator.php19
-rw-r--r--scssphp/scssphp/src/Type.php1
-rw-r--r--scssphp/scssphp/src/Util.php21
-rw-r--r--scssphp/scssphp/src/ValueConverter.php95
-rw-r--r--scssphp/scssphp/src/Version.php2
-rw-r--r--scssphp/scssphp/src/Warn.php84
37 files changed, 2640 insertions, 804 deletions
diff --git a/scssphp/scssphp/README.md b/scssphp/scssphp/README.md
index 54557344..65bb93ea 100644
--- a/scssphp/scssphp/README.md
+++ b/scssphp/scssphp/README.md
@@ -45,3 +45,27 @@ To enable the full `sass-spec` compatibility tests:
Run the following command from the root directory to check the code for "sniffs".
vendor/bin/phpcs --standard=PSR12 --extensions=php bin src tests *.php
+
+## Static Analysis
+
+`scssphp` uses [phpstan](https://phpstan.org/) for static analysis.
+
+Run the following command from the root directory to analyse the codebase:
+
+ make phpstan
+
+As most of the codebase is composed of legacy code which cannot be type-checked
+fully, the setup contains a baseline file with all errors we want to ignore. In
+particular, we ignore all errors related to not specifying the types inside arrays
+when these arrays correspond to the representation of Sass values and Sass AST nodes
+in the parser and compiler.
+When contributing, the proper process to deal with static analysis is the following:
+
+1. Make your change in the codebase
+2. Run `make phpstan`
+3. Fix errors reported by phpstan when possible
+4. Repeat step 2 and 3 until nothing gets fixed anymore at step 3
+5. Run `make phpstan-baseline` to regenerate the phpstan baseline
+
+Additions to the baseline will be reviewed to avoid ignoring errors that should have
+been fixed.
diff --git a/scssphp/scssphp/composer.json b/scssphp/scssphp/composer.json
index e4c47d34..86cd396b 100644
--- a/scssphp/scssphp/composer.json
+++ b/scssphp/scssphp/composer.json
@@ -30,12 +30,19 @@
"ext-json": "*",
"ext-ctype": "*"
},
+ "suggest": {
+ "ext-mbstring": "For best performance, mbstring should be installed as it is faster than ext-iconv",
+ "ext-iconv": "Can be used as fallback when ext-mbstring is not available"
+ },
"require-dev": {
+ "bamarni/composer-bin-plugin": "^1.4",
"phpunit/phpunit": "^5.7 || ^6.5 || ^7.5 || ^8.3 || ^9.4",
- "sass/sass-spec": "2020.12.29",
+ "sass/sass-spec": "*",
"squizlabs/php_codesniffer": "~3.5",
"symfony/phpunit-bridge": "^5.1",
- "twbs/bootstrap": "~4.3",
+ "thoughtbot/bourbon": "^7.0",
+ "twbs/bootstrap": "~5.0",
+ "twbs/bootstrap4": "4.6.0",
"zurb/foundation": "~6.5"
},
"repositories": [
@@ -43,16 +50,52 @@
"type": "package",
"package": {
"name": "sass/sass-spec",
- "version": "2020.12.29",
+ "version": "2021.09.15",
"source": {
"type": "git",
"url": "https://github.com/sass/sass-spec.git",
- "reference": "d975d33146fb679a6b359ceca329012f02e4a794"
+ "reference": "eb2d7a0865c1faf0b55a39ff962b24aca9b4c955"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sass/sass-spec/zipball/eb2d7a0865c1faf0b55a39ff962b24aca9b4c955",
+ "reference": "eb2d7a0865c1faf0b55a39ff962b24aca9b4c955",
+ "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.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/twbs/bootstrap.git",
+ "reference": "6ffb0b48e455430f8a5359ed689ad64c1143fac2"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/sass/sass-spec/zipball/d975d33146fb679a6b359ceca329012f02e4a794",
- "reference": "d975d33146fb679a6b359ceca329012f02e4a794",
+ "url": "https://api.github.com/repos/twbs/bootstrap/zipball/6ffb0b48e455430f8a5359ed689ad64c1143fac2",
+ "reference": "6ffb0b48e455430f8a5359ed689ad64c1143fac2",
"shasum": ""
}
}
diff --git a/scssphp/scssphp/scss.inc.php b/scssphp/scssphp/scss.inc.php
index 6b39d320..45983780 100644
--- a/scssphp/scssphp/scss.inc.php
+++ b/scssphp/scssphp/scss.inc.php
@@ -4,36 +4,18 @@ if (version_compare(PHP_VERSION, '5.6') < 0) {
throw new \Exception('scssphp requires PHP 5.6 or above');
}
-if (! class_exists('ScssPhp\ScssPhp\Version', false)) {
- include_once __DIR__ . '/src/Base/Range.php';
- include_once __DIR__ . '/src/OutputStyle.php';
- include_once __DIR__ . '/src/Block.php';
- include_once __DIR__ . '/src/Cache.php';
- include_once __DIR__ . '/src/Colors.php';
- include_once __DIR__ . '/src/Compiler.php';
- include_once __DIR__ . '/src/Compiler/Environment.php';
- include_once __DIR__ . '/src/Exception/SassException.php';
- include_once __DIR__ . '/src/Exception/SassScriptException.php';
- include_once __DIR__ . '/src/Exception/CompilerException.php';
- include_once __DIR__ . '/src/Exception/ParserException.php';
- include_once __DIR__ . '/src/Exception/RangeException.php';
- include_once __DIR__ . '/src/Exception/ServerException.php';
- include_once __DIR__ . '/src/Formatter.php';
- include_once __DIR__ . '/src/Formatter/Compact.php';
- include_once __DIR__ . '/src/Formatter/Compressed.php';
- include_once __DIR__ . '/src/Formatter/Crunched.php';
- include_once __DIR__ . '/src/Formatter/Debug.php';
- include_once __DIR__ . '/src/Formatter/Expanded.php';
- include_once __DIR__ . '/src/Formatter/Nested.php';
- include_once __DIR__ . '/src/Formatter/OutputBlock.php';
- include_once __DIR__ . '/src/Node.php';
- include_once __DIR__ . '/src/Node/Number.php';
- include_once __DIR__ . '/src/Parser.php';
- include_once __DIR__ . '/src/SourceMap/Base64.php';
- include_once __DIR__ . '/src/SourceMap/Base64VLQ.php';
- include_once __DIR__ . '/src/SourceMap/SourceMapGenerator.php';
- include_once __DIR__ . '/src/Type.php';
- include_once __DIR__ . '/src/Util/Path.php';
- include_once __DIR__ . '/src/Util.php';
- include_once __DIR__ . '/src/Version.php';
+if (! class_exists('ScssPhp\ScssPhp\Version')) {
+ spl_autoload_register(function ($class) {
+ if (0 !== strpos($class, 'ScssPhp\ScssPhp\\')) {
+ // Not a ScssPhp class
+ return;
+ }
+
+ $subClass = substr($class, strlen('ScssPhp\ScssPhp\\'));
+ $path = __DIR__ . '/src/' . str_replace('\\', '/', $subClass) . '.php';
+
+ if (file_exists($path)) {
+ require $path;
+ }
+ });
}
diff --git a/scssphp/scssphp/src/Base/Range.php b/scssphp/scssphp/src/Base/Range.php
index 2846746d..0c5f5840 100644
--- a/scssphp/scssphp/src/Base/Range.php
+++ b/scssphp/scssphp/src/Base/Range.php
@@ -16,10 +16,19 @@ namespace ScssPhp\ScssPhp\Base;
* Range
*
* @author Anthon Pang <anthon.pang@gmail.com>
+ *
+ * @internal
*/
class Range
{
+ /**
+ * @var float|int
+ */
public $first;
+
+ /**
+ * @var float|int
+ */
public $last;
/**
diff --git a/scssphp/scssphp/src/Block.php b/scssphp/scssphp/src/Block.php
index f7f4571e..3ae49d00 100644
--- a/scssphp/scssphp/src/Block.php
+++ b/scssphp/scssphp/src/Block.php
@@ -16,6 +16,8 @@ namespace ScssPhp\ScssPhp;
* Block
*
* @author Anthon Pang <anthon.pang@gmail.com>
+ *
+ * @internal
*/
class Block
{
@@ -65,7 +67,7 @@ class Block
public $children;
/**
- * @var \ScssPhp\ScssPhp\Block
+ * @var \ScssPhp\ScssPhp\Block|null
*/
public $selfParent;
}
diff --git a/scssphp/scssphp/src/Cache.php b/scssphp/scssphp/src/Cache.php
index 4738ee4e..9731c60a 100644
--- a/scssphp/scssphp/src/Cache.php
+++ b/scssphp/scssphp/src/Cache.php
@@ -30,30 +30,54 @@ use ScssPhp\ScssPhp\Version;
* SCSS cache
*
* @author Cedric Morin <cedric@yterium.com>
+ *
+ * @internal
*/
class Cache
{
const CACHE_VERSION = 1;
- // directory used for storing data
+ /**
+ * directory used for storing data
+ *
+ * @var string|false
+ */
public static $cacheDir = false;
- // prefix for the storing data
+ /**
+ * prefix for the storing data
+ *
+ * @var string
+ */
public static $prefix = 'scssphp_';
- // force a refresh : 'once' for refreshing the first hit on a cache only, true to never use the cache in this hit
+ /**
+ * force a refresh : 'once' for refreshing the first hit on a cache only, true to never use the cache in this hit
+ *
+ * @var bool|string
+ */
public static $forceRefresh = false;
- // specifies the number of seconds after which data cached will be seen as 'garbage' and potentially cleaned up
+ /**
+ * specifies the number of seconds after which data cached will be seen as 'garbage' and potentially cleaned up
+ *
+ * @var int
+ */
public static $gcLifetime = 604800;
- // array of already refreshed cache if $forceRefresh==='once'
+ /**
+ * array of already refreshed cache if $forceRefresh==='once'
+ *
+ * @var array<string, bool>
+ */
protected static $refreshed = [];
/**
* Constructor
*
* @param array $options
+ *
+ * @phpstan-param array{cacheDir?: string, prefix?: string, forceRefresh?: string} $options
*/
public function __construct($options)
{
@@ -85,10 +109,10 @@ class Cache
* Get the cached result of $operation on $what,
* which is known as dependant from the content of $options
*
- * @param string $operation parse, compile...
- * @param mixed $what content key (e.g., filename to be treated)
- * @param array $options any option that affect the operation result on the content
- * @param integer $lastModified last modified timestamp
+ * @param string $operation parse, compile...
+ * @param mixed $what content key (e.g., filename to be treated)
+ * @param array $options any option that affect the operation result on the content
+ * @param int|null $lastModified last modified timestamp
*
* @return mixed
*
@@ -128,6 +152,8 @@ class Cache
* @param mixed $what
* @param mixed $value
* @param array $options
+ *
+ * @return void
*/
public function setCache($operation, $what, $value, $options = [])
{
@@ -174,6 +200,8 @@ class Cache
/**
* Check that the cache dir exists and is writeable
*
+ * @return void
+ *
* @throws \Exception
*/
public static function checkCacheDir()
@@ -192,6 +220,8 @@ class Cache
/**
* Delete unused cached files
+ *
+ * @return void
*/
public static function cleanCache()
{
diff --git a/scssphp/scssphp/src/Colors.php b/scssphp/scssphp/src/Colors.php
index 4b62c361..e836e3f4 100644
--- a/scssphp/scssphp/src/Colors.php
+++ b/scssphp/scssphp/src/Colors.php
@@ -16,6 +16,8 @@ namespace ScssPhp\ScssPhp;
* CSS Colors
*
* @author Leaf Corcoran <leafot@gmail.com>
+ *
+ * @internal
*/
class Colors
{
@@ -24,13 +26,13 @@ class Colors
*
* @see http://www.w3.org/TR/css3-color
*
- * @var array
+ * @var array<string, string>
*/
protected static $cssColors = [
'aliceblue' => '240,248,255',
'antiquewhite' => '250,235,215',
- 'cyan' => '0,255,255',
'aqua' => '0,255,255',
+ 'cyan' => '0,255,255',
'aquamarine' => '127,255,212',
'azure' => '240,255,255',
'beige' => '245,245,220',
@@ -75,8 +77,8 @@ class Colors
'firebrick' => '178,34,34',
'floralwhite' => '255,250,240',
'forestgreen' => '34,139,34',
- 'magenta' => '255,0,255',
'fuchsia' => '255,0,255',
+ 'magenta' => '255,0,255',
'gainsboro' => '220,220,220',
'ghostwhite' => '248,248,255',
'gold' => '255,215,0',
@@ -183,7 +185,7 @@ class Colors
*
* @param string $colorName
*
- * @return array|null
+ * @return int[]|null
*/
public static function colorNameToRGBa($colorName)
{
@@ -205,7 +207,7 @@ class Colors
* @param integer $r
* @param integer $g
* @param integer $b
- * @param integer $a
+ * @param integer|float $a
*
* @return string|null
*/
diff --git a/scssphp/scssphp/src/CompilationResult.php b/scssphp/scssphp/src/CompilationResult.php
new file mode 100644
index 00000000..36adb0da
--- /dev/null
+++ b/scssphp/scssphp/src/CompilationResult.php
@@ -0,0 +1,69 @@
+<?php
+
+/**
+ * SCSSPHP
+ *
+ * @copyright 2012-2020 Leaf Corcoran
+ *
+ * @license http://opensource.org/licenses/MIT MIT
+ *
+ * @link http://scssphp.github.io/scssphp
+ */
+
+namespace ScssPhp\ScssPhp;
+
+class CompilationResult
+{
+ /**
+ * @var string
+ */
+ private $css;
+
+ /**
+ * @var string|null
+ */
+ private $sourceMap;
+
+ /**
+ * @var string[]
+ */
+ private $includedFiles;
+
+ /**
+ * @param string $css
+ * @param string|null $sourceMap
+ * @param string[] $includedFiles
+ */
+ public function __construct($css, $sourceMap, array $includedFiles)
+ {
+ $this->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/scssphp/scssphp/src/Compiler.php b/scssphp/scssphp/src/Compiler.php
index 0997814e..58ba795a 100644
--- a/scssphp/scssphp/src/Compiler.php
+++ b/scssphp/scssphp/src/Compiler.php
@@ -13,13 +13,17 @@
namespace ScssPhp\ScssPhp;
use ScssPhp\ScssPhp\Base\Range;
+use ScssPhp\ScssPhp\Compiler\CachedResult;
use ScssPhp\ScssPhp\Compiler\Environment;
use ScssPhp\ScssPhp\Exception\CompilerException;
use ScssPhp\ScssPhp\Exception\ParserException;
+use ScssPhp\ScssPhp\Exception\SassException;
use ScssPhp\ScssPhp\Exception\SassScriptException;
use ScssPhp\ScssPhp\Formatter\Compressed;
use ScssPhp\ScssPhp\Formatter\Expanded;
use ScssPhp\ScssPhp\Formatter\OutputBlock;
+use ScssPhp\ScssPhp\Logger\LoggerInterface;
+use ScssPhp\ScssPhp\Logger\StreamLogger;
use ScssPhp\ScssPhp\Node\Number;
use ScssPhp\ScssPhp\SourceMap\SourceMapGenerator;
use ScssPhp\ScssPhp\Util\Path;
@@ -55,6 +59,8 @@ use ScssPhp\ScssPhp\Util\Path;
* SCSS compiler
*
* @author Leaf Corcoran <leafot@gmail.com>
+ *
+ * @final Extending the Compiler is deprecated
*/
class Compiler
{
@@ -140,11 +146,20 @@ class Compiler
* @var array<string, Block>
*/
protected $importCache = [];
+
/**
* @var string[]
*/
protected $importedFiles = [];
+
+ /**
+ * @var array
+ * @phpstan-var array<string, array{0: callable, 1: array|null}>
+ */
protected $userFunctions = [];
+ /**
+ * @var array<string, mixed>
+ */
protected $registeredVars = [];
/**
* @var array<string, bool>
@@ -161,6 +176,7 @@ class Compiler
*/
protected $encoding = null;
/**
+ * @var null
* @deprecated
*/
protected $lineNumberStyle = null;
@@ -170,9 +186,19 @@ class Compiler
* @phpstan-var self::SOURCE_MAP_*|SourceMapGenerator
*/
protected $sourceMap = self::SOURCE_MAP_NONE;
+
+ /**
+ * @var array
+ * @phpstan-var array{sourceRoot?: string, sourceMapFilename?: string|null, sourceMapURL?: string|null, sourceMapWriteTo?: string|null, outputSourceFiles?: bool, sourceMapRootpath?: string, sourceMapBasepath?: string}
+ */
protected $sourceMapOptions = [];
/**
+ * @var bool
+ */
+ private $charset = true;
+
+ /**
* @var string|\ScssPhp\ScssPhp\Formatter
*/
protected $formatter = Expanded::class;
@@ -200,10 +226,12 @@ class Compiler
protected $storeEnv;
/**
* @var bool|null
+ *
+ * @deprecated
*/
protected $charsetSeen;
/**
- * @var array<int, string>
+ * @var array<int, string|null>
*/
protected $sourceNames;
@@ -213,6 +241,11 @@ class Compiler
protected $cache;
/**
+ * @var bool
+ */
+ protected $cacheCheckImportResolutions = false;
+
+ /**
* @var int
*/
protected $indentLevel;
@@ -224,10 +257,12 @@ class Compiler
* @var array<string, int[]>
*/
protected $extendsMap;
+
/**
* @var array<string, int>
*/
- protected $parsedFiles;
+ protected $parsedFiles = [];
+
/**
* @var Parser|null
*/
@@ -245,10 +280,6 @@ class Compiler
*/
protected $sourceColumn;
/**
- * @var resource
- */
- protected $stderr;
- /**
* @var bool|null
*/
protected $shouldEvaluate;
@@ -268,6 +299,12 @@ class Compiler
protected $callStack = [];
/**
+ * @var array
+ * @phpstan-var list<array{currentDir: string|null, path: string, filePath: string}>
+ */
+ private $resolvedImports = [];
+
+ /**
* The directory of the currently processed file
*
* @var string|null
@@ -281,29 +318,47 @@ class Compiler
*/
private $rootDirectory;
+ /**
+ * @var bool
+ */
private $legacyCwdImportPath = true;
/**
+ * @var LoggerInterface
+ */
+ private $logger;
+
+ /**
+ * @var array<string, bool>
+ */
+ private $warnedChildFunctions = [];
+
+ /**
* Constructor
*
* @param array|null $cacheOptions
+ * @phpstan-param array{cacheDir?: string, prefix?: string, forceRefresh?: string, checkImportResolutions?: bool}|null $cacheOptions
*/
public function __construct($cacheOptions = null)
{
- $this->parsedFiles = [];
$this->sourceNames = [];
if ($cacheOptions) {
$this->cache = new Cache($cacheOptions);
+ if (!empty($cacheOptions['checkImportResolutions'])) {
+ $this->cacheCheckImportResolutions = true;
+ }
}
- $this->stderr = fopen('php://stderr', 'w');
+ $this->logger = new StreamLogger(fopen('php://stderr', 'w'), true);
}
/**
* Get compiler options
*
* @return array<string, mixed>
+ *
+ * @internal
*/
public function getCompileOptions()
{
@@ -322,49 +377,89 @@ class Compiler
}
/**
+ * Sets an alternative logger.
+ *
+ * Changing the logger in the middle of the compilation is not
+ * supported and will result in an undefined behavior.
+ *
+ * @param LoggerInterface $logger
+ *
+ * @return void
+ */
+ public function setLogger(LoggerInterface $logger)
+ {
+ $this->logger = $logger;
+ }
+
+ /**
* Set an alternative error output stream, for testing purpose only
*
* @param resource $handle
*
* @return void
+ *
+ * @deprecated Use {@see setLogger} instead
*/
public function setErrorOuput($handle)
{
- $this->stderr = $handle;
+ @trigger_error('The method "setErrorOuput" is deprecated. Use "setLogger" instead.', E_USER_DEPRECATED);
+
+ $this->logger = new StreamLogger($handle);
}
/**
* Compile scss
*
- * @api
- *
- * @param string $code
- * @param string $path
+ * @param string $code
+ * @param string|null $path
*
* @return string
+ *
+ * @throws SassException when the source fails to compile
+ *
+ * @deprecated Use {@see compileString} instead.
*/
public function compile($code, $path = null)
{
- if ($this->cache) {
- $cacheKey = ($path ? $path : '(stdin)') . ':' . md5($code);
- $compileOptions = $this->getCompileOptions();
- $cache = $this->cache->getCache('compile', $cacheKey, $compileOptions);
+ @trigger_error(sprintf('The "%s" method is deprecated. Use "compileString" instead.', __METHOD__), E_USER_DEPRECATED);
- if (\is_array($cache) && isset($cache['dependencies']) && isset($cache['out'])) {
- // check if any dependency file changed before accepting the cache
- foreach ($cache['dependencies'] as $file => $mtime) {
- if (! is_file($file) || filemtime($file) !== $mtime) {
- unset($cache);
- break;
- }
- }
+ $result = $this->compileString($code, $path);
- if (isset($cache)) {
- return $cache['out'];
- }
+ $sourceMap = $result->getSourceMap();
+
+ if ($sourceMap !== null) {
+ if ($this->sourceMap instanceof SourceMapGenerator) {
+ $this->sourceMap->saveMap($sourceMap);
+ } elseif ($this->sourceMap === self::SOURCE_MAP_FILE) {
+ $sourceMapGenerator = new SourceMapGenerator($this->sourceMapOptions);
+ $sourceMapGenerator->saveMap($sourceMap);
}
}
+ return $result->getCss();
+ }
+
+ /**
+ * Compile scss
+ *
+ * @param string $source
+ * @param string|null $path
+ *
+ * @return CompilationResult
+ *
+ * @throws SassException when the source fails to compile
+ */
+ public function compileString($source, $path = null)
+ {
+ if ($this->cache) {
+ $cacheKey = ($path ? $path : '(stdin)') . ':' . md5($source);
+ $compileOptions = $this->getCompileOptions();
+ $cachedResult = $this->cache->getCache('compile', $cacheKey, $compileOptions);
+
+ if ($cachedResult instanceof CachedResult && $this->isFreshCachedResult($cachedResult)) {
+ return $cachedResult->getResult();
+ }
+ }
$this->indentLevel = -1;
$this->extends = [];
@@ -375,9 +470,11 @@ 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;
@@ -390,16 +487,25 @@ class Compiler
try {
$this->parser = $this->parserFactory($path);
- $tree = $this->parser->parse($code);
+ $tree = $this->parser->parse($source);
$this->parser = null;
$this->formatter = new $this->formatter();
$this->rootBlock = null;
$this->rootEnv = $this->pushEnv($tree);
- $this->injectVariables($this->registeredVars);
- $this->compileRoot($tree);
- $this->popEnv();
+ $warnCallback = function ($message, $deprecation) {
+ $this->logger->warn($message, $deprecation);
+ };
+ $previousWarnCallback = Warn::setCallback($warnCallback);
+
+ try {
+ $this->injectVariables($this->registeredVars);
+ $this->compileRoot($tree);
+ $this->popEnv();
+ } finally {
+ Warn::setCallback($previousWarnCallback);
+ }
$sourceMapGenerator = null;
@@ -416,15 +522,15 @@ class Compiler
$prefix = '';
- if (!$this->charsetSeen) {
- if (strlen($out) !== Util::mbStrlen($out)) {
- $prefix = '@charset "UTF-8";' . "\n";
- $out = $prefix . $out;
- }
+ 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) {
- $sourceMap = $sourceMapGenerator->generateJson($prefix);
+ $sourceMap = $sourceMapGenerator->generateJson($prefix);
$sourceMapUrl = null;
switch ($this->sourceMap) {
@@ -433,32 +539,80 @@ class Compiler
break;
case self::SOURCE_MAP_FILE:
- $sourceMapUrl = $sourceMapGenerator->saveMap($sourceMap);
+ if (isset($this->sourceMapOptions['sourceMapURL'])) {
+ $sourceMapUrl = $this->sourceMapOptions['sourceMapURL'];
+ }
break;
}
- $out .= sprintf('/*# sourceMappingURL=%s */', $sourceMapUrl);
+ if ($sourceMapUrl !== null) {
+ $out .= sprintf('/*# sourceMappingURL=%s */', $sourceMapUrl);
+ }
}
} catch (SassScriptException $e) {
- throw $this->error($e->getMessage());
+ throw new CompilerException($this->addLocationToMessage($e->getMessage()), 0, $e);
+ }
+
+ $includedFiles = [];
+
+ foreach ($this->resolvedImports as $resolvedImport) {
+ $includedFiles[$resolvedImport['filePath']] = $resolvedImport['filePath'];
}
+ $result = new CompilationResult($out, $sourceMap, array_values($includedFiles));
+
if ($this->cache && isset($cacheKey) && isset($compileOptions)) {
- $v = [
- 'dependencies' => $this->getParsedFiles(),
- 'out' => &$out,
- ];
+ $this->cache->setCache('compile', $cacheKey, new CachedResult($result, $this->parsedFiles, $this->resolvedImports), $compileOptions);
+ }
+
+ // Reset state to free memory
+ // TODO in 2.0, reset parsedFiles as well when the getter is removed.
+ $this->resolvedImports = [];
+ $this->importedFiles = [];
- $this->cache->setCache('compile', $cacheKey, $v, $compileOptions);
+ 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;
+ }
}
- return $out;
+ if ($this->cacheCheckImportResolutions) {
+ $resolvedImports = [];
+
+ foreach ($result->getResolvedImports() as $import) {
+ $currentDir = $import['currentDir'];
+ $path = $import['path'];
+ // store the check across all the results in memory to avoid multiple findImport() on the same path
+ // with same context.
+ // this is happening in a same hit with multiple compilations (especially with big frameworks)
+ if (empty($resolvedImports[$currentDir][$path])) {
+ $resolvedImports[$currentDir][$path] = $this->findImport($path, $currentDir);
+ }
+
+ if ($resolvedImports[$currentDir][$path] !== $import['filePath']) {
+ return false;
+ }
+ }
+ }
+
+ return true;
}
/**
* Instantiate parser
*
- * @param string $path
+ * @param string|null $path
*
* @return \ScssPhp\ScssPhp\Parser
*/
@@ -471,11 +625,11 @@ class Compiler
// Otherwise, the CSS will be rendered as-is. It can even be extended!
$cssOnly = false;
- if (substr($path, '-4') === '.css') {
+ if ($path !== null && substr($path, -4) === '.css') {
$cssOnly = true;
}
- $parser = new Parser($path, \count($this->sourceNames), $this->encoding, $this->cache, $cssOnly);
+ $parser = new Parser($path, \count($this->sourceNames), $this->encoding, $this->cache, $cssOnly, $this->logger);
$this->sourceNames[] = $path;
$this->addParsedFile($path);
@@ -528,8 +682,8 @@ class Compiler
/**
* Make output block
*
- * @param string $type
- * @param array $selectors
+ * @param string|null $type
+ * @param string[]|null $selectors
*
* @return \ScssPhp\ScssPhp\Formatter\OutputBlock
*/
@@ -661,7 +815,7 @@ class Compiler
}
/**
- * Glue parts of :not( or :nth-child( ... that are in general splitted in selectors parts
+ * Glue parts of :not( or :nth-child( ... that are in general split in selectors parts
*
* @param array $parts
*
@@ -1119,7 +1273,7 @@ class Compiler
$mediaQueries = $this->compileMediaQuery($this->multiplyMedia($this->env));
- if (! empty($mediaQueries) && $mediaQueries) {
+ if (! empty($mediaQueries)) {
$previousScope = $this->scope;
$parentScope = $this->mediaParent($this->scope);
@@ -1192,7 +1346,7 @@ class Compiler
/**
* Compile directive
*
- * @param \ScssPhp\ScssPhp\Block|array $block
+ * @param \ScssPhp\ScssPhp\Block|array $directive
* @param \ScssPhp\ScssPhp\Formatter\OutputBlock $out
*
* @return void
@@ -1237,7 +1391,7 @@ class Compiler
* directive names can include some interpolation
*
* @param string|array $directiveName
- * @return array|string
+ * @return string
* @throws CompilerException
*/
protected function compileDirectiveName($directiveName)
@@ -1280,9 +1434,10 @@ class Compiler
}
$selfParent = $block->selfParent;
+ assert($selfParent !== null, 'at-root blocks must have a selfParent set.');
if (
- ! $block->selfParent->selectors &&
+ ! $selfParent->selectors &&
isset($block->parent) && $block->parent &&
isset($block->parent->selectors) && $block->parent->selectors
) {
@@ -1318,12 +1473,12 @@ class Compiler
$filteredScopes = [];
$childStash = [];
- if ($scope->type === TYPE::T_ROOT) {
+ if ($scope->type === Type::T_ROOT) {
return $scope;
}
// start from the root
- while ($scope->parent && $scope->parent->type !== TYPE::T_ROOT) {
+ while ($scope->parent && $scope->parent->type !== Type::T_ROOT) {
array_unshift($childStash, $scope);
$scope = $scope->parent;
}
@@ -1451,7 +1606,7 @@ class Compiler
}
}
- if ($this->libMapHasKey([$withCondition, static::$with])) {
+ if ($this->mapHasKey($withCondition, static::$with)) {
$without = []; // cancel the default
$list = $this->coerceList($this->libMapGet([$withCondition, static::$with]));
@@ -1462,7 +1617,7 @@ class Compiler
}
}
- if ($this->libMapHasKey([$withCondition, static::$without])) {
+ if ($this->mapHasKey($withCondition, static::$without)) {
$without = []; // cancel the default
$list = $this->coerceList($this->libMapGet([$withCondition, static::$without]));
@@ -1578,7 +1733,7 @@ class Compiler
* Compile keyframe block
*
* @param \ScssPhp\ScssPhp\Block $block
- * @param array $selectors
+ * @param string[] $selectors
*
* @return void
*/
@@ -1644,7 +1799,7 @@ class Compiler
* Compile nested block
*
* @param \ScssPhp\ScssPhp\Block $block
- * @param array $selectors
+ * @param string[] $selectors
*
* @return void
*/
@@ -1760,17 +1915,16 @@ class Compiler
$this->pushEnv();
}
- $ignoreCallStackMessage = $this->ignoreCallStackMessage;
- $this->ignoreCallStackMessage = true;
-
try {
$c = $this->compileValue($value[2]);
- } catch (\Exception $e) {
+ } catch (SassScriptException $e) {
+ $this->logger->warn('Ignoring interpolation errors in multiline comments is deprecated and will be removed in ScssPhp 2.0. ' . $this->addLocationToMessage($e->getMessage()), true);
+ // ignore error in comment compilation which are only interpolation
+ } catch (SassException $e) {
+ $this->logger->warn('Ignoring interpolation errors in multiline comments is deprecated and will be removed in ScssPhp 2.0. ' . $e->getMessage(), true);
// ignore error in comment compilation which are only interpolation
}
- $this->ignoreCallStackMessage = $ignoreCallStackMessage;
-
if ($pushEnv) {
$this->popEnv();
}
@@ -1871,14 +2025,44 @@ class Compiler
/**
* Collapse selectors
*
- * @param array $selectors
- * @param boolean $selectorFormat
- * if false return a collapsed string
- * if true return an array description of a structured selector
+ * @param array $selectors
*
* @return string
*/
- protected function collapseSelectors($selectors, $selectorFormat = false)
+ protected function collapseSelectors($selectors)
+ {
+ $parts = [];
+
+ foreach ($selectors as $selector) {
+ $output = [];
+
+ foreach ($selector as $node) {
+ $compound = '';
+
+ array_walk_recursive(
+ $node,
+ function ($value, $key) use (&$compound) {
+ $compound .= $value;
+ }
+ );
+
+ $output[] = $compound;
+ }
+
+ $parts[] = implode(' ', $output);
+ }
+
+ return implode(', ', $parts);
+ }
+
+ /**
+ * Collapse selectors
+ *
+ * @param array $selectors
+ *
+ * @return array
+ */
+ private function collapseSelectorsAsList($selectors)
{
$parts = [];
@@ -1896,7 +2080,7 @@ class Compiler
}
);
- if ($selectorFormat && $this->isImmediateRelationshipCombinator($compound)) {
+ if ($this->isImmediateRelationshipCombinator($compound)) {
if (\count($output)) {
$output[\count($output) - 1] .= ' ' . $compound;
} else {
@@ -1912,32 +2096,21 @@ class Compiler
}
}
- if ($selectorFormat) {
- foreach ($output as &$o) {
- $o = [Type::T_STRING, '', [$o]];
- }
-
- $output = [Type::T_LIST, ' ', $output];
- } else {
- $output = implode(' ', $output);
+ foreach ($output as &$o) {
+ $o = [Type::T_STRING, '', [$o]];
}
- $parts[] = $output;
+ $parts[] = [Type::T_LIST, ' ', $output];
}
- if ($selectorFormat) {
- $parts = [Type::T_LIST, ',', $parts];
- } else {
- $parts = implode(', ', $parts);
- }
-
- return $parts;
+ return [Type::T_LIST, ',', $parts];
}
/**
* Parse down the selector and revert [self] to "&" before a reparsing
*
- * @param array $selectors
+ * @param array $selectors
+ * @param string|null $replace
*
* @return array
*/
@@ -2104,7 +2277,7 @@ class Compiler
* @param \ScssPhp\ScssPhp\Formatter\OutputBlock $out
* @param string $traceName
*
- * @return array|null
+ * @return array|Number|null
*/
protected function compileChildren($stms, OutputBlock $out, $traceName = '')
{
@@ -2146,7 +2319,7 @@ class Compiler
$stm[1]->selfParent = $selfParent;
$ret = $this->compileChild($stm, $out);
$stm[1]->selfParent = null;
- } elseif ($selfParent && \in_array($stm[0], [TYPE::T_INCLUDE, TYPE::T_EXTEND])) {
+ } elseif ($selfParent && \in_array($stm[0], [Type::T_INCLUDE, Type::T_EXTEND])) {
$stm['selfParent'] = $selfParent;
$ret = $this->compileChild($stm, $out);
unset($stm['selfParent']);
@@ -2234,7 +2407,7 @@ class Compiler
*
* @param array $queryList
*
- * @return array
+ * @return string[]
*/
protected function compileMediaQuery($queryList)
{
@@ -2472,10 +2645,12 @@ class Compiler
if ($rawPath[0] === Type::T_STRING) {
$path = $this->compileStringContent($rawPath);
- if (strpos($path, 'url(') !== 0 && $path = $this->findImport($path)) {
- if (! $once || ! \in_array($path, $this->importedFiles)) {
- $this->importFile($path, $out);
- $this->importedFiles[] = $path;
+ if (strpos($path, 'url(') !== 0 && $filePath = $this->findImport($path, $this->currentDirectory)) {
+ $this->registerImport($this->currentDirectory, $path, $filePath);
+
+ if (! $once || ! \in_array($filePath, $this->importedFiles)) {
+ $this->importFile($filePath, $out);
+ $this->importedFiles[] = $filePath;
}
return true;
@@ -2616,7 +2791,7 @@ class Compiler
*
* @param \ScssPhp\ScssPhp\Formatter\OutputBlock $out
* @param string $type
- * @param string|mixed $line
+ * @param string $line
*
* @return void
*/
@@ -2667,12 +2842,13 @@ class Compiler
$this->sourceColumn = $child[1]->sourceColumn;
} elseif (! empty($out->sourceLine) && ! empty($out->sourceName)) {
$this->sourceLine = $out->sourceLine;
- $this->sourceIndex = array_search($out->sourceName, $this->sourceNames);
+ $sourceIndex = array_search($out->sourceName, $this->sourceNames);
$this->sourceColumn = $out->sourceColumn;
- if ($this->sourceIndex === false) {
- $this->sourceIndex = null;
+ if ($sourceIndex === false) {
+ $sourceIndex = null;
}
+ $this->sourceIndex = $sourceIndex;
}
switch ($child[0]) {
@@ -2705,10 +2881,6 @@ class Compiler
break;
case Type::T_CHARSET:
- if (! $this->charsetSeen) {
- $this->charsetSeen = true;
- $this->appendRootDirective('@charset ' . $this->compileValue($child[1]) . ';', $out);
- }
break;
case Type::T_CUSTOM_PROPERTY:
@@ -2879,18 +3051,42 @@ class Compiler
case Type::T_EXTEND:
foreach ($child[1] as $sel) {
- $sel = $this->replaceSelfSelector($sel);
+ $replacedSel = $this->replaceSelfSelector($sel);
+
+ if ($replacedSel !== $sel) {
+ throw $this->error('Parent selectors aren\'t allowed here.');
+ }
+
$results = $this->evalSelectors([$sel]);
foreach ($results as $result) {
+ if (\count($result) !== 1) {
+ throw $this->error('complex selectors may not be extended.');
+ }
+
// only use the first one
- $result = current($result);
+ $result = $result[0];
$selectors = $out->selectors;
if (! $selectors && isset($child['selfParent'])) {
$selectors = $this->multiplySelectors($this->env, $child['selfParent']);
}
+ if (\count($result) > 1) {
+ $replacement = implode(', ', $result);
+ $fname = $this->getPrettyPath($this->sourceNames[$this->sourceIndex]);
+ $line = $this->sourceLine;
+
+ $message = <<<EOL
+on line $line of $fname:
+Compound selectors may no longer be extended.
+Consider `@extend $replacement` instead.
+See http://bit.ly/ExtendCompound for details.
+EOL;
+
+ $this->logger->warn($message);
+ }
+
$this->pushExtends($result, $selectors, $child);
}
}
@@ -2962,24 +3158,15 @@ class Compiler
case Type::T_FOR:
list(, $for) = $child;
- $start = $this->reduce($for->start, true);
- $end = $this->reduce($for->end, true);
-
- if (! $start instanceof Number) {
- throw $this->error('%s is not a number', $start[0]);
- }
+ $startNumber = $this->assertNumber($this->reduce($for->start, true));
+ $endNumber = $this->assertNumber($this->reduce($for->end, true));
- if (! $end instanceof Number) {
- throw $this->error('%s is not a number', $end[0]);
- }
+ $start = $this->assertInteger($startNumber);
- $start->assertSameUnitOrUnitless($end);
+ $numeratorUnits = $startNumber->getNumeratorUnits();
+ $denominatorUnits = $startNumber->getDenominatorUnits();
- $numeratorUnits = $start->getNumeratorUnits();
- $denominatorUnits = $start->getDenominatorUnits();
-
- $start = $start->getDimension();
- $end = $end->getDimension();
+ $end = $this->assertInteger($endNumber->coerce($numeratorUnits, $denominatorUnits));
$d = $start < $end ? 1 : -1;
@@ -3131,7 +3318,7 @@ class Compiler
$line = $this->sourceLine;
$value = $this->compileDebugValue($value);
- fwrite($this->stderr, "$fname:$line DEBUG: $value\n");
+ $this->logger->debug("$fname:$line DEBUG: $value");
break;
case Type::T_WARN:
@@ -3141,7 +3328,7 @@ class Compiler
$line = $this->sourceLine;
$value = $this->compileDebugValue($value);
- fwrite($this->stderr, "WARNING: $value\n on line $line of $fname\n\n");
+ $this->logger->warn("$value\n on line $line of $fname");
break;
case Type::T_ERROR:
@@ -3204,7 +3391,7 @@ class Compiler
*
* @return boolean
*/
- protected function isTruthy($value)
+ public function isTruthy($value)
{
return $value !== static::$false && $value !== static::$null;
}
@@ -3251,12 +3438,12 @@ class Compiler
* @param array|Number $value
* @param boolean $inExp
*
- * @return null|string|array|Number
+ * @return array|Number
*/
protected function reduce($value, $inExp = false)
{
- if (\is_null($value)) {
- return null;
+ if ($value instanceof Number) {
+ return $value;
}
switch ($value[0]) {
@@ -3357,6 +3544,14 @@ class Compiler
foreach ($value[2] as &$item) {
$item = $this->reduce($item);
}
+ unset($item);
+
+ if (isset($value[3]) && \is_array($value[3])) {
+ foreach ($value[3] as &$item) {
+ $item = $this->reduce($item);
+ }
+ unset($item);
+ }
return $value;
@@ -3373,7 +3568,7 @@ class Compiler
case Type::T_STRING:
foreach ($value[2] as &$item) {
- if (\is_array($item) || $item instanceof \ArrayAccess) {
+ if (\is_array($item) || $item instanceof Number) {
$item = $this->reduce($item);
}
}
@@ -3384,7 +3579,7 @@ class Compiler
$value[1] = $this->reduce($value[1]);
if ($inExp) {
- return $value[1];
+ return [Type::T_KEYWORD, $this->compileValue($value, false)];
}
return $value;
@@ -3395,7 +3590,7 @@ class Compiler
case Type::T_SELF:
$selfParent = ! empty($this->env->block->selfParent) ? $this->env->block->selfParent : null;
$selfSelector = $this->multiplySelectors($this->env, $selfParent);
- $selfSelector = $this->collapseSelectors($selfSelector, true);
+ $selfSelector = $this->collapseSelectorsAsList($selfSelector);
return $selfSelector;
@@ -3407,8 +3602,8 @@ class Compiler
/**
* Function caller
*
- * @param string $name
- * @param array $argValues
+ * @param string|array $functionReference
+ * @param array $argValues
*
* @return array|Number
*/
@@ -3455,7 +3650,7 @@ class Compiler
// special cases of css valid functions min/max
$name = strtolower($name);
- if (\in_array($name, ['min', 'max'])) {
+ if (\in_array($name, ['min', 'max']) && count($argValues) >= 1) {
$cssFunction = $this->cssValidArg(
[Type::T_FUNCTION_CALL, $name, $argValues],
['min', 'max', 'calc', 'env', 'var']
@@ -3477,8 +3672,19 @@ class Compiler
}
}
+ /**
+ * @param array|Number $arg
+ * @param string[] $allowed_function
+ * @param bool $inFunction
+ *
+ * @return array|Number|false
+ */
protected function cssValidArg($arg, $allowed_function = [], $inFunction = false)
{
+ if ($arg instanceof Number) {
+ return $this->stringifyFncallArgs($arg);
+ }
+
switch ($arg[0]) {
case Type::T_INTERPOLATE:
return [Type::T_KEYWORD, $this->CompileValue($arg)];
@@ -3523,9 +3729,6 @@ class Compiler
}
return $this->stringifyFncallArgs($arg);
- case Type::T_NUMBER:
- return $this->stringifyFncallArgs($arg);
-
case Type::T_LIST:
if (!$inFunction) {
return false;
@@ -3564,12 +3767,15 @@ class Compiler
/**
* Reformat fncall arguments to proper css function output
*
- * @param $arg
+ * @param array|Number $arg
*
- * @return array|\ArrayAccess|Number|string|null
+ * @return array|Number
*/
protected function stringifyFncallArgs($arg)
{
+ if ($arg instanceof Number) {
+ return $arg;
+ }
switch ($arg[0]) {
case Type::T_LIST:
@@ -3621,7 +3827,6 @@ class Compiler
// try to find a native lib function
$normalizedName = $this->normalizeName($name);
- $libName = null;
if (isset($this->userFunctions[$normalizedName])) {
// see if we can find a user function
@@ -3630,10 +3835,60 @@ class Compiler
return [Type::T_FUNCTION_REFERENCE, 'user', $name, $f, $prototype];
}
+ $lowercasedName = strtolower($normalizedName);
+
+ // 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)) {
$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;
+ }
+ }
+
+ if (\get_class($this) !== __CLASS__ && !isset($this->warnedChildFunctions[$libName])) {
+ $r = new \ReflectionMethod($this, $libName);
+ $declaringClass = $r->getDeclaringClass()->name;
+
+ $needsWarning = $this->warnedChildFunctions[$libName] = $declaringClass !== __CLASS__;
+
+ if ($needsWarning) {
+ if (method_exists(__CLASS__, $libName)) {
+ @trigger_error(sprintf('Overriding the "%s" core function by extending the Compiler is deprecated and will be unsupported in 2.0. Remove the "%s::%s" method.', $normalizedName, $declaringClass, $libName), E_USER_DEPRECATED);
+ } else {
+ @trigger_error(sprintf('Registering custom functions by extending the Compiler and using the lib* discovery mechanism is deprecated and will be removed in 2.0. Replace the "%s::%s" method with registering the "%s" function through "Compiler::registerFunction".', $declaringClass, $libName, $normalizedName), E_USER_DEPRECATED);
+ }
+ }
+ }
+
return [Type::T_FUNCTION_REFERENCE, 'native', $name, $f, $prototype];
}
@@ -3656,6 +3911,8 @@ class Compiler
/**
* Normalize value
*
+ * @internal
+ *
* @param array|Number $value
*
* @return array|Number
@@ -3664,6 +3921,10 @@ class Compiler
{
$value = $this->coerceForExpression($this->reduce($value));
+ if ($value instanceof Number) {
+ return $value;
+ }
+
switch ($value[0]) {
case Type::T_LIST:
$value = $this->extractInterpolation($value);
@@ -3864,7 +4125,7 @@ class Compiler
$fname = $this->getPrettyPath($this->sourceNames[$this->sourceIndex]);
$line = $this->sourceLine;
- fwrite($this->stderr, "DEPRECATION WARNING: $warning\n on line $line of $fname\n\n");
+ Warn::deprecation("$warning\n on line $line of $fname");
}
$out = [Type::T_COLOR];
@@ -4103,7 +4364,7 @@ class Compiler
*
* @api
*
- * @param mixed $thing
+ * @param bool $thing
*
* @return array
*/
@@ -4114,7 +4375,12 @@ class Compiler
/**
* Escape non printable chars in strings output as in dart-sass
+ *
+ * @internal
+ *
* @param string $string
+ * @param bool $inKeyword
+ *
* @return string
*/
public function escapeNonPrintableChars($string, $inKeyword = false)
@@ -4167,20 +4433,22 @@ class Compiler
*
* @api
*
- * @param array|Number|string $value
+ * @param array|Number $value
+ * @param bool $quote
*
* @return string
*/
- public function compileValue($value)
+ public function compileValue($value, $quote = true)
{
$value = $this->reduce($value);
+ if ($value instanceof Number) {
+ return $value->output($this);
+ }
+
switch ($value[0]) {
case Type::T_KEYWORD:
- if (is_string($value[1])) {
- $value[1] = $this->escapeNonPrintableChars($value[1], true);
- }
- return $value[1];
+ return $this->escapeNonPrintableChars($value[1], true);
case Type::T_COLOR:
// [1] - red component (either number for a %)
@@ -4232,13 +4500,10 @@ class Compiler
return $h;
- case Type::T_NUMBER:
- return $value->output($this);
-
case Type::T_STRING:
- $content = $this->compileStringContent($value);
+ $content = $this->compileStringContent($value, $quote);
- if ($value[1]) {
+ if ($value[1] && $quote) {
$content = str_replace('\\', '\\\\', $content);
$content = $this->escapeNonPrintableChars($content);
@@ -4263,7 +4528,7 @@ class Compiler
return $value[1] . $content . $value[1];
case Type::T_FUNCTION:
- $args = ! empty($value[2]) ? $this->compileValue($value[2]) : '';
+ $args = ! empty($value[2]) ? $this->compileValue($value[2], $quote) : '';
return "$value[1]($args)";
@@ -4276,7 +4541,7 @@ class Compiler
$value = $this->extractInterpolation($value);
if ($value[0] !== Type::T_LIST) {
- return $this->compileValue($value);
+ return $this->compileValue($value, $quote);
}
list(, $delim, $items) = $value;
@@ -4329,7 +4594,7 @@ class Compiler
$item[1] = $same_string_quote;
}
- $compiled = $this->compileValue($item);
+ $compiled = $this->compileValue($item, $quote);
if ($prefix_value && \strlen($compiled)) {
$compiled = $prefix_value . $compiled;
@@ -4346,7 +4611,7 @@ class Compiler
$filtered = [];
for ($i = 0, $s = \count($keys); $i < $s; $i++) {
- $filtered[$this->compileValue($keys[$i])] = $this->compileValue($values[$i]);
+ $filtered[$this->compileValue($keys[$i], $quote)] = $this->compileValue($values[$i], $quote);
}
array_walk($filtered, function (&$value, $key) {
@@ -4367,7 +4632,7 @@ class Compiler
}
$left = \count($left[2]) > 0
- ? $this->compileValue($left) . $delim . $whiteLeft
+ ? $this->compileValue($left, $quote) . $delim . $whiteLeft
: '';
$delim = $right[1];
@@ -4377,14 +4642,18 @@ class Compiler
}
$right = \count($right[2]) > 0 ?
- $whiteRight . $delim . $this->compileValue($right) : '';
+ $whiteRight . $delim . $this->compileValue($right, $quote) : '';
- return $left . $this->compileValue($interpolate) . $right;
+ return $left . $this->compileValue($interpolate, $quote) . $right;
case Type::T_INTERPOLATE:
// strip quotes if it's a string
$reduced = $this->reduce($value[1]);
+ if ($reduced instanceof Number) {
+ return $this->compileValue($reduced, $quote);
+ }
+
switch ($reduced[0]) {
case Type::T_LIST:
$reduced = $this->extractInterpolation($reduced);
@@ -4406,14 +4675,12 @@ class Compiler
continue;
}
- $temp = $this->compileValue([Type::T_KEYWORD, $item]);
-
- if ($temp[0] === Type::T_STRING) {
- $filtered[] = $this->compileStringContent($temp);
- } elseif ($temp[0] === Type::T_KEYWORD) {
- $filtered[] = $temp[1];
+ if ($item[0] === Type::T_STRING) {
+ $filtered[] = $this->compileStringContent($item, $quote);
+ } elseif ($item[0] === Type::T_KEYWORD) {
+ $filtered[] = $item[1];
} else {
- $filtered[] = $this->compileValue($temp);
+ $filtered[] = $this->compileValue($item, $quote);
}
}
@@ -4428,7 +4695,7 @@ class Compiler
$reduced = [Type::T_KEYWORD, ''];
}
- return $this->compileValue($reduced);
+ return $this->compileValue($reduced, $quote);
case Type::T_NULL:
return 'null';
@@ -4442,14 +4709,18 @@ class Compiler
}
/**
- * @param array $value
+ * @param array|Number $value
*
- * @return array|string
+ * @return string
*/
protected function compileDebugValue($value)
{
$value = $this->reduce($value, true);
+ if ($value instanceof Number) {
+ return $this->compileValue($value);
+ }
+
switch ($value[0]) {
case Type::T_STRING:
return $this->compileStringContent($value);
@@ -4465,26 +4736,50 @@ class Compiler
* @param array $list
*
* @return string
+ *
+ * @deprecated
*/
protected function flattenList($list)
{
+ @trigger_error(sprintf('The "%s" method is deprecated.', __METHOD__), E_USER_DEPRECATED);
+
return $this->compileValue($list);
}
/**
+ * Gets the text of a Sass string
+ *
+ * Calling this method on anything else than a SassString is unsupported. Use {@see assertString} first
+ * to ensure that the value is indeed a string.
+ *
+ * @param array $value
+ *
+ * @return string
+ */
+ public function getStringText(array $value)
+ {
+ if ($value[0] !== Type::T_STRING) {
+ throw new \InvalidArgumentException('The argument is not a sass string. Did you forgot to use "assertString"?');
+ }
+
+ return $this->compileStringContent($value);
+ }
+
+ /**
* Compile string content
*
* @param array $string
+ * @param bool $quote
*
* @return string
*/
- protected function compileStringContent($string)
+ protected function compileStringContent($string, $quote = true)
{
$parts = [];
foreach ($string[2] as $part) {
- if (\is_array($part) || $part instanceof \ArrayAccess) {
- $parts[] = $this->compileValue($part);
+ if (\is_array($part) || $part instanceof Number) {
+ $parts[] = $this->compileValue($part, $quote);
} else {
$parts[] = $part;
}
@@ -4898,7 +5193,7 @@ class Compiler
/**
* Get variable
*
- * @api
+ * @internal
*
* @param string $name
* @param boolean $shouldThrow
@@ -4997,7 +5292,7 @@ class Compiler
$name = substr($name, 1);
}
- if (! $parser->parseValue($strValue, $value)) {
+ if (!\is_string($strValue) || ! $parser->parseValue($strValue, $value)) {
$value = $this->coerceValue($strValue);
}
@@ -5006,6 +5301,43 @@ class Compiler
}
/**
+ * Replaces variables.
+ *
+ * @param array<string, mixed> $variables
+ *
+ * @return void
+ */
+ public function replaceVariables(array $variables)
+ {
+ $this->registeredVars = [];
+ $this->addVariables($variables);
+ }
+
+ /**
+ * Replaces variables.
+ *
+ * @param array<string, mixed> $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
@@ -5013,10 +5345,14 @@ class Compiler
* @param array $variables
*
* @return void
+ *
+ * @deprecated Use "addVariables" or "replaceVariables" instead.
*/
public function setVariables(array $variables)
{
- $this->registeredVars = array_merge($this->registeredVars, $variables);
+ @trigger_error('The method "setVariables" of the Compiler is deprecated. Use the "addVariables" method for the equivalent behavior or "replaceVariables" if merging with previous variables was not desired.');
+
+ $this->addVariables($variables);
}
/**
@@ -5048,15 +5384,15 @@ class Compiler
/**
* Adds to list of parsed files
*
- * @api
+ * @internal
*
- * @param string $path
+ * @param string|null $path
*
* @return void
*/
public function addParsedFile($path)
{
- if (isset($path) && is_file($path)) {
+ if (! \is_null($path) && is_file($path)) {
$this->parsedFiles[realpath($path)] = filemtime($path);
}
}
@@ -5064,12 +5400,12 @@ class Compiler
/**
* Returns list of parsed files
*
- * @api
- *
- * @return array
+ * @deprecated
+ * @return array<string, int>
*/
public function getParsedFiles()
{
+ @trigger_error('The method "getParsedFiles" of the Compiler is deprecated. Use the "getIncludedFiles" method on the CompilationResult instance returned by compileString() instead. Be careful that the signature of the method is different.', E_USER_DEPRECATED);
return $this->parsedFiles;
}
@@ -5108,7 +5444,7 @@ class Compiler
$this->legacyCwdImportPath = \count($actualImportPaths) !== \count($paths);
if ($this->legacyCwdImportPath) {
- @trigger_error('Passing an empty string in the import paths to refer to the current working directory is deprecated. If that\'s the intended behavior, the value of "getcwd()" should be used directly instead. If this was used for resolving relative imports of the input alongside "chdir" with the source directory, the path of the input file should be passed to "compile()" instead.', E_USER_DEPRECATED);
+ @trigger_error('Passing an empty string in the import paths to refer to the current working directory is deprecated. If that\'s the intended behavior, the value of "getcwd()" should be used directly instead. If this was used for resolving relative imports of the input alongside "chdir" with the source directory, the path of the input file should be passed to "compileString()" instead.', E_USER_DEPRECATED);
}
$this->importPaths = $actualImportPaths;
@@ -5197,6 +5533,25 @@ class Compiler
}
/**
+ * 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;
+ }
+
+ /**
* Enable/disable source maps
*
* @api
@@ -5219,6 +5574,8 @@ class Compiler
*
* @param array $sourceMapOptions
*
+ * @phpstan-param array{sourceRoot?: string, sourceMapFilename?: string|null, sourceMapURL?: string|null, sourceMapWriteTo?: string|null, outputSourceFiles?: bool, sourceMapRootpath?: string, sourceMapBasepath?: string} $sourceMapOptions
+ *
* @return void
*/
public function setSourceMapOptions($sourceMapOptions)
@@ -5231,15 +5588,23 @@ class Compiler
*
* @api
*
- * @param string $name
- * @param callable $func
- * @param array|null $prototype
+ * @param string $name
+ * @param callable $callback
+ * @param string[]|null $argumentDeclaration
*
* @return void
*/
- public function registerFunction($name, $func, $prototype = null)
+ public function registerFunction($name, $callback, $argumentDeclaration = null)
{
- $this->userFunctions[$this->normalizeName($name)] = [$func, $prototype];
+ if (self::isNativeFunction($name)) {
+ @trigger_error(sprintf('The "%s" function is a core sass function. Overriding it with a custom implementation through "%s" is deprecated and won\'t be supported in ScssPhp 2.0 anymore.', $name, __METHOD__), E_USER_DEPRECATED);
+ }
+
+ if ($argumentDeclaration === null) {
+ @trigger_error('Omitting the argument declaration when registering custom function is deprecated and won\'t be supported in ScssPhp 2.0 anymore.', E_USER_DEPRECATED);
+ }
+
+ $this->userFunctions[$this->normalizeName($name)] = [$callback, $argumentDeclaration];
}
/**
@@ -5288,6 +5653,15 @@ class Compiler
// see if tree is cached
$realPath = realpath($path);
+ if (substr($path, -5) === '.sass') {
+ $this->sourceIndex = \count($this->sourceNames);
+ $this->sourceNames[] = $path;
+ $this->sourceLine = 1;
+ $this->sourceColumn = 1;
+
+ throw $this->error('The Sass indented syntax is not implemented.');
+ }
+
if (isset($this->importCache[$realPath])) {
$this->handleImportLoop($realPath);
@@ -5309,19 +5683,51 @@ class Compiler
}
/**
- * Return the file path for an import url if it exists
+ * Save the imported files with their resolving path context
*
- * @api
+ * @param string|null $currentDirectory
+ * @param string $path
+ * @param string $filePath
+ *
+ * @return void
+ */
+ private function registerImport($currentDirectory, $path, $filePath)
+ {
+ $this->resolvedImports[] = ['currentDir' => $currentDirectory, 'path' => $path, 'filePath' => $filePath];
+ }
+
+ /**
+ * Detects whether the import is a CSS import.
+ *
+ * For legacy reasons, custom importers are called for those, allowing them
+ * to replace them with an actual Sass import. However this behavior is
+ * deprecated. Custom importers are expected to return null when they receive
+ * a CSS import.
*
* @param string $url
*
+ * @return bool
+ */
+ public static function isCssImport($url)
+ {
+ return 1 === preg_match('~\.css$|^https?://|^//~', $url);
+ }
+
+ /**
+ * Return the file path for an import url if it exists
+ *
+ * @internal
+ *
+ * @param string $url
+ * @param string|null $currentDir
+ *
* @return string|null
*/
- public function findImport($url)
+ public function findImport($url, $currentDir = null)
{
- // for "normal" scss imports (ignore vanilla css and external requests)
+ // Vanilla css and external requests. These are not meant to be Sass imports.
// Callback importers are still called for BC.
- if (preg_match('~\.css$|^https?://|^//~', $url)) {
+ if (self::isCssImport($url)) {
foreach ($this->importPaths as $dir) {
if (\is_string($dir)) {
continue;
@@ -5332,6 +5738,24 @@ class Compiler
$file = \call_user_func($dir, $url);
if (! \is_null($file)) {
+ if (\is_array($dir)) {
+ $callableDescription = (\is_object($dir[0]) ? \get_class($dir[0]) : $dir[0]).'::'.$dir[1];
+ } elseif ($dir instanceof \Closure) {
+ $r = new \ReflectionFunction($dir);
+ if (false !== strpos($r->name, '{closure}')) {
+ $callableDescription = sprintf('closure{%s:%s}', $r->getFileName(), $r->getStartLine());
+ } elseif ($class = $r->getClosureScopeClass()) {
+ $callableDescription = $class->name.'::'.$r->name;
+ } else {
+ $callableDescription = $r->name;
+ }
+ } elseif (\is_object($dir)) {
+ $callableDescription = \get_class($dir) . '::__invoke';
+ } else {
+ $callableDescription = 'callable'; // Fallback if we don't have a dedicated description
+ }
+ @trigger_error(sprintf('Returning a file to import for CSS or external references in custom importer callables is deprecated and will not be supported anymore in ScssPhp 2.0. This behavior is not compliant with the Sass specification. Update your "%s" importer.', $callableDescription), E_USER_DEPRECATED);
+
return $file;
}
}
@@ -5339,8 +5763,8 @@ class Compiler
return null;
}
- if (!\is_null($this->currentDirectory)) {
- $relativePath = $this->resolveImportPath($url, $this->currentDirectory);
+ if (!\is_null($currentDir)) {
+ $relativePath = $this->resolveImportPath($url, $currentDir);
if (!\is_null($relativePath)) {
return $relativePath;
@@ -5368,7 +5792,7 @@ class Compiler
$path = $this->resolveImportPath($url, getcwd());
if (!\is_null($path)) {
- @trigger_error('Resolving imports relatively to the current working directory is deprecated. If that\'s the intended behavior, the value of "getcwd()" should be added as an import path explicitly instead. If this was used for resolving relative imports of the input alongside "chdir" with the source directory, the path of the input file should be passed to "compile()" instead.', E_USER_DEPRECATED);
+ @trigger_error('Resolving imports relatively to the current working directory is deprecated. If that\'s the intended behavior, the value of "getcwd()" should be added as an import path explicitly instead. If this was used for resolving relative imports of the input alongside "chdir" with the source directory, the path of the input file should be passed to "compileString()" instead.', E_USER_DEPRECATED);
return $path;
}
@@ -5387,7 +5811,7 @@ class Compiler
{
$path = Path::join($baseDir, $url);
- $hasExtension = preg_match('/.scss$/', $url);
+ $hasExtension = preg_match('/.s[ac]ss$/', $url);
if ($hasExtension) {
return $this->checkImportPathConflicts($this->tryImportPath($path));
@@ -5433,7 +5857,10 @@ class Compiler
*/
private function tryImportPathWithExtensions($path)
{
- $result = $this->tryImportPath($path.'.scss');
+ $result = array_merge(
+ $this->tryImportPath($path.'.sass'),
+ $this->tryImportPath($path.'.scss')
+ );
if ($result) {
return $result;
@@ -5479,12 +5906,16 @@ class Compiler
}
/**
- * @param string $path
+ * @param string|null $path
*
* @return string
*/
private function getPrettyPath($path)
{
+ if ($path === null) {
+ return '(unknown file)';
+ }
+
$normalizedPath = $path;
$normalizedRootDirectory = $this->rootDirectory.'/';
@@ -5494,7 +5925,7 @@ class Compiler
}
if (0 === strpos($normalizedPath, $normalizedRootDirectory)) {
- return substr($normalizedPath, \strlen($normalizedRootDirectory));
+ return substr($path, \strlen($normalizedRootDirectory));
}
return $path;
@@ -5505,12 +5936,20 @@ class Compiler
*
* @api
*
- * @param string $encoding
+ * @param string|null $encoding
*
* @return void
+ *
+ * @deprecated Non-compliant support for other encodings than UTF-8 is deprecated.
*/
public function setEncoding($encoding)
{
+ if (!$encoding || strtolower($encoding) === 'utf-8') {
+ @trigger_error(sprintf('The "%s" method is deprecated.', __METHOD__), E_USER_DEPRECATED);
+ } else {
+ @trigger_error(sprintf('The "%s" method is deprecated. Parsing will only support UTF-8 in ScssPhp 2.0. The non-UTF-8 parsing of ScssPhp 1.x is not spec compliant.', __METHOD__), E_USER_DEPRECATED);
+ }
+
$this->encoding = $encoding;
}
@@ -5538,9 +5977,13 @@ class Compiler
* @api
*
* @return array
+ *
+ * @deprecated
*/
public function getSourcePosition()
{
+ @trigger_error(sprintf('The "%s" method is deprecated.', __METHOD__), E_USER_DEPRECATED);
+
$sourceFile = isset($this->sourceNames[$this->sourceIndex]) ? $this->sourceNames[$this->sourceIndex] : '';
return [$sourceFile, $this->sourceLine, $this->sourceColumn];
@@ -5570,7 +6013,7 @@ class Compiler
/**
* Build an error (exception)
*
- * @api
+ * @internal
*
* @param string $msg Message with optional sprintf()-style vararg parameters
*
@@ -5583,23 +6026,35 @@ class Compiler
}
if (! $this->ignoreCallStackMessage) {
- $line = $this->sourceLine;
- $column = $this->sourceColumn;
+ $msg = $this->addLocationToMessage($msg);
+ }
- $loc = isset($this->sourceNames[$this->sourceIndex])
- ? $this->getPrettyPath($this->sourceNames[$this->sourceIndex]) . " on line $line, at column $column"
- : "line: $line, column: $column";
+ return new CompilerException($msg);
+ }
- $msg = "$msg: $loc";
+ /**
+ * @param string $msg
+ *
+ * @return string
+ */
+ private function addLocationToMessage($msg)
+ {
+ $line = $this->sourceLine;
+ $column = $this->sourceColumn;
- $callStackMsg = $this->callStackMessage();
+ $loc = isset($this->sourceNames[$this->sourceIndex])
+ ? $this->getPrettyPath($this->sourceNames[$this->sourceIndex]) . " on line $line, at column $column"
+ : "line: $line, column: $column";
- if ($callStackMsg) {
- $msg .= "\nCall Stack:\n" . $callStackMsg;
- }
+ $msg = "$msg: $loc";
+
+ $callStackMsg = $this->callStackMessage();
+
+ if ($callStackMsg) {
+ $msg .= "\nCall Stack:\n" . $callStackMsg;
}
- return new CompilerException($msg);
+ return $msg;
}
/**
@@ -5607,9 +6062,13 @@ class Compiler
* @param array $ExpectedArgs
* @param int $nbActual
* @return CompilerException
+ *
+ * @deprecated
*/
public function errorArgsNumber($functionName, $ExpectedArgs, $nbActual)
{
+ @trigger_error(sprintf('The "%s" method is deprecated.', __METHOD__), E_USER_DEPRECATED);
+
$nbExpected = \count($ExpectedArgs);
if ($nbActual > $nbExpected) {
@@ -5638,8 +6097,8 @@ class Compiler
/**
* Beautify call stack for output
*
- * @param boolean $all
- * @param null $limit
+ * @param boolean $all
+ * @param int|null $limit
*
* @return string
*/
@@ -5685,6 +6144,10 @@ class Compiler
$file = $this->sourceNames[$env->block->sourceIndex];
+ if ($file === null) {
+ continue;
+ }
+
if (realpath($file) === $name) {
throw $this->error('An @import loop has been found: %s imports %s', $file, basename($file));
}
@@ -5697,7 +6160,7 @@ class Compiler
* @param Object $func
* @param array $argValues
*
- * @return array
+ * @return array|Number
*/
protected function callScssFunction($func, $argValues)
{
@@ -5737,7 +6200,7 @@ class Compiler
* Call built-in and registered (PHP) functions
*
* @param string $name
- * @param string|array $function
+ * @param callable $function
* @param array $prototype
* @param array $args
*
@@ -5754,14 +6217,10 @@ class Compiler
@list($sorted, $kwargs) = $sorted_kwargs;
if ($name !== 'if') {
- $inExp = true;
-
- if ($name === 'join') {
- $inExp = false;
- }
-
foreach ($sorted as &$val) {
- $val = $this->reduce($val, $inExp);
+ if ($val !== null) {
+ $val = $this->reduce($val, true);
+ }
}
}
@@ -5771,6 +6230,12 @@ class Compiler
return null;
}
+ if (\is_array($returnValue) || $returnValue instanceof Number) {
+ return $returnValue;
+ }
+
+ @trigger_error(sprintf('Returning a PHP value from the "%s" custom function is deprecated. A sass value must be returned instead.', $name), E_USER_DEPRECATED);
+
return $this->coerceValue($returnValue);
}
@@ -5789,7 +6254,11 @@ class Compiler
/**
* Normalize native function name
+ *
+ * @internal
+ *
* @param string $name
+ *
* @return string
*/
public static function normalizeNativeFunctionName($name)
@@ -5807,7 +6276,11 @@ class Compiler
/**
* Check if a function is a native built-in scss function, for css parsing
+ *
+ * @internal
+ *
* @param string $name
+ *
* @return bool
*/
public static function isNativeFunction($name)
@@ -5819,7 +6292,7 @@ class Compiler
* Sorts keyword arguments
*
* @param string $functionName
- * @param array $prototypes
+ * @param array|null $prototypes
* @param array $args
*
* @return array|null
@@ -5855,229 +6328,352 @@ class Compiler
// notation 100 127 255 / 0 is in fact a simple list of 4 values
foreach ($args as $k => $arg) {
if ($arg[1][0] === Type::T_LIST && \count($arg[1][2]) === 3) {
- $last = end($arg[1][2]);
-
- if ($last[0] === Type::T_EXPRESSION && $last[1] === '/') {
- array_pop($arg[1][2]);
- $arg[1][2][] = $last[2];
- $arg[1][2][] = $last[3];
- $args[$k] = $arg;
- }
+ $args[$k][1][2] = $this->extractSlashAlphaInColorFunction($arg[1][2]);
}
}
}
- $finalArgs = [];
+ list($positionalArgs, $namedArgs, $names, $separator, $hasSplat) = $this->evaluateArguments($args, false);
if (! \is_array(reset($prototypes))) {
$prototypes = [$prototypes];
}
+ $parsedPrototypes = array_map([$this, 'parseFunctionPrototype'], $prototypes);
+ assert(!empty($parsedPrototypes));
+ $matchedPrototype = $this->selectFunctionPrototype($parsedPrototypes, \count($positionalArgs), $names);
+
+ $this->verifyPrototype($matchedPrototype, \count($positionalArgs), $names, $hasSplat);
+
+ $vars = $this->applyArgumentsToDeclaration($matchedPrototype, $positionalArgs, $namedArgs, $separator);
+
+ $finalArgs = [];
$keyArgs = [];
- // trying each prototypes
- $prototypeHasMatch = false;
- $exceptionMessage = '';
+ foreach ($matchedPrototype['arguments'] as $argument) {
+ list($normalizedName, $originalName, $default) = $argument;
- foreach ($prototypes as $prototype) {
- $argDef = [];
+ if (isset($vars[$normalizedName])) {
+ $value = $vars[$normalizedName];
+ } else {
+ $value = $default;
+ }
- foreach ($prototype as $i => $p) {
- $default = null;
- $p = explode(':', $p, 2);
- $name = array_shift($p);
+ // special null value as default: translate to real null here
+ if ($value === [Type::T_KEYWORD, 'null']) {
+ $value = null;
+ }
- if (\count($p)) {
- $p = trim(reset($p));
+ $finalArgs[] = $value;
+ $keyArgs[$originalName] = $value;
+ }
- if ($p === 'null') {
- // differentiate this null from the static::$null
- $default = [Type::T_KEYWORD, 'null'];
- } else {
- if (\is_null($parser)) {
- $parser = $this->parserFactory(__METHOD__);
- }
+ if ($matchedPrototype['rest_argument'] !== null) {
+ $value = $vars[$matchedPrototype['rest_argument']];
- $parser->parseValue($p, $default);
- }
- }
+ $finalArgs[] = $value;
+ $keyArgs[$matchedPrototype['rest_argument']] = $value;
+ }
- $isVariable = false;
+ return [$finalArgs, $keyArgs];
+ }
- if (substr($name, -3) === '...') {
- $isVariable = true;
- $name = substr($name, 0, -3);
- }
+ /**
+ * Parses a function prototype to the internal representation of arguments.
+ *
+ * The input is an array of strings describing each argument, as supported
+ * in {@see registerFunction}. Argument names don't include the `$`.
+ * The output contains the list of positional argument, with their normalized
+ * name (underscores are replaced by dashes), their original name (to be used
+ * in case of error reporting) and their default value. The output also contains
+ * the normalized name of the rest argument, or null if the function prototype
+ * is not variadic.
+ *
+ * @param string[] $prototype
+ *
+ * @return array
+ * @phpstan-return array{arguments: list<array{0: string, 1: string, 2: array|Number|null}>, rest_argument: string|null}
+ */
+ private function parseFunctionPrototype(array $prototype)
+ {
+ static $parser = null;
- $argDef[] = [$name, $default, $isVariable];
+ $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.');
}
- $ignoreCallStackMessage = $this->ignoreCallStackMessage;
- $this->ignoreCallStackMessage = true;
+ $default = null;
+ $p = explode(':', $p, 2);
+ $name = str_replace('_', '-', $p[0]);
- try {
- if (\count($args) > \count($argDef)) {
- $lastDef = end($argDef);
+ if (isset($p[1])) {
+ $defaultSource = trim($p[1]);
- // check that last arg is not a ...
- if (empty($lastDef[2])) {
- throw $this->errorArgsNumber($functionName, $argDef, \count($args));
+ 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);
}
- $vars = $this->applyArguments($argDef, $args, false, false);
+ }
- // ensure all args are populated
- foreach ($prototype as $i => $p) {
- $name = explode(':', $p)[0];
+ if (substr($name, -3) === '...') {
+ $restArgument = substr($name, 0, -3);
+ } else {
+ $arguments[] = [$name, $p[0], $default];
+ }
+ }
- if (! isset($finalArgs[$i])) {
- $finalArgs[$i] = null;
- }
- }
+ return [
+ 'arguments' => $arguments,
+ 'rest_argument' => $restArgument,
+ ];
+ }
- // apply positional args
- foreach (array_values($vars) as $i => $val) {
- $finalArgs[$i] = $val;
- }
+ /**
+ * 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<string, string> $names A set of names, as both keys and values
+ *
+ * @return array
+ *
+ * @phpstan-param non-empty-list<array{arguments: list<array{0: string, 1: string, 2: array|Number|null}>, rest_argument: string|null}> $prototypes
+ * @phpstan-return array{arguments: list<array{0: string, 1: string, 2: array|Number|null}>, rest_argument: string|null}
+ */
+ private function selectFunctionPrototype(array $prototypes, $positional, array $names)
+ {
+ $fuzzyMatch = null;
+ $minMismatchDistance = null;
- $keyArgs = array_merge($keyArgs, $vars);
- $prototypeHasMatch = true;
+ foreach ($prototypes as $prototype) {
+ // Ideally, find an exact match.
+ if ($this->checkPrototypeMatches($prototype, $positional, $names)) {
+ return $prototype;
+ }
- // overwrite positional args with keyword args
- foreach ($prototype as $i => $p) {
- $name = explode(':', $p)[0];
+ $mismatchDistance = \count($prototype['arguments']) - $positional;
- if (isset($keyArgs[$name])) {
- $finalArgs[$i] = $keyArgs[$name];
- }
+ if ($minMismatchDistance !== null) {
+ if (abs($mismatchDistance) > abs($minMismatchDistance)) {
+ continue;
+ }
- // special null value as default: translate to real null here
- if ($finalArgs[$i] === [Type::T_KEYWORD, 'null']) {
- $finalArgs[$i] = null;
- }
+ // If two overloads have the same mismatch distance, favor the overload
+ // that has more arguments.
+ if (abs($mismatchDistance) === abs($minMismatchDistance) && $mismatchDistance < 0) {
+ continue;
}
- // should we break if this prototype seems fulfilled?
- } catch (CompilerException $e) {
- $exceptionMessage = $e->getMessage();
}
- $this->ignoreCallStackMessage = $ignoreCallStackMessage;
+
+ $minMismatchDistance = $mismatchDistance;
+ $fuzzyMatch = $prototype;
}
- if ($exceptionMessage && ! $prototypeHasMatch) {
- if (\in_array($functionName, ['libRgb', 'libRgba', 'libHsl', 'libHsla'])) {
- // if var() or calc() is used as an argument, return as a css function
- foreach ($args as $arg) {
- if ($arg[1][0] === Type::T_FUNCTION_CALL && in_array($arg[1][1], ['var'])) {
- return null;
- }
+ return $fuzzyMatch;
+ }
+
+ /**
+ * Checks whether the argument invocation matches the callable prototype.
+ *
+ * The rules are similar to {@see verifyPrototype}. The boolean return value
+ * avoids the overhead of building and catching exceptions when the reason of
+ * not matching the prototype does not need to be known.
+ *
+ * @param array $prototype
+ * @param int $positional
+ * @param array<string, string> $names
+ *
+ * @return bool
+ *
+ * @phpstan-param array{arguments: list<array{0: string, 1: string, 2: array|Number|null}>, 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;
}
+ }
- throw $this->error($exceptionMessage);
+ if ($prototype['rest_argument'] !== null) {
+ return true;
}
- return [$finalArgs, $keyArgs];
+ if ($positional > \count($prototype['arguments'])) {
+ return false;
+ }
+
+ if ($nameUsed < \count($names)) {
+ return false;
+ }
+
+ return true;
}
/**
- * Apply argument values per definition
+ * Verifies that the argument invocation is valid for the callable prototype.
*
- * @param array $argDef
- * @param array $argValues
- * @param boolean $storeInEnv
- * @param boolean $reduce
- * only used if $storeInEnv = false
+ * @param array $prototype
+ * @param int $positional
+ * @param array<string, string> $names
+ * @param bool $hasSplat
*
- * @return array
+ * @return void
*
- * @throws \Exception
+ * @throws SassScriptException
+ *
+ * @phpstan-param array{arguments: list<array{0: string, 1: string, 2: array|Number|null}>, rest_argument: string|null} $prototype
*/
- protected function applyArguments($argDef, $argValues, $storeInEnv = true, $reduce = true)
+ private function verifyPrototype(array $prototype, $positional, array $names, $hasSplat)
{
- $output = [];
+ $nameUsed = 0;
- if (\is_array($argValues) && \count($argValues) && end($argValues) === static::$null) {
- array_pop($argValues);
- }
+ foreach ($prototype['arguments'] as $i => $argument) {
+ list ($name, $originalName, $default) = $argument;
- if ($storeInEnv) {
- $storeEnv = $this->getStoreEnv();
+ 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));
+ }
+ }
- $env = new Environment();
- $env->store = $storeEnv->store;
+ if ($prototype['rest_argument'] !== null) {
+ return;
}
- $hasVariable = false;
- $args = [];
+ 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);
+ }
- foreach ($argDef as $i => $arg) {
- list($name, $default, $isVariable) = $argDef[$i];
+ $message = $this->addLocationToMessage($message);
+ $message .= "\nThis will be an error in future versions of Sass.";
+ $this->logger->warn($message, true);
+ }
- $args[$name] = [$i, $name, $default, $isVariable];
- $hasVariable |= $isVariable;
+ 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<array|Number>, 1: array<string, array|Number>, 2: array<string, string>, 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 = [];
- $deferredKeywordArgs = [];
- $deferredNamedKeywordArgs = [];
- $remaining = [];
- $hasKeywordArgument = false;
+ $splatSeparator = null;
+ $keywordArgs = [];
+ $names = [];
+ $positionalArgs = [];
+ $hasKeywordArgument = false;
+ $hasSplat = false;
- // assign the keyword args
- foreach ((array) $argValues as $arg) {
- if (! empty($arg[0])) {
+ foreach ($args as $arg) {
+ if (!empty($arg[0])) {
$hasKeywordArgument = true;
- $name = $arg[0][1];
+ assert(\is_string($arg[0][1]));
+ $name = str_replace('_', '-', $arg[0][1]);
- if (! isset($args[$name])) {
- foreach (array_keys($args) as $an) {
- if (str_replace('_', '-', $an) === str_replace('_', '-', $name)) {
- $name = $an;
- break;
- }
- }
+ if (isset($keywordArgs[$name])) {
+ throw new SassScriptException(sprintf('Duplicate named argument $%s.', $arg[0][1]));
}
- if (! isset($args[$name]) || $args[$name][3]) {
- if ($hasVariable) {
- $deferredNamedKeywordArgs[$name] = $arg[1];
- } else {
- throw $this->error("Mixin or function doesn't have an argument named $%s.", $arg[0][1]);
- }
- } elseif ($args[$name][0] < \count($remaining)) {
- throw $this->error("The argument $%s was passed both by position and by name.", $arg[0][1]);
- } else {
- $keywordArgs[$name] = $arg[1];
- }
+ $keywordArgs[$name] = $this->maybeReduce($reduce, $arg[1]);
+ $names[$name] = $name;
} elseif (! empty($arg[2])) {
// $arg[2] means a var followed by ... in the arg ($list... )
$val = $this->reduce($arg[1], true);
+ $hasSplat = true;
if ($val[0] === Type::T_LIST) {
- foreach ($val[2] as $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;
- }
- }
- }
+ foreach ($val[2] as $item) {
+ if (\is_null($splatSeparator)) {
+ $splatSeparator = $val[1];
+ }
- if ($hasVariable) {
- $deferredKeywordArgs[$name] = $item;
- } else {
- $keywordArgs[$name] = $item;
- }
- } else {
- 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));
}
- $remaining[] = $item;
+ $keywordArgs[$normalizedName] = $this->maybeReduce($reduce, $item);
+ $names[$normalizedName] = $normalizedName;
+ $hasKeywordArgument = true;
}
}
} elseif ($val[0] === Type::T_MAP) {
@@ -6086,72 +6682,122 @@ class Compiler
$item = $val[2][$i];
if (! is_numeric($name)) {
- if (! isset($args[$name])) {
- foreach (array_keys($args) as $an) {
- if (str_replace('_', '-', $an) === str_replace('_', '-', $name)) {
- $name = $an;
- break;
- }
- }
- }
+ $normalizedName = str_replace('_', '-', $name);
- if ($hasVariable) {
- $deferredKeywordArgs[$name] = $item;
- } else {
- $keywordArgs[$name] = $item;
+ if (isset($keywordArgs[$normalizedName])) {
+ throw new SassScriptException(sprintf('Duplicate named argument $%s.', $name));
}
+
+ $keywordArgs[$normalizedName] = $this->maybeReduce($reduce, $item);
+ $names[$normalizedName] = $normalizedName;
+ $hasKeywordArgument = true;
} else {
if (\is_null($splatSeparator)) {
$splatSeparator = $val[1];
}
- $remaining[] = $item;
+ $positionalArgs[] = $this->maybeReduce($reduce, $item);
}
}
- } else {
- $remaining[] = $val;
+ } elseif ($val[0] !== Type::T_NULL) { // values other than null are treated a single-element lists, while null is the empty list
+ $positionalArgs[] = $this->maybeReduce($reduce, $val);
}
} elseif ($hasKeywordArgument) {
- throw $this->error('Positional arguments must come before keyword arguments.');
+ throw new SassScriptException('Positional arguments must come before keyword arguments.');
} else {
- $remaining[] = $arg[1];
+ $positionalArgs[] = $this->maybeReduce($reduce, $arg[1]);
}
}
- foreach ($args as $arg) {
- list($i, $name, $default, $isVariable) = $arg;
+ 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 boolean $storeInEnv
+ * @param boolean $reduce
+ * only used if $storeInEnv = false
+ *
+ * @return array<string, array|Number>
+ *
+ * @phpstan-param list<array{0: string, 1: array|Number|null, 2: bool}> $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 $i => $arg) {
+ list($name, $default, $isVariable) = $arg;
+ $normalizedName = str_replace('_', '-', $name);
if ($isVariable) {
- // only if more than one arg : can not be passed as position and value
- // see https://github.com/sass/libsass/issues/2927
- if (count($args) > 1) {
- if (isset($remaining[$i]) && isset($deferredNamedKeywordArgs[$name])) {
- throw $this->error("The argument $%s was passed both by position and by name.", $name);
- }
- }
+ $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);
- foreach ($deferredKeywordArgs as $itemName => $item) {
- $val[2][$itemName] = $item;
- }
+ $vars = $this->applyArgumentsToDeclaration($prototype, $positionalArgs, $namedArgs, $splatSeparator);
- foreach ($deferredNamedKeywordArgs as $itemName => $item) {
- $val[2][$itemName] = $item;
- }
- } elseif (isset($remaining[$i])) {
- $val = $remaining[$i];
- } elseif (isset($keywordArgs[$name])) {
- $val = $keywordArgs[$name];
- } elseif (! empty($default)) {
+ foreach ($prototype['arguments'] as $argument) {
+ list($normalizedName, $name) = $argument;
+
+ if (!isset($vars[$normalizedName])) {
continue;
+ }
+
+ $val = $vars[$normalizedName];
+
+ if ($storeInEnv) {
+ $this->set($name, $this->reduce($val, true), true, $env);
} else {
- throw $this->error("Missing argument $name");
+ $output[$name] = ($reduce ? $this->reduce($val, true) : $val);
}
+ }
+
+ if ($prototype['rest_argument'] !== null) {
+ assert($originalRestArgumentName !== null);
+ $name = $originalRestArgumentName;
+ $val = $vars[$prototype['rest_argument']];
if ($storeInEnv) {
$this->set($name, $this->reduce($val, true), true, $env);
@@ -6164,12 +6810,13 @@ class Compiler
$storeEnv->store = $env->store;
}
- foreach ($args as $arg) {
- list($i, $name, $default, $isVariable) = $arg;
+ foreach ($prototype['arguments'] as $argument) {
+ list($normalizedName, $name, $default) = $argument;
- if ($isVariable || isset($remaining[$i]) || isset($keywordArgs[$name]) || empty($default)) {
+ if (isset($vars[$normalizedName])) {
continue;
}
+ assert($default !== null);
if ($storeInEnv) {
$this->set($name, $this->reduce($default, true), true);
@@ -6182,6 +6829,67 @@ class Compiler
}
/**
+ * 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<array|Number> $positionalArgs
+ * @param array<string, array|Number> $namedArgs
+ * @param string|null $splatSeparator
+ *
+ * @return array<string, array|Number>
+ *
+ * @phpstan-param array{arguments: list<array{0: string, 1: string, 2: array|Number|null}>, 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
@@ -6190,7 +6898,7 @@ class Compiler
*/
protected function coerceValue($value)
{
- if (\is_array($value) || $value instanceof \ArrayAccess) {
+ if (\is_array($value) || $value instanceof Number) {
return $value;
}
@@ -6234,9 +6942,8 @@ class Compiler
}
if (
- $item[0] === static::$emptyList[0] &&
- $item[1] === static::$emptyList[1] &&
- $item[2] === static::$emptyList[2]
+ $item[0] === Type::T_LIST &&
+ $item[2] === []
) {
return static::$emptyMap;
}
@@ -6247,15 +6954,19 @@ class Compiler
/**
* Coerce something to list
*
- * @param array $item
- * @param string $delim
- * @param boolean $removeTrailingNull
+ * @param array|Number $item
+ * @param string $delim
+ * @param boolean $removeTrailingNull
*
* @return array
*/
protected function coerceList($item, $delim = ',', $removeTrailingNull = false)
{
- if (isset($item) && $item[0] === Type::T_LIST) {
+ if ($item instanceof Number) {
+ return [Type::T_LIST, $delim, [$item]];
+ }
+
+ if ($item[0] === Type::T_LIST) {
// remove trailing null from the list
if ($removeTrailingNull && end($item[2]) === static::$null) {
array_pop($item[2]);
@@ -6264,7 +6975,7 @@ class Compiler
return $item;
}
- if (isset($item) && $item[0] === Type::T_MAP) {
+ if ($item[0] === Type::T_MAP) {
$keys = $item[1];
$values = $item[2];
$list = [];
@@ -6295,7 +7006,7 @@ class Compiler
return [Type::T_LIST, ',', $list];
}
- return [Type::T_LIST, $delim, ! isset($item) ? [] : [$item]];
+ return [Type::T_LIST, $delim, [$item]];
}
/**
@@ -6324,6 +7035,10 @@ class Compiler
*/
protected function coerceColor($value, $inRGBFunction = false)
{
+ if ($value instanceof Number) {
+ return null;
+ }
+
switch ($value[0]) {
case Type::T_COLOR:
for ($i = 1; $i <= 3; $i++) {
@@ -6510,16 +7225,21 @@ class Compiler
}
/**
- * Assert value is a string (or keyword)
+ * Assert value is a string
+ *
+ * This method deals with internal implementation details of the value
+ * representation where unquoted strings can sometimes be stored under
+ * other types.
+ * The returned value is always using the T_STRING type.
*
* @api
*
* @param array|Number $value
- * @param string $varName
+ * @param string|null $varName
*
* @return array
*
- * @throws \Exception
+ * @throws SassScriptException
*/
public function assertString($value, $varName = null)
{
@@ -6530,13 +7250,10 @@ class Compiler
if (! \in_array($value[0], [Type::T_STRING, Type::T_KEYWORD])) {
$value = $this->compileValue($value);
- $var_display = ($varName ? " \${$varName}:" : '');
- throw $this->error("Error:{$var_display} $value is not a string.");
+ throw SassScriptException::forArgument("$value is not a string.", $varName);
}
- $value = $this->coerceString($value);
-
- return $value;
+ return $this->coerceString($value);
}
/**
@@ -6545,9 +7262,13 @@ class Compiler
* @param array|Number $value
*
* @return integer|float
+ *
+ * @deprecated
*/
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;
@@ -6565,17 +7286,20 @@ class Compiler
* @api
*
* @param array|Number $value
+ * @param string|null $varName
*
* @return array
*
- * @throws \Exception
+ * @throws SassScriptException
*/
- public function assertMap($value)
+ public function assertMap($value, $varName = null)
{
$value = $this->coerceMap($value);
if ($value[0] !== Type::T_MAP) {
- throw $this->error('expecting map, %s received', $value[0]);
+ $value = $this->compileValue($value);
+
+ throw SassScriptException::forArgument("$value is not a map.", $varName);
}
return $value;
@@ -6602,23 +7326,47 @@ class Compiler
}
/**
+ * 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<string, array|Number>
+ */
+ public function getArgumentListKeywords($value)
+ {
+ if ($value[0] !== Type::T_LIST || !isset($value[3]) || !\is_array($value[3])) {
+ throw new \InvalidArgumentException('The argument is not a sass argument list.');
+ }
+
+ return $value[3];
+ }
+
+ /**
* Assert value is a color
*
* @api
*
* @param array|Number $value
+ * @param string|null $varName
*
* @return array
*
- * @throws \Exception
+ * @throws SassScriptException
*/
- public function assertColor($value)
+ public function assertColor($value, $varName = null)
{
if ($color = $this->coerceColor($value)) {
return $color;
}
- throw $this->error('expecting color, %s received', $value[0]);
+ $value = $this->compileValue($value);
+
+ throw SassScriptException::forArgument("$value is not a color.", $varName);
}
/**
@@ -6627,18 +7375,17 @@ class Compiler
* @api
*
* @param array|Number $value
- * @param string $varName
+ * @param string|null $varName
*
* @return Number
*
- * @throws \Exception
+ * @throws SassScriptException
*/
public function assertNumber($value, $varName = null)
{
if (!$value instanceof Number) {
$value = $this->compileValue($value);
- $var_display = ($varName ? " \${$varName}:" : '');
- throw $this->error("Error:{$var_display} $value is not a number.");
+ throw SassScriptException::forArgument("$value is not a number.", $varName);
}
return $value;
@@ -6650,24 +7397,40 @@ class Compiler
* @api
*
* @param array|Number $value
- * @param string $varName
+ * @param string|null $varName
*
* @return integer
*
- * @throws \Exception
+ * @throws SassScriptException
*/
public function assertInteger($value, $varName = null)
{
-
$value = $this->assertNumber($value, $varName)->getDimension();
if (round($value - \intval($value), Number::PRECISION) > 0) {
- $var_display = ($varName ? " \${$varName}:" : '');
- throw $this->error("Error:{$var_display} $value is not an integer.");
+ throw SassScriptException::forArgument("$value is not an integer.", $varName);
}
return intval($value);
}
+ /**
+ * Extract the ... / alpha on the last argument of channel arg
+ * in color functions
+ *
+ * @param array $args
+ * @return array
+ */
+ private function extractSlashAlphaInColorFunction($args)
+ {
+ $last = end($args);
+ if (\count($args) === 3 && $last[0] === Type::T_EXPRESSION && $last[1] === '/') {
+ array_pop($args);
+ $args[] = $last[2];
+ $args[] = $last[3];
+ }
+ return $args;
+ }
+
/**
* Make sure a color's components don't go out of bounds
@@ -6686,6 +7449,10 @@ class Compiler
if ($c[$i] > 255) {
$c[$i] = 255;
}
+
+ if (!\is_int($c[$i])) {
+ $c[$i] = round($c[$i]);
+ }
}
return $c;
@@ -6694,7 +7461,7 @@ class Compiler
/**
* Convert RGB to HSL
*
- * @api
+ * @internal
*
* @param integer $red
* @param integer $green
@@ -6728,7 +7495,7 @@ class Compiler
}
}
- return [Type::T_HSL, fmod($h, 360), $s * 100, $l / 5.1];
+ return [Type::T_HSL, fmod($h + 360, 360), $s * 100, $l / 5.1];
}
/**
@@ -6766,11 +7533,11 @@ class Compiler
/**
* Convert HSL to RGB
*
- * @api
+ * @internal
*
- * @param integer $hue H from 0 to 360
- * @param integer $saturation S from 0 to 100
- * @param integer $lightness L from 0 to 100
+ * @param int|float $hue H from 0 to 360
+ * @param int|float $saturation S from 0 to 100
+ * @param int|float $lightness L from 0 to 100
*
* @return array
*/
@@ -6796,19 +7563,87 @@ class Compiler
return $out;
}
+ /**
+ * Convert HWB to RGB
+ * https://www.w3.org/TR/css-color-4/#hwb-to-rgb
+ *
+ * @api
+ *
+ * @param integer $hue H from 0 to 360
+ * @param integer $whiteness W from 0 to 100
+ * @param integer $blackness B from 0 to 100
+ *
+ * @return array
+ */
+ private function HWBtoRGB($hue, $whiteness, $blackness)
+ {
+ $w = min(100, max(0, $whiteness)) / 100;
+ $b = min(100, max(0, $blackness)) / 100;
+
+ $sum = $w + $b;
+ if ($sum > 1.0) {
+ $w = $w / $sum;
+ $b = $b / $sum;
+ }
+ $b = min(1.0 - $w, $b);
+
+ $rgb = $this->toRGB($hue, 100, 50);
+ for($i = 1; $i < 4; $i++) {
+ $rgb[$i] *= (1.0 - $w - $b);
+ $rgb[$i] = round($rgb[$i] + 255 * $w + 0.0001);
+ }
+
+ return $rgb;
+ }
+
+ /**
+ * Convert RGB to HWB
+ *
+ * @api
+ *
+ * @param integer $red
+ * @param integer $green
+ * @param integer $blue
+ *
+ * @return array
+ */
+ private function RGBtoHWB($red, $green, $blue)
+ {
+ $min = min($red, $green, $blue);
+ $max = max($red, $green, $blue);
+
+ $d = $max - $min;
+
+ if ((int) $d === 0) {
+ $h = 0;
+ } else {
+
+ if ($red == $max) {
+ $h = 60 * ($green - $blue) / $d;
+ } elseif ($green == $max) {
+ $h = 60 * ($blue - $red) / $d + 120;
+ } elseif ($blue == $max) {
+ $h = 60 * ($red - $green) / $d + 240;
+ }
+ }
+
+ return [Type::T_HWB, fmod($h, 360), $min / 255 * 100, 100 - $max / 255 *100];
+ }
+
+
// Built in functions
protected static $libCall = ['function', 'args...'];
- protected function libCall($args, $kwargs)
+ protected function libCall($args)
{
- $functionReference = array_shift($args);
+ $functionReference = $args[0];
if (in_array($functionReference[0], [Type::T_STRING, Type::T_KEYWORD])) {
$name = $this->compileStringContent($this->coerceString($functionReference));
- $warning = "DEPRECATION WARNING: Passing a string to call() is deprecated and will be illegal\n"
+ $warning = "Passing a string to call() is deprecated and will be illegal\n"
. "in Sass 4.0. Use call(function-reference($name)) instead.";
- fwrite($this->stderr, "$warning\n\n");
- $functionReference = $this->libGetFunction([$functionReference]);
+ Warn::deprecation($warning);
+ $functionReference = $this->libGetFunction([$this->assertString($functionReference, 'function')]);
}
if ($functionReference === static::$null) {
@@ -6819,18 +7654,9 @@ class Compiler
throw $this->error('Function reference expected, got ' . $functionReference[0]);
}
- $callArgs = [];
-
- // $kwargs['args'] is [Type::T_LIST, ',', [..]]
- foreach ($kwargs['args'][2] as $varname => $arg) {
- if (is_numeric($varname)) {
- $varname = null;
- } else {
- $varname = [ 'var', $varname];
- }
-
- $callArgs[] = [$varname, $arg, false];
- }
+ $callArgs = [
+ [null, $args[1], true]
+ ];
return $this->reduce([Type::T_FUNCTION_CALL, $functionReference, $callArgs]);
}
@@ -6842,7 +7668,7 @@ class Compiler
];
protected function libGetFunction($args)
{
- $name = $this->compileStringContent($this->coerceString(array_shift($args)));
+ $name = $this->compileStringContent($this->assertString(array_shift($args), 'name'));
$isCss = false;
if (count($args)) {
@@ -6912,7 +7738,7 @@ class Compiler
$key = array_search($this->normalizeValue($value), $values);
- return false === $key ? static::$null : $key + 1;
+ return false === $key ? static::$null : new Number($key + 1, '');
}
protected static $libRgb = [
@@ -6950,7 +7776,7 @@ class Compiler
[$funcName . '(', $color[1], ', ', $color[2], ', ', $color[3], ', ', $alpha, ')']];
}
} else {
- $color = [Type::T_STRING, '', [$funcName . '(', $args[0], ')']];
+ $color = [Type::T_STRING, '', [$funcName . '(', $args[0], ', ', $args[1], ')']];
}
break;
@@ -6983,35 +7809,129 @@ class Compiler
* Helper function for adjust_color, change_color, and scale_color
*
* @param array<array|Number> $args
+ * @param string $operation
* @param callable $fn
*
* @return array
+ *
+ * @phpstan-param callable(float|int, float|int|null, float|int): (float|int) $fn
*/
- protected function alterColor($args, $fn)
+ protected function alterColor(array $args, $operation, $fn)
{
- $color = $this->assertColor($args[0]);
+ $color = $this->assertColor($args[0], 'color');
- foreach ([1 => 1, 2 => 2, 3 => 3, 7 => 4] as $iarg => $irgba) {
- if (isset($args[$iarg])) {
- $val = $this->assertNumber($args[$iarg])->getDimension();
+ if ($args[1][2]) {
+ throw new SassScriptException('Only one positional argument is allowed. All other arguments must be passed by name.');
+ }
+
+ $kwargs = $this->getArgumentListKeywords($args[1]);
+
+ $scale = $operation === 'scale';
+ $change = $operation === 'change';
+
+ /**
+ * @param string $name
+ * @param float|int $max
+ * @param bool $checkPercent
+ * @param bool $assertPercent
+ *
+ * @return float|int|null
+ */
+ $getParam = function ($name, $max, $checkPercent = false, $assertPercent = false) use (&$kwargs, $scale, $change) {
+ if (!isset($kwargs[$name])) {
+ return null;
+ }
+
+ $number = $this->assertNumber($kwargs[$name], $name);
+ unset($kwargs[$name]);
- if (! isset($color[$irgba])) {
- $color[$irgba] = (($irgba < 4) ? 0 : 1);
+ if (!$scale && $checkPercent) {
+ if (!$number->hasUnit('%')) {
+ $warning = $this->error("{$name} Passing a number `$number` without unit % is deprecated.");
+ $this->logger->warn($warning->getMessage(), true);
}
+ }
- $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;
+ $found = false;
+
+ if ($hasRgb && ($hasSL || $hasWB || $hue !== null)) {
+ throw new SassScriptException(sprintf('RGB parameters may not be passed along with %s parameters.', $hasWB ? 'HWB' : 'HSL'));
}
- if (! empty($args[4]) || ! empty($args[5]) || ! empty($args[6])) {
+ if ($hasWB && $hasSL) {
+ throw new SassScriptException('HSL parameters may not be passed along with HWB parameters.');
+ }
+
+ if ($hasRgb) {
+ $color[1] = round($fn($color[1], $red, 255));
+ $color[2] = round($fn($color[2], $green, 255));
+ $color[3] = round($fn($color[3], $blue, 255));
+ } elseif ($hasWB) {
+ $hwb = $this->RGBtoHWB($color[1], $color[2], $color[3]);
+ if ($hue !== null) {
+ $hwb[1] = $change ? $hue : $hwb[1] + $hue;
+ }
+ $hwb[2] = $fn($hwb[2], $whiteness, 100);
+ $hwb[3] = $fn($hwb[3], $blackness, 100);
+
+ $rgb = $this->HWBtoRGB($hwb[1], $hwb[2], $hwb[3]);
+
+ if (isset($color[4])) {
+ $rgb[4] = $color[4];
+ }
+
+ $color = $rgb;
+ } elseif ($hue !== null || $hasSL) {
$hsl = $this->toHSL($color[1], $color[2], $color[3]);
- foreach ([4 => 1, 5 => 2, 6 => 3] as $iarg => $ihsl) {
- if (! empty($args[$iarg])) {
- $val = $this->assertNumber($args[$iarg])->getDimension();
- $hsl[$ihsl] = \call_user_func($fn, $hsl[$ihsl], $val, $iarg);
- }
+ if ($hue !== null) {
+ $hsl[1] = $change ? $hue : $hsl[1] + $hue;
}
+ $hsl[2] = $fn($hsl[2], $saturation, 100);
+ $hsl[3] = $fn($hsl[3], $lightness, 100);
$rgb = $this->toRGB($hsl[1], $hsl[2], $hsl[3]);
@@ -7022,58 +7942,54 @@ class Compiler
$color = $rgb;
}
+ if ($alpha !== null) {
+ $existingAlpha = isset($color[4]) ? $color[4] : 1;
+ $color[4] = $fn($existingAlpha, $alpha, 1);
+ }
+
return $color;
}
- protected static $libAdjustColor = [
- 'color', 'red:null', 'green:null', 'blue:null',
- 'hue:null', 'saturation:null', 'lightness:null', 'alpha:null'
- ];
+ protected static $libAdjustColor = ['color', 'kwargs...'];
protected function libAdjustColor($args)
{
- return $this->alterColor($args, function ($base, $alter, $i) {
- return $base + $alter;
+ return $this->alterColor($args, 'adjust', function ($base, $alter, $max) {
+ if ($alter === null) {
+ return $base;
+ }
+
+ $new = $base + $alter;
+
+ if ($new < 0) {
+ return 0;
+ }
+
+ if ($new > $max) {
+ return $max;
+ }
+
+ return $new;
});
}
- protected static $libChangeColor = [
- 'color', 'red:null', 'green:null', 'blue:null',
- 'hue:null', 'saturation:null', 'lightness:null', 'alpha:null'
- ];
+ protected static $libChangeColor = ['color', 'kwargs...'];
protected function libChangeColor($args)
{
- return $this->alterColor($args, function ($base, $alter, $i) {
+ return $this->alterColor($args,'change', function ($base, $alter, $max) {
+ if ($alter === null) {
+ return $base;
+ }
+
return $alter;
});
}
- protected static $libScaleColor = [
- 'color', 'red:null', 'green:null', 'blue:null',
- 'hue:null', 'saturation:null', 'lightness:null', 'alpha:null'
- ];
+ protected static $libScaleColor = ['color', 'kwargs...'];
protected function libScaleColor($args)
{
- return $this->alterColor($args, function ($base, $scale, $i) {
- // 1, 2, 3 - rgb
- // 4, 5, 6 - hsl
- // 7 - a
- switch ($i) {
- case 1:
- case 2:
- case 3:
- $max = 255;
- break;
-
- case 4:
- $max = 360;
- break;
-
- case 7:
- $max = 1;
- break;
-
- default:
- $max = 100;
+ return $this->alterColor($args, 'scale', function ($base, $scale, $max) {
+ if ($scale === null) {
+ return $base;
}
$scale = $scale / 100;
@@ -7109,7 +8025,7 @@ class Compiler
throw $this->error('Error: argument `$color` of `red($color)` must be a color');
}
- return $color[1];
+ return new Number((int) $color[1], '');
}
protected static $libGreen = ['color'];
@@ -7121,7 +8037,7 @@ class Compiler
throw $this->error('Error: argument `$color` of `green($color)` must be a color');
}
- return $color[2];
+ return new Number((int) $color[2], '');
}
protected static $libBlue = ['color'];
@@ -7133,14 +8049,14 @@ class Compiler
throw $this->error('Error: argument `$color` of `blue($color)` must be a color');
}
- return $color[3];
+ return new Number((int) $color[3], '');
}
protected static $libAlpha = ['color'];
protected function libAlpha($args)
{
if ($color = $this->coerceColor($args[0])) {
- return isset($color[4]) ? $color[4] : 1;
+ return new Number(isset($color[4]) ? $color[4] : 1, '');
}
// this might be the IE function, so return value unchanged
@@ -7161,39 +8077,35 @@ class Compiler
// mix two colors
protected static $libMix = [
- ['color1', 'color2', 'weight:0.5'],
- ['color-1', 'color-2', 'weight:0.5']
+ ['color1', 'color2', 'weight:50%'],
+ ['color-1', 'color-2', 'weight:50%']
];
protected function libMix($args)
{
list($first, $second, $weight) = $args;
- $first = $this->assertColor($first);
- $second = $this->assertColor($second);
-
- if (! isset($weight)) {
- $weight = 0.5;
- } else {
- $weight = $this->coercePercent($weight);
- }
+ $first = $this->assertColor($first, 'color1');
+ $second = $this->assertColor($second, 'color2');
+ $weightScale = $this->assertNumber($weight, 'weight')->valueInRange(0, 100, 'weight') / 100;
$firstAlpha = isset($first[4]) ? $first[4] : 1;
$secondAlpha = isset($second[4]) ? $second[4] : 1;
- $w = $weight * 2 - 1;
- $a = $firstAlpha - $secondAlpha;
+ $normalizedWeight = $weightScale * 2 - 1;
+ $alphaDistance = $firstAlpha - $secondAlpha;
- $w1 = (($w * $a === -1 ? $w : ($w + $a) / (1 + $w * $a)) + 1) / 2.0;
- $w2 = 1.0 - $w1;
+ $combinedWeight = $normalizedWeight * $alphaDistance == -1 ? $normalizedWeight : ($normalizedWeight + $alphaDistance) / (1 + $normalizedWeight * $alphaDistance);
+ $weight1 = ($combinedWeight + 1) / 2.0;
+ $weight2 = 1.0 - $weight1;
$new = [Type::T_COLOR,
- $w1 * $first[1] + $w2 * $second[1],
- $w1 * $first[2] + $w2 * $second[2],
- $w1 * $first[3] + $w2 * $second[3],
+ $weight1 * $first[1] + $weight2 * $second[1],
+ $weight1 * $first[2] + $weight2 * $second[2],
+ $weight1 * $first[3] + $weight2 * $second[3],
];
if ($firstAlpha != 1.0 || $secondAlpha != 1.0) {
- $new[] = $firstAlpha * $weight + $secondAlpha * (1 - $weight);
+ $new[] = $firstAlpha * $weightScale + $secondAlpha * (1 - $weightScale);
}
return $this->fixColor($new);
@@ -7201,6 +8113,7 @@ class Compiler
protected static $libHsl = [
['channels'],
+ ['hue', 'saturation'],
['hue', 'saturation', 'lightness'],
['hue', 'saturation', 'lightness', 'alpha'] ];
protected function libHsl($args, $kwargs, $funcName = 'hsl')
@@ -7216,14 +8129,25 @@ class Compiler
$args_to_check = $kwargs['channels'][2];
}
+ if (\count($args) === 2) {
+ // if var() is used as an argument, return as a css function
+ foreach ($args as $arg) {
+ if ($arg[0] === Type::T_FUNCTION && in_array($arg[1], ['var'])) {
+ return null;
+ }
+ }
+
+ throw new SassScriptException('Missing argument $lightness.');
+ }
+
foreach ($kwargs as $k => $arg) {
- if (in_array($arg[0], [Type::T_FUNCTION_CALL]) && in_array($arg[1], ['min', 'max'])) {
+ if (in_array($arg[0], [Type::T_FUNCTION_CALL, Type::T_FUNCTION]) && in_array($arg[1], ['min', 'max'])) {
return null;
}
}
foreach ($args_to_check as $k => $arg) {
- if (in_array($arg[0], [Type::T_FUNCTION_CALL]) && in_array($arg[1], ['min', 'max'])) {
+ if (in_array($arg[0], [Type::T_FUNCTION_CALL, Type::T_FUNCTION]) && in_array($arg[1], ['min', 'max'])) {
if (count($kwargs) > 1 || ($k >= 2 && count($args) === 4)) {
return null;
}
@@ -7258,7 +8182,7 @@ class Compiler
}
}
- $hueValue = $hue->getDimension() % 360;
+ $hueValue = fmod($hue->getDimension(), 360);
while ($hueValue < 0) {
$hueValue += 360;
@@ -7275,6 +8199,7 @@ class Compiler
protected static $libHsla = [
['channels'],
+ ['hue', 'saturation'],
['hue', 'saturation', 'lightness'],
['hue', 'saturation', 'lightness', 'alpha']];
protected function libHsla($args, $kwargs)
@@ -7285,7 +8210,7 @@ class Compiler
protected static $libHue = ['color'];
protected function libHue($args)
{
- $color = $this->assertColor($args[0]);
+ $color = $this->assertColor($args[0], 'color');
$hsl = $this->toHSL($color[1], $color[2], $color[3]);
return new Number($hsl[1], 'deg');
@@ -7294,7 +8219,7 @@ class Compiler
protected static $libSaturation = ['color'];
protected function libSaturation($args)
{
- $color = $this->assertColor($args[0]);
+ $color = $this->assertColor($args[0], 'color');
$hsl = $this->toHSL($color[1], $color[2], $color[3]);
return new Number($hsl[2], '%');
@@ -7303,16 +8228,148 @@ class Compiler
protected static $libLightness = ['color'];
protected function libLightness($args)
{
- $color = $this->assertColor($args[0]);
+ $color = $this->assertColor($args[0], 'color');
$hsl = $this->toHSL($color[1], $color[2], $color[3]);
return new Number($hsl[3], '%');
}
+ /*
+ * Todo : a integrer dans le futur module color
+ protected static $libHwb = [
+ ['channels'],
+ ['hue', 'whiteness', 'blackness'],
+ ['hue', 'whiteness', 'blackness', 'alpha'] ];
+ protected function libHwb($args, $kwargs, $funcName = 'hwb')
+ {
+ $args_to_check = $args;
+
+ if (\count($args) == 1) {
+ if ($args[0][0] !== Type::T_LIST) {
+ throw $this->error("Missing elements \$whiteness and \$blackness");
+ }
+
+ if (\trim($args[0][1])) {
+ throw $this->error("\$channels must be a space-separated list.");
+ }
+
+ if (! empty($args[0]['enclosing'])) {
+ throw $this->error("\$channels must be an unbracketed list.");
+ }
+
+ $args = $args[0][2];
+ if (\count($args) > 3) {
+ throw $this->error("hwb() : Only 3 elements are allowed but ". \count($args) . "were passed");
+ }
+
+ $args_to_check = $this->extractSlashAlphaInColorFunction($kwargs['channels'][2]);
+ if (\count($args_to_check) !== \count($kwargs['channels'][2])) {
+ $args = $args_to_check;
+ }
+ }
+
+ if (\count($args_to_check) < 2) {
+ throw $this->error("Missing elements \$whiteness and \$blackness");
+ }
+ if (\count($args_to_check) < 3) {
+ throw $this->error("Missing element \$blackness");
+ }
+ if (\count($args_to_check) > 4) {
+ throw $this->error("hwb() : Only 4 elements are allowed but ". \count($args) . "were passed");
+ }
+
+ foreach ($kwargs as $k => $arg) {
+ if (in_array($arg[0], [Type::T_FUNCTION_CALL]) && in_array($arg[1], ['min', 'max'])) {
+ return null;
+ }
+ }
+
+ foreach ($args_to_check as $k => $arg) {
+ if (in_array($arg[0], [Type::T_FUNCTION_CALL]) && in_array($arg[1], ['min', 'max'])) {
+ if (count($kwargs) > 1 || ($k >= 2 && count($args) === 4)) {
+ return null;
+ }
+
+ $args[$k] = $this->stringifyFncallArgs($arg);
+ }
+
+ if (
+ $k >= 2 && count($args) === 4 &&
+ in_array($arg[0], [Type::T_FUNCTION_CALL, Type::T_FUNCTION]) &&
+ in_array($arg[1], ['calc','env'])
+ ) {
+ return null;
+ }
+ }
+
+ $hue = $this->reduce($args[0]);
+ $whiteness = $this->reduce($args[1]);
+ $blackness = $this->reduce($args[2]);
+ $alpha = null;
+
+ if (\count($args) === 4) {
+ $alpha = $this->compileColorPartValue($args[3], 0, 1, false);
+
+ if (! \is_numeric($alpha)) {
+ $val = $this->compileValue($args[3]);
+ throw $this->error("\$alpha: $val is not a number");
+ }
+ }
+
+ $this->assertNumber($hue, 'hue');
+ $this->assertUnit($whiteness, ['%'], 'whiteness');
+ $this->assertUnit($blackness, ['%'], 'blackness');
+
+ $this->assertRange($whiteness, 0, 100, "0% and 100%", "whiteness");
+ $this->assertRange($blackness, 0, 100, "0% and 100%", "blackness");
+
+ $w = $whiteness->getDimension();
+ $b = $blackness->getDimension();
+
+ $hueValue = $hue->getDimension() % 360;
+
+ while ($hueValue < 0) {
+ $hueValue += 360;
+ }
+
+ $color = $this->HWBtoRGB($hueValue, $w, $b);
+
+ if (! \is_null($alpha)) {
+ $color[4] = $alpha;
+ }
+
+ return $color;
+ }
+
+ protected static $libWhiteness = ['color'];
+ protected function libWhiteness($args, $kwargs, $funcName = 'whiteness') {
+
+ $color = $this->assertColor($args[0]);
+ $hwb = $this->RGBtoHWB($color[1], $color[2], $color[3]);
+
+ return new Number($hwb[2], '%');
+ }
+
+ protected static $libBlackness = ['color'];
+ protected function libBlackness($args, $kwargs, $funcName = 'blackness') {
+
+ $color = $this->assertColor($args[0]);
+ $hwb = $this->RGBtoHWB($color[1], $color[2], $color[3]);
+
+ return new Number($hwb[3], '%');
+ }
+ */
+
protected function adjustHsl($color, $idx, $amount)
{
$hsl = $this->toHSL($color[1], $color[2], $color[3]);
$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])) {
@@ -7325,8 +8382,8 @@ class Compiler
protected static $libAdjustHue = ['color', 'degrees'];
protected function libAdjustHue($args)
{
- $color = $this->assertColor($args[0]);
- $degrees = $this->assertNumber($args[1])->getDimension();
+ $color = $this->assertColor($args[0], 'color');
+ $degrees = $this->assertNumber($args[1], 'degrees')->getDimension();
return $this->adjustHsl($color, 1, $degrees);
}
@@ -7334,7 +8391,7 @@ class Compiler
protected static $libLighten = ['color', 'amount'];
protected function libLighten($args)
{
- $color = $this->assertColor($args[0]);
+ $color = $this->assertColor($args[0], 'color');
$amount = Util::checkRange('amount', new Range(0, 100), $args[1], '%');
return $this->adjustHsl($color, 3, $amount);
@@ -7343,7 +8400,7 @@ class Compiler
protected static $libDarken = ['color', 'amount'];
protected function libDarken($args)
{
- $color = $this->assertColor($args[0]);
+ $color = $this->assertColor($args[0], 'color');
$amount = Util::checkRange('amount', new Range(0, 100), $args[1], '%');
return $this->adjustHsl($color, 3, -$amount);
@@ -7354,28 +8411,25 @@ class Compiler
{
$value = $args[0];
- if ($value instanceof Number) {
- return null;
- }
-
if (count($args) === 1) {
- $val = $this->compileValue($value);
- throw $this->error("\$amount: $val is not a number");
+ $this->assertNumber($args[0], 'amount');
+
+ return null;
}
- $color = $this->assertColor($value);
- $amount = 100 * $this->coercePercent($args[1]);
+ $color = $this->assertColor($args[0], 'color');
+ $amount = $this->assertNumber($args[1], 'amount');
- return $this->adjustHsl($color, 2, $amount);
+ return $this->adjustHsl($color, 2, $amount->valueInRange(0, 100, 'amount'));
}
protected static $libDesaturate = ['color', 'amount'];
protected function libDesaturate($args)
{
- $color = $this->assertColor($args[0]);
- $amount = 100 * $this->coercePercent($args[1]);
+ $color = $this->assertColor($args[0], 'color');
+ $amount = $this->assertNumber($args[1], 'amount');
- return $this->adjustHsl($color, 2, -$amount);
+ return $this->adjustHsl($color, 2, -$amount->valueInRange(0, 100, 'amount'));
}
protected static $libGrayscale = ['color'];
@@ -7387,51 +8441,47 @@ class Compiler
return null;
}
- return $this->adjustHsl($this->assertColor($value), 2, -100);
+ return $this->adjustHsl($this->assertColor($value, 'color'), 2, -100);
}
protected static $libComplement = ['color'];
protected function libComplement($args)
{
- return $this->adjustHsl($this->assertColor($args[0]), 1, 180);
+ return $this->adjustHsl($this->assertColor($args[0], 'color'), 1, 180);
}
- protected static $libInvert = ['color', 'weight:1'];
+ protected static $libInvert = ['color', 'weight:100%'];
protected function libInvert($args)
{
- list($value, $weight) = $args;
+ $value = $args[0];
- if (! isset($weight)) {
- $weight = 1;
- } else {
- $weight = $this->coercePercent($weight);
- }
+ $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.');
+ }
+
return null;
}
- $color = $this->assertColor($value);
+ $color = $this->assertColor($value, 'color');
$inverted = $color;
$inverted[1] = 255 - $inverted[1];
$inverted[2] = 255 - $inverted[2];
$inverted[3] = 255 - $inverted[3];
- if ($weight < 1) {
- return $this->libMix([$inverted, $color, new Number($weight, '')]);
- }
-
- return $inverted;
+ return $this->libMix([$inverted, $color, $weight]);
}
// increases opacity by amount
protected static $libOpacify = ['color', 'amount'];
protected function libOpacify($args)
{
- $color = $this->assertColor($args[0]);
- $amount = $this->coercePercent($args[1]);
+ $color = $this->assertColor($args[0], 'color');
+ $amount = $this->assertNumber($args[1], 'amount');
- $color[4] = (isset($color[4]) ? $color[4] : 1) + $amount;
+ $color[4] = (isset($color[4]) ? $color[4] : 1) + $amount->valueInRange(0, 1, 'amount');
$color[4] = min(1, max(0, $color[4]));
return $color;
@@ -7447,10 +8497,10 @@ class Compiler
protected static $libTransparentize = ['color', 'amount'];
protected function libTransparentize($args)
{
- $color = $this->assertColor($args[0]);
- $amount = $this->coercePercent($args[1]);
+ $color = $this->assertColor($args[0], 'color');
+ $amount = $this->assertNumber($args[1], 'amount');
- $color[4] = (isset($color[4]) ? $color[4] : 1) - $amount;
+ $color[4] = (isset($color[4]) ? $color[4] : 1) - $amount->valueInRange(0, 1, 'amount');
$color[4] = min(1, max(0, $color[4]));
return $color;
@@ -7465,26 +8515,34 @@ class Compiler
protected static $libUnquote = ['string'];
protected function libUnquote($args)
{
- $str = $args[0];
+ try {
+ $str = $this->assertString($args[0], 'string');
+ } catch (SassScriptException $e) {
+ $value = $this->compileValue($args[0]);
+ $fname = $this->getPrettyPath($this->sourceNames[$this->sourceIndex]);
+ $line = $this->sourceLine;
- if ($str[0] === Type::T_STRING) {
- $str[1] = '';
+ $message = "Passing $value, a non-string value, to unquote()
+will be an error in future versions of Sass.\n on line $line of $fname";
+
+ $this->logger->warn($message, true);
+
+ return $args[0];
}
+ $str[1] = '';
+
return $str;
}
protected static $libQuote = ['string'];
protected function libQuote($args)
{
- $value = $args[0];
+ $value = $this->assertString($args[0], 'string');
- if ($value[0] === Type::T_STRING && ! empty($value[1])) {
- $value[1] = '"';
- return $value;
- }
+ $value[1] = '"';
- return [Type::T_STRING, '"', [$value]];
+ return $value;
}
protected static $libPercentage = ['number'];
@@ -7528,6 +8586,7 @@ class Compiler
return new Number(abs($num->getDimension()), $num->getNumeratorUnits(), $num->getDenominatorUnits());
}
+ protected static $libMin = ['numbers...'];
protected function libMin($args)
{
/**
@@ -7535,7 +8594,7 @@ class Compiler
*/
$min = null;
- foreach ($args as $arg) {
+ foreach ($args[0][2] as $arg) {
$number = $this->assertNumber($arg);
if (\is_null($min) || $min->greaterThan($number)) {
@@ -7550,6 +8609,7 @@ class Compiler
throw $this->error('At least one argument must be passed.');
}
+ protected static $libMax = ['numbers...'];
protected function libMax($args)
{
/**
@@ -7557,7 +8617,7 @@ class Compiler
*/
$max = null;
- foreach ($args as $arg) {
+ foreach ($args[0][2] as $arg) {
$number = $this->assertNumber($arg);
if (\is_null($max) || $max->lessThan($number)) {
@@ -7577,31 +8637,27 @@ class Compiler
{
$list = $this->coerceList($args[0], ',', true);
- return \count($list[2]);
+ return new Number(\count($list[2]), '');
}
- //protected static $libListSeparator = ['list...'];
+ protected static $libListSeparator = ['list'];
protected function libListSeparator($args)
{
- if (\count($args) > 1) {
- return 'comma';
- }
-
if (! \in_array($args[0][0], [Type::T_LIST, Type::T_MAP])) {
- return 'space';
+ return [Type::T_KEYWORD, 'space'];
}
$list = $this->coerceList($args[0]);
if (\count($list[2]) <= 1 && empty($list['enclosing'])) {
- return 'space';
+ return [Type::T_KEYWORD, 'space'];
}
if ($list[1] === ',') {
- return 'comma';
+ return [Type::T_KEYWORD, 'comma'];
}
- return 'space';
+ return [Type::T_KEYWORD, 'space'];
}
protected static $libNth = ['list', 'n'];
@@ -7643,7 +8699,7 @@ class Compiler
protected static $libMapGet = ['map', 'key'];
protected function libMapGet($args)
{
- $map = $this->assertMap($args[0]);
+ $map = $this->assertMap($args[0], 'map');
$key = $args[1];
if (! \is_null($key)) {
@@ -7662,7 +8718,7 @@ class Compiler
protected static $libMapKeys = ['map'];
protected function libMapKeys($args)
{
- $map = $this->assertMap($args[0]);
+ $map = $this->assertMap($args[0], 'map');
$keys = $map[1];
return [Type::T_LIST, ',', $keys];
@@ -7671,21 +8727,28 @@ class Compiler
protected static $libMapValues = ['map'];
protected function libMapValues($args)
{
- $map = $this->assertMap($args[0]);
+ $map = $this->assertMap($args[0], 'map');
$values = $map[2];
return [Type::T_LIST, ',', $values];
}
- protected static $libMapRemove = ['map', 'key...'];
+ protected static $libMapRemove = [
+ ['map'],
+ ['map', 'key', 'keys...'],
+ ];
protected function libMapRemove($args)
{
- $map = $this->assertMap($args[0]);
- $keyList = $this->assertList($args[1]);
+ $map = $this->assertMap($args[0], 'map');
+
+ if (\count($args) === 1) {
+ return $map;
+ }
$keys = [];
+ $keys[] = $this->compileStringContent($this->coerceString($args[1]));
- foreach ($keyList[2] as $key) {
+ foreach ($args[2][2] as $key) {
$keys[] = $this->compileStringContent($this->coerceString($key));
}
@@ -7702,8 +8765,19 @@ class Compiler
protected static $libMapHasKey = ['map', 'key'];
protected function libMapHasKey($args)
{
- $map = $this->assertMap($args[0]);
- $key = $this->compileStringContent($this->coerceString($args[1]));
+ $map = $this->assertMap($args[0], 'map');
+
+ return $this->toBool($this->mapHasKey($map, $args[1]));
+ }
+
+ /**
+ * @param array|Number $keyValue
+ *
+ * @return bool
+ */
+ private function mapHasKey(array $map, $keyValue)
+ {
+ $key = $this->compileStringContent($this->coerceString($keyValue));
for ($i = \count($map[1]) - 1; $i >= 0; $i--) {
if ($key === $this->compileStringContent($this->coerceString($map[1][$i]))) {
@@ -7720,8 +8794,8 @@ class Compiler
];
protected function libMapMerge($args)
{
- $map1 = $this->assertMap($args[0]);
- $map2 = $this->assertMap($args[1]);
+ $map1 = $this->assertMap($args[0], 'map1');
+ $map2 = $this->assertMap($args[1], 'map2');
foreach ($map2[1] as $i2 => $key2) {
$key = $this->compileStringContent($this->coerceString($key2));
@@ -7743,12 +8817,18 @@ class Compiler
protected static $libKeywords = ['args'];
protected function libKeywords($args)
{
- $this->assertList($args[0]);
+ $value = $args[0];
+
+ if ($value[0] !== Type::T_LIST || !isset($value[3]) || !\is_array($value[3])) {
+ $compiledValue = $this->compileValue($value);
+
+ throw SassScriptException::forArgument($compiledValue . ' is not an argument list.', 'args');
+ }
$keys = [];
$values = [];
- foreach ($args[0][2] as $name => $arg) {
+ foreach ($this->getArgumentListKeywords($value) as $name => $arg) {
$keys[] = [Type::T_KEYWORD, $name];
$values[] = $arg;
}
@@ -7763,10 +8843,10 @@ class Compiler
$this->coerceList($list, ' ');
if (! empty($list['enclosing']) && $list['enclosing'] === 'bracket') {
- return true;
+ return self::$true;
}
- return false;
+ return self::$false;
}
/**
@@ -7857,21 +8937,23 @@ class Compiler
return $res;
}
+ protected static $libZip = ['lists...'];
protected function libZip($args)
{
- foreach ($args as $key => $arg) {
- $args[$key] = $this->coerceList($arg);
+ $argLists = [];
+ foreach ($args[0][2] as $arg) {
+ $argLists[] = $this->coerceList($arg);
}
$lists = [];
- $firstList = array_shift($args);
+ $firstList = array_shift($argLists);
$result = [Type::T_LIST, ',', $lists];
if (! \is_null($firstList)) {
foreach ($firstList[2] as $key => $item) {
$list = [Type::T_LIST, '', [$item]];
- foreach ($args as $arg) {
+ foreach ($argLists as $arg) {
if (isset($arg[2][$key])) {
$list[2][] = $arg[2][$key];
} else {
@@ -7895,6 +8977,16 @@ class Compiler
{
$value = $args[0];
+ return [Type::T_KEYWORD, $this->getTypeOf($value)];
+ }
+
+ /**
+ * @param array|Number $value
+ *
+ * @return string
+ */
+ private function getTypeOf($value)
+ {
switch ($value[0]) {
case Type::T_KEYWORD:
if ($value === static::$true || $value === static::$false) {
@@ -7913,7 +9005,7 @@ class Compiler
return 'function';
case Type::T_LIST:
- if (isset($value[3]) && $value[3]) {
+ if (isset($value[3]) && \is_array($value[3])) {
return 'arglist';
}
@@ -7926,21 +9018,17 @@ class Compiler
protected static $libUnit = ['number'];
protected function libUnit($args)
{
- $num = $args[0];
-
- if ($num instanceof Number) {
- return [Type::T_STRING, '"', [$num->unitStr()]];
- }
+ $num = $this->assertNumber($args[0], 'number');
- return '';
+ return [Type::T_STRING, '"', [$num->unitStr()]];
}
protected static $libUnitless = ['number'];
protected function libUnitless($args)
{
- $value = $args[0];
+ $value = $this->assertNumber($args[0], 'number');
- return $value instanceof Number && $value->unitless();
+ return $this->toBool($value->unitless());
}
protected static $libComparable = [
@@ -7958,7 +9046,7 @@ class Compiler
throw $this->error('Invalid argument(s) for "comparable"');
}
- return $number1->isComparableTo($number2);
+ return $this->toBool($number1->isComparableTo($number2));
}
protected static $libStrIndex = ['string', 'substring'];
@@ -8017,25 +9105,37 @@ class Compiler
protected static $libStrSlice = ['string', 'start-at', 'end-at:-1'];
protected function libStrSlice($args)
{
- if (isset($args[2]) && ! $args[2][1]) {
- return static::$nullString;
+ $string = $this->assertString($args[0], 'string');
+ $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');
+
+ if ($endInt === 0) {
+ return [Type::T_STRING, $string[1], []];
}
- $string = $this->coerceString($args[0]);
- $stringContent = $this->compileStringContent($string);
+ if ($startInt > 0) {
+ $startInt--;
+ }
- $start = (int) $args[1][1];
+ if ($endInt < 0) {
+ $endInt = Util::mbStrlen($stringContent) + $endInt;
+ } else {
+ $endInt--;
+ }
- if ($start > 0) {
- $start--;
+ if ($endInt < $startInt) {
+ return [Type::T_STRING, $string[1], []];
}
- $end = isset($args[2]) ? (int) $args[2][1] : -1;
- $length = $end < 0 ? $end + 1 : ($end > 0 ? $end - $start : $end);
+ $length = $endInt - $startInt + 1; // The end of the slice is inclusive
- $string[2] = $length
- ? [substr($stringContent, $start, $length)]
- : [substr($stringContent, $start)];
+ $string[2] = [Util::mbSubstr($stringContent, $startInt, $length)];
return $string;
}
@@ -8043,7 +9143,7 @@ class Compiler
protected static $libToLowerCase = ['string'];
protected function libToLowerCase($args)
{
- $string = $this->coerceString($args[0]);
+ $string = $this->assertString($args[0], 'string');
$stringContent = $this->compileStringContent($string);
$string[2] = [$this->stringTransformAsciiOnly($stringContent, 'strtolower')];
@@ -8054,7 +9154,7 @@ class Compiler
protected static $libToUpperCase = ['string'];
protected function libToUpperCase($args)
{
- $string = $this->coerceString($args[0]);
+ $string = $this->assertString($args[0], 'string');
$stringContent = $this->compileStringContent($string);
$string[2] = [$this->stringTransformAsciiOnly($stringContent, 'strtoupper')];
@@ -8092,7 +9192,7 @@ class Compiler
protected static $libFeatureExists = ['feature'];
protected function libFeatureExists($args)
{
- $string = $this->coerceString($args[0]);
+ $string = $this->assertString($args[0], 'feature');
$name = $this->compileStringContent($string);
return $this->toBool(
@@ -8103,18 +9203,18 @@ class Compiler
protected static $libFunctionExists = ['name'];
protected function libFunctionExists($args)
{
- $string = $this->coerceString($args[0]);
+ $string = $this->assertString($args[0], 'name');
$name = $this->compileStringContent($string);
// user defined functions
if ($this->has(static::$namespaces['function'] . $name)) {
- return true;
+ return self::$true;
}
$name = $this->normalizeName($name);
if (isset($this->userFunctions[$name])) {
- return true;
+ return self::$true;
}
// built-in functions
@@ -8126,30 +9226,31 @@ class Compiler
protected static $libGlobalVariableExists = ['name'];
protected function libGlobalVariableExists($args)
{
- $string = $this->coerceString($args[0]);
+ $string = $this->assertString($args[0], 'name');
$name = $this->compileStringContent($string);
- return $this->has($name, $this->rootEnv);
+ return $this->toBool($this->has($name, $this->rootEnv));
}
protected static $libMixinExists = ['name'];
protected function libMixinExists($args)
{
- $string = $this->coerceString($args[0]);
+ $string = $this->assertString($args[0], 'name');
$name = $this->compileStringContent($string);
- return $this->has(static::$namespaces['mixin'] . $name);
+ return $this->toBool($this->has(static::$namespaces['mixin'] . $name));
}
protected static $libVariableExists = ['name'];
protected function libVariableExists($args)
{
- $string = $this->coerceString($args[0]);
+ $string = $this->assertString($args[0], 'name');
$name = $this->compileStringContent($string);
- return $this->has($name);
+ return $this->toBool($this->has($name));
}
+ protected static $libCounter = ['args...'];
/**
* Workaround IE7's content counter bug.
*
@@ -8159,7 +9260,7 @@ class Compiler
*/
protected function libCounter($args)
{
- $list = array_map([$this, 'compileValue'], $args);
+ $list = array_map([$this, 'compileValue'], $args[0][2]);
return [Type::T_STRING, '', ['counter(' . implode(',', $list) . ')']];
}
@@ -8167,24 +9268,21 @@ class Compiler
protected static $libRandom = ['limit:null'];
protected function libRandom($args)
{
- if (isset($args[0]) & $args[0] !== static::$null) {
- $n = $this->assertNumber($args[0])->getDimension();
+ if (isset($args[0]) && $args[0] !== static::$null) {
+ $n = $this->assertInteger($args[0], 'limit');
if ($n < 1) {
- throw $this->error("\$limit must be greater than or equal to 1");
- }
-
- if (round($n - \intval($n), Number::PRECISION) > 0) {
- throw $this->error("Expected \$limit to be an integer but got $n for `random`");
+ throw new SassScriptException("\$limit: Must be greater than 0, was $n.");
}
- return new Number(mt_rand(1, \intval($n)), '');
+ return new Number(mt_rand(1, $n), '');
}
$max = mt_getrandmax();
return new Number(mt_rand(0, $max - 1) / $max, '');
}
+ protected static $libUniqueId = [];
protected function libUniqueId()
{
static $id;
@@ -8200,6 +9298,12 @@ class Compiler
return [Type::T_STRING, '', ['u' . str_pad(base_convert($id, 10, 36), 8, '0', STR_PAD_LEFT)]];
}
+ /**
+ * @param array|Number $value
+ * @param bool $force_enclosing_display
+ *
+ * @return array
+ */
protected function inspectFormatValue($value, $force_enclosing_display = false)
{
if ($value === static::$null) {
@@ -8208,6 +9312,10 @@ class Compiler
$stringValue = [$value];
+ if ($value instanceof Number) {
+ return [Type::T_STRING, '', $stringValue];
+ }
+
if ($value[0] === Type::T_LIST) {
if (end($value[2]) === static::$null) {
array_pop($value[2]);
@@ -8246,9 +9354,11 @@ class Compiler
/**
* Preprocess selector args
*
- * @param array $arg
+ * @param array $arg
+ * @param string|null $varname
+ * @param bool $allowParent
*
- * @return array|boolean
+ * @return array
*/
protected function getSelectorArg($arg, $varname = null, $allowParent = false)
{
@@ -8259,13 +9369,14 @@ class Compiler
}
if (! $this->checkSelectorArgType($arg)) {
- $var_display = ($varname ? ' $' . $varname . ':' : '');
$var_value = $this->compileValue($arg);
- throw $this->error("Error:{$var_display} $var_value is not a valid selector: it must be a string,"
- . " a list of strings, or a list of lists of strings");
+ throw SassScriptException::forArgument("$var_value is not a valid selector: it must be a string, a list of strings, or a list of lists of strings", $varname);
}
- $arg = $this->libUnquote([$arg]);
+
+ if ($arg[0] === Type::T_STRING) {
+ $arg[1] = '';
+ }
$arg = $this->compileValue($arg);
$parsedSelector = [];
@@ -8278,8 +9389,7 @@ class Compiler
foreach ($gluedSelector as $selector) {
foreach ($selector as $s) {
if (in_array(static::$selfSelector, $s)) {
- $var_display = ($varname ? ' $' . $varname . ':' : '');
- throw $this->error("Error:{$var_display} Parent selectors aren't allowed here.");
+ throw SassScriptException::forArgument("Parent selectors aren't allowed here.", $varname);
}
}
}
@@ -8288,8 +9398,7 @@ class Compiler
return $gluedSelector;
}
- $var_display = ($varname ? ' $' . $varname . ':' : '');
- throw $this->error("Error:{$var_display} expected more input, invalid selector.");
+ throw SassScriptException::forArgument("expected more input, invalid selector.", $varname);
}
/**
@@ -8319,11 +9428,11 @@ class Compiler
*
* @param array $selectors
*
- * @return string
+ * @return array
*/
protected function formatOutputSelector($selectors)
{
- $selectors = $this->collapseSelectors($selectors, true);
+ $selectors = $this->collapseSelectorsAsList($selectors);
return $selectors;
}
@@ -8336,7 +9445,7 @@ class Compiler
$super = $this->getSelectorArg($super, 'super');
$sub = $this->getSelectorArg($sub, 'sub');
- return $this->isSuperSelector($super, $sub);
+ return $this->toBool($this->isSuperSelector($super, $sub));
}
/**
@@ -8579,6 +9688,10 @@ class Compiler
$this->extendsMap = [];
foreach ($extendee as $es) {
+ if (\count($es) !== 1) {
+ throw $this->error('Can\'t extend complex selector.');
+ }
+
// only use the first one
$this->pushExtends(reset($es), $extender, null);
}
@@ -8679,7 +9792,7 @@ class Compiler
* @param array $compound1
* @param array $compound2
*
- * @return array|mixed
+ * @return array
*/
protected function unifyCompoundSelectors($compound1, $compound2)
{
@@ -8913,9 +10026,9 @@ class Compiler
/**
* Find the html tag name in a selector parts list
*
- * @param array $parts
+ * @param string[] $parts
*
- * @return mixed|string
+ * @return string
*/
protected function findTagName($parts)
{
@@ -8952,7 +10065,11 @@ class Compiler
protected static $libScssphpGlob = ['pattern'];
protected function libScssphpGlob($args)
{
- $string = $this->coerceString($args[0]);
+ @trigger_error(sprintf('The "scssphp-glob" function is deprecated an will be removed in ScssPhp 2.0. Register your own alternative through "%s::registerFunction', __CLASS__), E_USER_DEPRECATED);
+
+ $this->logger->warn('The "scssphp-glob" function is deprecated an will be removed in ScssPhp 2.0.', true);
+
+ $string = $this->assertString($args[0], 'pattern');
$pattern = $this->compileStringContent($string);
$matches = glob($pattern);
$listParts = [];
diff --git a/scssphp/scssphp/src/Compiler/CachedResult.php b/scssphp/scssphp/src/Compiler/CachedResult.php
new file mode 100644
index 00000000..a6629199
--- /dev/null
+++ b/scssphp/scssphp/src/Compiler/CachedResult.php
@@ -0,0 +1,77 @@
+<?php
+
+/**
+ * SCSSPHP
+ *
+ * @copyright 2012-2020 Leaf Corcoran
+ *
+ * @license http://opensource.org/licenses/MIT MIT
+ *
+ * @link http://scssphp.github.io/scssphp
+ */
+
+namespace ScssPhp\ScssPhp\Compiler;
+
+use ScssPhp\ScssPhp\CompilationResult;
+
+/**
+ * @internal
+ */
+class CachedResult
+{
+ /**
+ * @var CompilationResult
+ */
+ private $result;
+
+ /**
+ * @var array<string, int>
+ */
+ private $parsedFiles;
+
+ /**
+ * @var array
+ * @phpstan-var list<array{currentDir: string|null, path: string, filePath: string}>
+ */
+ private $resolvedImports;
+
+ /**
+ * @param CompilationResult $result
+ * @param array<string, int> $parsedFiles
+ * @param array $resolvedImports
+ *
+ * @phpstan-param list<array{currentDir: string|null, path: string, filePath: string}> $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<string, int>
+ */
+ public function getParsedFiles()
+ {
+ return $this->parsedFiles;
+ }
+
+ /**
+ * @return array
+ *
+ * @phpstan-return list<array{currentDir: string|null, path: string, filePath: string}>
+ */
+ public function getResolvedImports()
+ {
+ return $this->resolvedImports;
+ }
+}
diff --git a/scssphp/scssphp/src/Compiler/Environment.php b/scssphp/scssphp/src/Compiler/Environment.php
index dc2f86c1..306b15a6 100644
--- a/scssphp/scssphp/src/Compiler/Environment.php
+++ b/scssphp/scssphp/src/Compiler/Environment.php
@@ -16,16 +16,18 @@ namespace ScssPhp\ScssPhp\Compiler;
* Compiler environment
*
* @author Anthon Pang <anthon.pang@gmail.com>
+ *
+ * @internal
*/
class Environment
{
/**
- * @var \ScssPhp\ScssPhp\Block
+ * @var \ScssPhp\ScssPhp\Block|null
*/
public $block;
/**
- * @var \ScssPhp\ScssPhp\Compiler\Environment
+ * @var \ScssPhp\ScssPhp\Compiler\Environment|null
*/
public $parent;
diff --git a/scssphp/scssphp/src/Exception/CompilerException.php b/scssphp/scssphp/src/Exception/CompilerException.php
index 343da4c7..0b00cf52 100644
--- a/scssphp/scssphp/src/Exception/CompilerException.php
+++ b/scssphp/scssphp/src/Exception/CompilerException.php
@@ -16,6 +16,8 @@ namespace ScssPhp\ScssPhp\Exception;
* Compiler exception
*
* @author Oleksandr Savchenko <traveltino@gmail.com>
+ *
+ * @internal
*/
class CompilerException extends \Exception implements SassException
{
diff --git a/scssphp/scssphp/src/Exception/ParserException.php b/scssphp/scssphp/src/Exception/ParserException.php
index 5237f307..00d77ec9 100644
--- a/scssphp/scssphp/src/Exception/ParserException.php
+++ b/scssphp/scssphp/src/Exception/ParserException.php
@@ -16,6 +16,8 @@ namespace ScssPhp\ScssPhp\Exception;
* Parser Exception
*
* @author Oleksandr Savchenko <traveltino@gmail.com>
+ *
+ * @internal
*/
class ParserException extends \Exception implements SassException
{
diff --git a/scssphp/scssphp/src/Exception/RangeException.php b/scssphp/scssphp/src/Exception/RangeException.php
index b18c32d6..4be4dee7 100644
--- a/scssphp/scssphp/src/Exception/RangeException.php
+++ b/scssphp/scssphp/src/Exception/RangeException.php
@@ -16,6 +16,8 @@ namespace ScssPhp\ScssPhp\Exception;
* Range exception
*
* @author Anthon Pang <anthon.pang@gmail.com>
+ *
+ * @internal
*/
class RangeException extends \Exception implements SassException
{
diff --git a/scssphp/scssphp/src/Exception/ServerException.php b/scssphp/scssphp/src/Exception/ServerException.php
index ad5b3799..e593c401 100644
--- a/scssphp/scssphp/src/Exception/ServerException.php
+++ b/scssphp/scssphp/src/Exception/ServerException.php
@@ -12,10 +12,14 @@
namespace ScssPhp\ScssPhp\Exception;
+@trigger_error(sprintf('The "%s" class is deprecated.', ServerException::class), E_USER_DEPRECATED);
+
/**
* Server Exception
*
* @author Anthon Pang <anthon.pang@gmail.com>
+ *
+ * @deprecated The Scssphp server should define its own exception instead.
*/
class ServerException extends \Exception implements SassException
{
diff --git a/scssphp/scssphp/src/Formatter.php b/scssphp/scssphp/src/Formatter.php
index d52a6744..cc42ae80 100644
--- a/scssphp/scssphp/src/Formatter.php
+++ b/scssphp/scssphp/src/Formatter.php
@@ -19,6 +19,8 @@ use ScssPhp\ScssPhp\SourceMap\SourceMapGenerator;
* Base formatter
*
* @author Leaf Corcoran <leafot@gmail.com>
+ *
+ * @internal
*/
abstract class Formatter
{
@@ -78,7 +80,7 @@ abstract class Formatter
protected $currentColumn;
/**
- * @var \ScssPhp\ScssPhp\SourceMap\SourceMapGenerator
+ * @var \ScssPhp\ScssPhp\SourceMap\SourceMapGenerator|null
*/
protected $sourceMapGenerator;
@@ -139,6 +141,8 @@ abstract class Formatter
* Output lines inside a block
*
* @param \ScssPhp\ScssPhp\Formatter\OutputBlock $block
+ *
+ * @return void
*/
protected function blockLines(OutputBlock $block)
{
@@ -156,9 +160,13 @@ abstract class Formatter
* Output block selectors
*
* @param \ScssPhp\ScssPhp\Formatter\OutputBlock $block
+ *
+ * @return void
*/
protected function blockSelectors(OutputBlock $block)
{
+ assert(! empty($block->selectors));
+
$inner = $this->indentStr();
$this->write($inner
@@ -170,6 +178,8 @@ abstract class Formatter
* Output block children
*
* @param \ScssPhp\ScssPhp\Formatter\OutputBlock $block
+ *
+ * @return void
*/
protected function blockChildren(OutputBlock $block)
{
@@ -182,6 +192,8 @@ abstract class Formatter
* Output non-empty block
*
* @param \ScssPhp\ScssPhp\Formatter\OutputBlock $block
+ *
+ * @return void
*/
protected function block(OutputBlock $block)
{
@@ -285,6 +297,8 @@ abstract class Formatter
* Output content
*
* @param string $str
+ *
+ * @return void
*/
protected function write($str)
{
diff --git a/scssphp/scssphp/src/Formatter/Compact.php b/scssphp/scssphp/src/Formatter/Compact.php
index 249920ef..22f22688 100644
--- a/scssphp/scssphp/src/Formatter/Compact.php
+++ b/scssphp/scssphp/src/Formatter/Compact.php
@@ -20,6 +20,8 @@ use ScssPhp\ScssPhp\Formatter;
* @author Leaf Corcoran <leafot@gmail.com>
*
* @deprecated since 1.4.0. Use the Compressed formatter instead.
+ *
+ * @internal
*/
class Compact extends Formatter
{
diff --git a/scssphp/scssphp/src/Formatter/Compressed.php b/scssphp/scssphp/src/Formatter/Compressed.php
index d666a665..de13c188 100644
--- a/scssphp/scssphp/src/Formatter/Compressed.php
+++ b/scssphp/scssphp/src/Formatter/Compressed.php
@@ -18,6 +18,8 @@ use ScssPhp\ScssPhp\Formatter;
* Compressed formatter
*
* @author Leaf Corcoran <leafot@gmail.com>
+ *
+ * @internal
*/
class Compressed extends Formatter
{
@@ -67,6 +69,8 @@ class Compressed extends Formatter
*/
protected function blockSelectors(OutputBlock $block)
{
+ assert(! empty($block->selectors));
+
$inner = $this->indentStr();
$this->write(
diff --git a/scssphp/scssphp/src/Formatter/Crunched.php b/scssphp/scssphp/src/Formatter/Crunched.php
index 91c31443..2bc1e929 100644
--- a/scssphp/scssphp/src/Formatter/Crunched.php
+++ b/scssphp/scssphp/src/Formatter/Crunched.php
@@ -20,6 +20,8 @@ use ScssPhp\ScssPhp\Formatter;
* @author Anthon Pang <anthon.pang@gmail.com>
*
* @deprecated since 1.4.0. Use the Compressed formatter instead.
+ *
+ * @internal
*/
class Crunched extends Formatter
{
@@ -69,6 +71,8 @@ class Crunched extends Formatter
*/
protected function blockSelectors(OutputBlock $block)
{
+ assert(! empty($block->selectors));
+
$inner = $this->indentStr();
$this->write(
diff --git a/scssphp/scssphp/src/Formatter/Debug.php b/scssphp/scssphp/src/Formatter/Debug.php
index c676601b..b3f44225 100644
--- a/scssphp/scssphp/src/Formatter/Debug.php
+++ b/scssphp/scssphp/src/Formatter/Debug.php
@@ -20,6 +20,8 @@ use ScssPhp\ScssPhp\Formatter;
* @author Anthon Pang <anthon.pang@gmail.com>
*
* @deprecated since 1.4.0.
+ *
+ * @internal
*/
class Debug extends Formatter
{
diff --git a/scssphp/scssphp/src/Formatter/Expanded.php b/scssphp/scssphp/src/Formatter/Expanded.php
index b7cbde18..a280416d 100644
--- a/scssphp/scssphp/src/Formatter/Expanded.php
+++ b/scssphp/scssphp/src/Formatter/Expanded.php
@@ -18,6 +18,8 @@ use ScssPhp\ScssPhp\Formatter;
* Expanded formatter
*
* @author Leaf Corcoran <leafot@gmail.com>
+ *
+ * @internal
*/
class Expanded extends Formatter
{
diff --git a/scssphp/scssphp/src/Formatter/Nested.php b/scssphp/scssphp/src/Formatter/Nested.php
index 3249c182..9e729568 100644
--- a/scssphp/scssphp/src/Formatter/Nested.php
+++ b/scssphp/scssphp/src/Formatter/Nested.php
@@ -21,6 +21,8 @@ use ScssPhp\ScssPhp\Type;
* @author Leaf Corcoran <leafot@gmail.com>
*
* @deprecated since 1.4.0. Use the Expanded formatter instead.
+ *
+ * @internal
*/
class Nested extends Formatter
{
diff --git a/scssphp/scssphp/src/Formatter/OutputBlock.php b/scssphp/scssphp/src/Formatter/OutputBlock.php
index fe0321bd..88deb2d3 100644
--- a/scssphp/scssphp/src/Formatter/OutputBlock.php
+++ b/scssphp/scssphp/src/Formatter/OutputBlock.php
@@ -16,6 +16,8 @@ namespace ScssPhp\ScssPhp\Formatter;
* Output block
*
* @author Anthon Pang <anthon.pang@gmail.com>
+ *
+ * @internal
*/
class OutputBlock
{
@@ -30,17 +32,17 @@ class OutputBlock
public $depth;
/**
- * @var array
+ * @var array|null
*/
public $selectors;
/**
- * @var array
+ * @var string[]
*/
public $lines;
/**
- * @var array
+ * @var OutputBlock[]
*/
public $children;
@@ -50,17 +52,17 @@ class OutputBlock
public $parent;
/**
- * @var string
+ * @var string|null
*/
public $sourceName;
/**
- * @var integer
+ * @var integer|null
*/
public $sourceLine;
/**
- * @var integer
+ * @var integer|null
*/
public $sourceColumn;
}
diff --git a/scssphp/scssphp/src/Logger/LoggerInterface.php b/scssphp/scssphp/src/Logger/LoggerInterface.php
new file mode 100644
index 00000000..7c0a2f76
--- /dev/null
+++ b/scssphp/scssphp/src/Logger/LoggerInterface.php
@@ -0,0 +1,48 @@
+<?php
+
+/**
+ * SCSSPHP
+ *
+ * @copyright 2012-2020 Leaf Corcoran
+ *
+ * @license http://opensource.org/licenses/MIT MIT
+ *
+ * @link http://scssphp.github.io/scssphp
+ */
+
+namespace ScssPhp\ScssPhp\Logger;
+
+/**
+ * Interface implemented by loggers for warnings and debug messages.
+ *
+ * The official Sass implementation recommends that loggers report the
+ * messages immediately rather than waiting for the end of the
+ * compilation, to provide a better debugging experience when the
+ * compilation does not end (error or infinite loop after the warning
+ * for instance).
+ */
+interface LoggerInterface
+{
+ /**
+ * Emits a warning with the given message.
+ *
+ * If $deprecation is true, it indicates that this is a deprecation
+ * warning. Implementations should surface all this information to
+ * the end user.
+ *
+ * @param string $message
+ * @param bool $deprecation
+ *
+ * @return void
+ */
+ public function warn($message, $deprecation = false);
+
+ /**
+ * Emits a debugging message.
+ *
+ * @param string $message
+ *
+ * @return void
+ */
+ public function debug($message);
+}
diff --git a/scssphp/scssphp/src/Logger/QuietLogger.php b/scssphp/scssphp/src/Logger/QuietLogger.php
new file mode 100644
index 00000000..0f358c64
--- /dev/null
+++ b/scssphp/scssphp/src/Logger/QuietLogger.php
@@ -0,0 +1,27 @@
+<?php
+
+/**
+ * SCSSPHP
+ *
+ * @copyright 2012-2020 Leaf Corcoran
+ *
+ * @license http://opensource.org/licenses/MIT MIT
+ *
+ * @link http://scssphp.github.io/scssphp
+ */
+
+namespace ScssPhp\ScssPhp\Logger;
+
+/**
+ * A logger that silently ignores all messages.
+ */
+class QuietLogger implements LoggerInterface
+{
+ public function warn($message, $deprecation = false)
+ {
+ }
+
+ public function debug($message)
+ {
+ }
+}
diff --git a/scssphp/scssphp/src/Logger/StreamLogger.php b/scssphp/scssphp/src/Logger/StreamLogger.php
new file mode 100644
index 00000000..f5da6c9c
--- /dev/null
+++ b/scssphp/scssphp/src/Logger/StreamLogger.php
@@ -0,0 +1,60 @@
+<?php
+
+/**
+ * SCSSPHP
+ *
+ * @copyright 2012-2020 Leaf Corcoran
+ *
+ * @license http://opensource.org/licenses/MIT MIT
+ *
+ * @link http://scssphp.github.io/scssphp
+ */
+
+namespace ScssPhp\ScssPhp\Logger;
+
+/**
+ * A logger that prints to a PHP stream (for instance stderr)
+ */
+class StreamLogger implements LoggerInterface
+{
+ private $stream;
+ private $closeOnDestruct;
+
+ /**
+ * @param resource $stream A stream resource
+ * @param bool $closeOnDestruct If true, takes ownership of the stream and close it on destruct to avoid leaks.
+ */
+ public function __construct($stream, $closeOnDestruct = false)
+ {
+ $this->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/scssphp/scssphp/src/Node.php b/scssphp/scssphp/src/Node.php
index 60d357e0..53019375 100644
--- a/scssphp/scssphp/src/Node.php
+++ b/scssphp/scssphp/src/Node.php
@@ -16,6 +16,8 @@ namespace ScssPhp\ScssPhp;
* Base node
*
* @author Anthon Pang <anthon.pang@gmail.com>
+ *
+ * @internal
*/
abstract class Node
{
@@ -30,12 +32,12 @@ abstract class Node
public $sourceIndex;
/**
- * @var integer
+ * @var int|null
*/
public $sourceLine;
/**
- * @var integer
+ * @var int|null
*/
public $sourceColumn;
}
diff --git a/scssphp/scssphp/src/Node/Number.php b/scssphp/scssphp/src/Node/Number.php
index 166de50d..b326906b 100644
--- a/scssphp/scssphp/src/Node/Number.php
+++ b/scssphp/scssphp/src/Node/Number.php
@@ -12,10 +12,13 @@
namespace ScssPhp\ScssPhp\Node;
+use ScssPhp\ScssPhp\Base\Range;
use ScssPhp\ScssPhp\Compiler;
+use ScssPhp\ScssPhp\Exception\RangeException;
use ScssPhp\ScssPhp\Exception\SassScriptException;
use ScssPhp\ScssPhp\Node;
use ScssPhp\ScssPhp\Type;
+use ScssPhp\ScssPhp\Util;
/**
* Dimension + optional units
@@ -27,6 +30,8 @@ use ScssPhp\ScssPhp\Type;
* }}
*
* @author Anthon Pang <anthon.pang@gmail.com>
+ *
+ * @template-implements \ArrayAccess<int, mixed>
*/
class Number extends Node implements \ArrayAccess
{
@@ -42,6 +47,7 @@ class Number extends Node implements \ArrayAccess
* @see http://www.w3.org/TR/2012/WD-css3-values-20120308/
*
* @var array
+ * @phpstan-var array<string, array<string, float|int>>
*/
protected static $unitTable = [
'in' => [
@@ -143,6 +149,7 @@ class Number extends Node implements \ArrayAccess
/**
* {@inheritdoc}
*/
+ #[\ReturnTypeWillChange]
public function offsetExists($offset)
{
if ($offset === -3) {
@@ -168,6 +175,7 @@ class Number extends Node implements \ArrayAccess
/**
* {@inheritdoc}
*/
+ #[\ReturnTypeWillChange]
public function offsetGet($offset)
{
switch ($offset) {
@@ -194,6 +202,7 @@ class Number extends Node implements \ArrayAccess
/**
* {@inheritdoc}
*/
+ #[\ReturnTypeWillChange]
public function offsetSet($offset, $value)
{
throw new \BadMethodCallException('Number is immutable');
@@ -202,6 +211,7 @@ class Number extends Node implements \ArrayAccess
/**
* {@inheritdoc}
*/
+ #[\ReturnTypeWillChange]
public function offsetUnset($offset)
{
throw new \BadMethodCallException('Number is immutable');
@@ -244,6 +254,23 @@ class Number extends Node implements \ArrayAccess
}
/**
+ * @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
@@ -254,7 +281,22 @@ class Number extends Node implements \ArrayAccess
return;
}
- throw SassScriptException::forArgument(sprintf('Expected %s to have no units', $this), $varName);
+ throw SassScriptException::forArgument(sprintf('Expected %s to have no units.', $this), $varName);
+ }
+
+ /**
+ * @param string $unit
+ * @param string|null $varName
+ *
+ * @return void
+ */
+ public function assertUnit($unit, $varName = null)
+ {
+ if ($this->hasUnit($unit)) {
+ return;
+ }
+
+ throw SassScriptException::forArgument(sprintf('Expected %s to have unit "%s".', $this, $unit), $varName);
}
/**
@@ -280,6 +322,29 @@ class Number extends Node implements \ArrayAccess
}
/**
+ * Returns a copy of this number, converted to the units represented by $newNumeratorUnits and $newDenominatorUnits.
+ *
+ * This does not throw an error if this number is unitless and
+ * $newNumeratorUnits/$newDenominatorUnits are not empty, or vice versa. Instead,
+ * it treats all unitless numbers as convertible to and from all units without
+ * changing the value.
+ *
+ * @param string[] $newNumeratorUnits
+ * @param string[] $newDenominatorUnits
+ *
+ * @return Number
+ *
+ * @phpstan-param list<string> $newNumeratorUnits
+ * @phpstan-param list<string> $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
@@ -560,6 +625,8 @@ class Number extends Node implements \ArrayAccess
*
* @phpstan-param list<string> $numeratorUnits
* @phpstan-param list<string> $denominatorUnits
+ *
+ * @throws SassScriptException if this number's units are not compatible with $numeratorUnits and $denominatorUnits
*/
private function valueInUnits(array $numeratorUnits, array $denominatorUnits)
{
diff --git a/scssphp/scssphp/src/Parser.php b/scssphp/scssphp/src/Parser.php
index 1faa82f8..3ba2f67f 100644
--- a/scssphp/scssphp/src/Parser.php
+++ b/scssphp/scssphp/src/Parser.php
@@ -13,11 +13,15 @@
namespace ScssPhp\ScssPhp;
use ScssPhp\ScssPhp\Exception\ParserException;
+use ScssPhp\ScssPhp\Logger\LoggerInterface;
+use ScssPhp\ScssPhp\Logger\QuietLogger;
/**
* Parser
*
* @author Leaf Corcoran <leafot@gmail.com>
+ *
+ * @internal
*/
class Parser
{
@@ -80,7 +84,7 @@ class Parser
*/
private $count;
/**
- * @var Block
+ * @var Block|null
*/
private $env;
/**
@@ -111,17 +115,23 @@ class Parser
private $cssOnly;
/**
+ * @var LoggerInterface
+ */
+ private $logger;
+
+ /**
* Constructor
*
* @api
*
- * @param string $sourceName
- * @param integer $sourceIndex
- * @param string|null $encoding
- * @param Cache|null $cache
- * @param bool $cssOnly
+ * @param string|null $sourceName
+ * @param integer $sourceIndex
+ * @param string|null $encoding
+ * @param Cache|null $cache
+ * @param bool $cssOnly
+ * @param LoggerInterface|null $logger
*/
- public function __construct($sourceName, $sourceIndex = 0, $encoding = 'utf-8', Cache $cache = null, $cssOnly = false)
+ public function __construct($sourceName, $sourceIndex = 0, $encoding = 'utf-8', Cache $cache = null, $cssOnly = false, LoggerInterface $logger = null)
{
$this->sourceName = $sourceName ?: '(stdin)';
$this->sourceIndex = $sourceIndex;
@@ -132,6 +142,7 @@ class Parser
$this->commentsSeen = [];
$this->allowVars = true;
$this->cssOnly = $cssOnly;
+ $this->logger = $logger ?: new QuietLogger();
if (empty(static::$operatorPattern)) {
static::$operatorPattern = '([*\/%+-]|[!=]\=|\>\=?|\<\=?|and|or)';
@@ -168,6 +179,8 @@ class Parser
*
* @param string $msg
*
+ * @phpstan-return never-return
+ *
* @throws ParserException
*
* @deprecated use "parseError" and throw the exception in the caller instead.
@@ -514,6 +527,10 @@ class Parser
) {
! $this->cssOnly || $this->assertPlainCssValid(false, $s);
+ list($line, $column) = $this->getSourcePosition($s);
+ $file = $this->sourceName;
+ $this->logger->warn("The \"@scssphp-import-once\" directive is deprecated and will be removed in ScssPhp 2.0, in \"$file\", line $line, column $column.", true);
+
$this->append([Type::T_SCSSPHP_IMPORT_ONCE, $importPath], $s);
return true;
@@ -973,11 +990,6 @@ class Parser
$this->seek($s);
- // misc
- if ($this->literal('-->', 3)) {
- return true;
- }
-
// opening css block
if (
$this->selectors($selectors) &&
@@ -1065,10 +1077,7 @@ class Parser
}
// extra stuff
- if (
- $this->matchChar(';') ||
- $this->literal('<!--', 4)
- ) {
+ if ($this->matchChar(';')) {
return true;
}
@@ -1187,7 +1196,7 @@ class Parser
}
$r = '/' . $regex . '/' . $this->patternModifiers;
- $result = preg_match($r, $this->buffer, $out, null, $from);
+ $result = preg_match($r, $this->buffer, $out, 0, $from);
return $result;
}
@@ -1255,6 +1264,7 @@ class Parser
'grayscale',
'hsl',
'hsla',
+ 'hwb',
'invert',
'linear-gradient',
'min',
@@ -1459,7 +1469,7 @@ class Parser
{
$r = '/' . $regex . '/' . $this->patternModifiers;
- if (! preg_match($r, $this->buffer, $out, null, $this->count)) {
+ if (! preg_match($r, $this->buffer, $out, 0, $this->count)) {
return false;
}
@@ -1540,7 +1550,7 @@ class Parser
{
$gotWhite = false;
- while (preg_match(static::$whitePattern, $this->buffer, $m, null, $this->count)) {
+ while (preg_match(static::$whitePattern, $this->buffer, $m, 0, $this->count)) {
if (isset($m[1]) && empty($this->commentsSeen[$this->count])) {
// comment that are kept in the output CSS
$comment = [];
@@ -1568,6 +1578,11 @@ class Parser
$comment[] = [Type::T_COMMENT, substr($this->buffer, $p, $this->count - $p), $out];
} else {
+ list($line, $column) = $this->getSourcePosition($this->count);
+ $file = $this->sourceName;
+ if (!$this->discardComments) {
+ $this->logger->warn("Unterminated interpolations in multiline comments are deprecated and will be removed in ScssPhp 2.0, in \"$file\", line $line, column $column.", true);
+ }
$comment[] = substr($this->buffer, $this->count, 2);
$this->count += 2;
@@ -1585,7 +1600,14 @@ class Parser
} else {
$comment[] = $c;
$staticComment = substr($this->buffer, $startCommentCount, $endCommentCount - $startCommentCount);
- $this->appendComment([Type::T_COMMENT, $staticComment, [Type::T_STRING, '', $comment]]);
+ $commentStatement = [Type::T_COMMENT, $staticComment, [Type::T_STRING, '', $comment]];
+
+ list($line, $column) = $this->getSourcePosition($startCommentCount);
+ $commentStatement[self::SOURCE_LINE] = $line;
+ $commentStatement[self::SOURCE_COLUMN] = $column;
+ $commentStatement[self::SOURCE_INDEX] = $this->sourceIndex;
+
+ $this->appendComment($commentStatement);
}
$this->commentsSeen[$startCommentCount] = true;
@@ -2395,10 +2417,8 @@ class Parser
break;
}
- // peek and see if rhs belongs to next operator
- if ($this->peek($operators, $next) && static::$precedence[$next[1]] > static::$precedence[$op]) {
- $rhs = $this->expHelper($rhs, static::$precedence[$next[1]]);
- }
+ // consume higher-precedence operators on the right-hand side
+ $rhs = $this->expHelper($rhs, static::$precedence[$op] + 1);
$lhs = [Type::T_EXPRESSION, $op, $lhs, $rhs, $this->inParens, $whiteBefore, $whiteAfter];
@@ -3303,7 +3323,7 @@ class Parser
}
// match comment hack
- if (preg_match(static::$whitePattern, $this->buffer, $m, null, $this->count)) {
+ if (preg_match(static::$whitePattern, $this->buffer, $m, 0, $this->count)) {
if (! empty($m[0])) {
$parts[] = $m[0];
$this->count += \strlen($m[0]);
@@ -4074,11 +4094,20 @@ class Parser
}
/**
- * Save internal encoding
+ * Save internal encoding of mbstring
+ *
+ * When mbstring.func_overload is used to replace the standard PHP string functions,
+ * this method configures the internal encoding to a single-byte one so that the
+ * behavior matches the normal behavior of PHP string functions while using the parser.
+ * The existing internal encoding is saved and will be restored when calling {@see restoreEncoding}.
+ *
+ * If mbstring.func_overload is not used (or does not override string functions), this method is a no-op.
+ *
+ * @return void
*/
private function saveEncoding()
{
- if (\extension_loaded('mbstring')) {
+ if (\PHP_VERSION_ID < 80000 && \extension_loaded('mbstring') && (2 & (int) ini_get('mbstring.func_overload')) > 0) {
$this->encoding = mb_internal_encoding();
mb_internal_encoding('iso-8859-1');
@@ -4087,6 +4116,8 @@ class Parser
/**
* Restore internal encoding
+ *
+ * @return void
*/
private function restoreEncoding()
{
diff --git a/scssphp/scssphp/src/SourceMap/Base64.php b/scssphp/scssphp/src/SourceMap/Base64.php
index 5fc56b25..4a5ed8bb 100644
--- a/scssphp/scssphp/src/SourceMap/Base64.php
+++ b/scssphp/scssphp/src/SourceMap/Base64.php
@@ -16,11 +16,13 @@ namespace ScssPhp\ScssPhp\SourceMap;
* Base 64 Encode/Decode
*
* @author Anthon Pang <anthon.pang@gmail.com>
+ *
+ * @internal
*/
class Base64
{
/**
- * @var array
+ * @var array<int, string>
*/
private static $encodingMap = [
0 => 'A',
@@ -90,7 +92,7 @@ class Base64
];
/**
- * @var array
+ * @var array<string|int, int>
*/
private static $decodingMap = [
'A' => 0,
diff --git a/scssphp/scssphp/src/SourceMap/Base64VLQ.php b/scssphp/scssphp/src/SourceMap/Base64VLQ.php
index 11acfbc8..d47b96a1 100644
--- a/scssphp/scssphp/src/SourceMap/Base64VLQ.php
+++ b/scssphp/scssphp/src/SourceMap/Base64VLQ.php
@@ -34,6 +34,8 @@ namespace ScssPhp\ScssPhp\SourceMap;
*
* @author John Lenz <johnlenz@google.com>
* @author Anthon Pang <anthon.pang@gmail.com>
+ *
+ * @internal
*/
class Base64VLQ
{
diff --git a/scssphp/scssphp/src/SourceMap/SourceMapGenerator.php b/scssphp/scssphp/src/SourceMap/SourceMapGenerator.php
index 2d956351..4f14bdce 100644
--- a/scssphp/scssphp/src/SourceMap/SourceMapGenerator.php
+++ b/scssphp/scssphp/src/SourceMap/SourceMapGenerator.php
@@ -21,6 +21,8 @@ use ScssPhp\ScssPhp\Exception\CompilerException;
*
* @author Josh Schmidt <oyejorge@gmail.com>
* @author Nicolas FRANÇOIS <nicolas.francois@frog-labs.com>
+ *
+ * @internal
*/
class SourceMapGenerator
{
@@ -33,6 +35,7 @@ class SourceMapGenerator
* Array of default options
*
* @var array
+ * @phpstan-var array{sourceRoot: string, sourceMapFilename: string|null, sourceMapURL: string|null, sourceMapWriteTo: string|null, outputSourceFiles: bool, sourceMapRootpath: string, sourceMapBasepath: string}
*/
protected $defaultOptions = [
// an optional source root, useful for relocating source files
@@ -70,6 +73,7 @@ class SourceMapGenerator
* Array of mappings
*
* @var array
+ * @phpstan-var list<array{generated_line: int, generated_column: int, original_line: int, original_column: int, source_file: string}>
*/
protected $mappings = [];
@@ -83,16 +87,24 @@ class SourceMapGenerator
/**
* File to content map
*
- * @var array
+ * @var array<string, string>
*/
protected $sources = [];
+
+ /**
+ * @var array<string, int>
+ */
protected $sourceKeys = [];
/**
* @var array
+ * @phpstan-var array{sourceRoot: string, sourceMapFilename: string|null, sourceMapURL: string|null, sourceMapWriteTo: string|null, outputSourceFiles: bool, sourceMapRootpath: string, sourceMapBasepath: string}
*/
private $options;
+ /**
+ * @phpstan-param array{sourceRoot?: string, sourceMapFilename?: string|null, sourceMapURL?: string|null, sourceMapWriteTo?: string|null, outputSourceFiles?: bool, sourceMapRootpath?: string, sourceMapBasepath?: string} $options
+ */
public function __construct(array $options = [])
{
$this->options = array_merge($this->defaultOptions, $options);
@@ -107,6 +119,8 @@ class SourceMapGenerator
* @param integer $originalLine The line number in original file
* @param integer $originalColumn The column number in original file
* @param string $sourceFile The original source file
+ *
+ * @return void
*/
public function addMapping($generatedLine, $generatedColumn, $originalLine, $originalColumn, $sourceFile)
{
@@ -129,6 +143,7 @@ class SourceMapGenerator
* @return string
*
* @throws \ScssPhp\ScssPhp\Exception\CompilerException If the file could not be saved
+ * @deprecated
*/
public function saveMap($content)
{
@@ -214,7 +229,7 @@ class SourceMapGenerator
/**
* Returns the sources contents
*
- * @return array|null
+ * @return string[]|null
*/
protected function getSourcesContent()
{
diff --git a/scssphp/scssphp/src/Type.php b/scssphp/scssphp/src/Type.php
index 549b08ed..fb2a1d7f 100644
--- a/scssphp/scssphp/src/Type.php
+++ b/scssphp/scssphp/src/Type.php
@@ -45,6 +45,7 @@ class Type
const T_FUNCTION_REFERENCE = 'function-reference';
const T_FUNCTION_CALL = 'fncall';
const T_HSL = 'hsl';
+ const T_HWB = 'hwb';
const T_IF = 'if';
const T_IMPORT = 'import';
const T_INCLUDE = 'include';
diff --git a/scssphp/scssphp/src/Util.php b/scssphp/scssphp/src/Util.php
index d32e0c34..62cd2a20 100644
--- a/scssphp/scssphp/src/Util.php
+++ b/scssphp/scssphp/src/Util.php
@@ -14,11 +14,14 @@ namespace ScssPhp\ScssPhp;
use ScssPhp\ScssPhp\Base\Range;
use ScssPhp\ScssPhp\Exception\RangeException;
+use ScssPhp\ScssPhp\Node\Number;
/**
- * Utilty functions
+ * Utility functions
*
* @author Anthon Pang <anthon.pang@gmail.com>
+ *
+ * @internal
*/
class Util
{
@@ -26,10 +29,10 @@ class Util
* Asserts that `value` falls within `range` (inclusive), leaving
* room for slight floating-point errors.
*
- * @param string $name The name of the value. Used in the error message.
- * @param \ScssPhp\ScssPhp\Base\Range $range Range of values.
- * @param array $value The value to check.
- * @param string $unit The unit of the value. Used in error reporting.
+ * @param string $name The name of the value. Used in the error message.
+ * @param Range $range Range of values.
+ * @param array|Number $value The value to check.
+ * @param string $unit The unit of the value. Used in error reporting.
*
* @return mixed `value` adjusted to fall within range, if it was outside by a floating-point margin.
*
@@ -115,10 +118,10 @@ class Util
}
if (\function_exists('iconv_strlen')) {
- return @iconv_strlen($string, 'UTF-8');
+ return (int) @iconv_strlen($string, 'UTF-8');
}
- return strlen($string);
+ throw new \LogicException('Either mbstring (recommended) or iconv is necessary to use Scssphp.');
}
/**
@@ -155,7 +158,7 @@ class Util
return (string)iconv_substr($string, $start, $length, 'UTF-8');
}
- return substr($string, $start, $length);
+ throw new \LogicException('Either mbstring (recommended) or iconv is necessary to use Scssphp.');
}
/**
@@ -176,6 +179,6 @@ class Util
return iconv_strpos($haystack, $needle, $offset, 'UTF-8');
}
- return strpos($haystack, $needle, $offset);
+ throw new \LogicException('Either mbstring (recommended) or iconv is necessary to use Scssphp.');
}
}
diff --git a/scssphp/scssphp/src/ValueConverter.php b/scssphp/scssphp/src/ValueConverter.php
new file mode 100644
index 00000000..e12a0eb5
--- /dev/null
+++ b/scssphp/scssphp/src/ValueConverter.php
@@ -0,0 +1,95 @@
+<?php
+
+/**
+ * SCSSPHP
+ *
+ * @copyright 2012-2020 Leaf Corcoran
+ *
+ * @license http://opensource.org/licenses/MIT MIT
+ *
+ * @link http://scssphp.github.io/scssphp
+ */
+
+namespace ScssPhp\ScssPhp;
+
+use ScssPhp\ScssPhp\Node\Number;
+
+final class ValueConverter
+{
+ // Prevent instantiating it
+ private function __construct()
+ {
+ }
+
+ /**
+ * Parses a value from a Scss source string.
+ *
+ * The returned value is guaranteed to be supported by the
+ * Compiler methods for registering custom variables. No other
+ * guarantee about it is provided. It should be considered
+ * opaque values by the caller.
+ *
+ * @param string $source
+ *
+ * @return mixed
+ */
+ public static function parseValue($source)
+ {
+ $parser = new Parser(__CLASS__);
+
+ if (!$parser->parseValue($source, $value)) {
+ throw new \InvalidArgumentException(sprintf('Invalid value source "%s".', $source));
+ }
+
+ return $value;
+ }
+
+ /**
+ * Converts a PHP value to a Sass value
+ *
+ * The returned value is guaranteed to be supported by the
+ * Compiler methods for registering custom variables. No other
+ * guarantee about it is provided. It should be considered
+ * opaque values by the caller.
+ *
+ * @param mixed $value
+ *
+ * @return mixed
+ */
+ public static function fromPhp($value)
+ {
+ if ($value instanceof Number) {
+ return $value;
+ }
+
+ if (is_array($value) && isset($value[0]) && \in_array($value[0], [Type::T_NULL, Type::T_COLOR, Type::T_KEYWORD, Type::T_LIST, Type::T_MAP, Type::T_STRING])) {
+ return $value;
+ }
+
+ if ($value === null) {
+ return Compiler::$null;
+ }
+
+ if ($value === true) {
+ return Compiler::$true;
+ }
+
+ if ($value === false) {
+ return Compiler::$false;
+ }
+
+ if ($value === '') {
+ return Compiler::$emptyString;
+ }
+
+ if (\is_int($value) || \is_float($value)) {
+ return new Number($value, '');
+ }
+
+ if (\is_string($value)) {
+ return [Type::T_STRING, '"', [$value]];
+ }
+
+ throw new \InvalidArgumentException(sprintf('Cannot convert the value of type "%s" to a Sass value.', gettype($value)));
+ }
+}
diff --git a/scssphp/scssphp/src/Version.php b/scssphp/scssphp/src/Version.php
index b1e7d6de..62c8006a 100644
--- a/scssphp/scssphp/src/Version.php
+++ b/scssphp/scssphp/src/Version.php
@@ -19,5 +19,5 @@ namespace ScssPhp\ScssPhp;
*/
class Version
{
- const VERSION = '1.4.1';
+ const VERSION = '1.8.1';
}
diff --git a/scssphp/scssphp/src/Warn.php b/scssphp/scssphp/src/Warn.php
new file mode 100644
index 00000000..592b44c7
--- /dev/null
+++ b/scssphp/scssphp/src/Warn.php
@@ -0,0 +1,84 @@
+<?php
+
+/**
+ * SCSSPHP
+ *
+ * @copyright 2012-2020 Leaf Corcoran
+ *
+ * @license http://opensource.org/licenses/MIT MIT
+ *
+ * @link http://scssphp.github.io/scssphp
+ */
+
+namespace ScssPhp\ScssPhp;
+
+final class Warn
+{
+ /**
+ * @var callable|null
+ * @phpstan-var (callable(string, bool): void)|null
+ */
+ private static $callback;
+
+ /**
+ * Prints a warning message associated with the current `@import` or function call.
+ *
+ * This may only be called within a custom function or importer callback.
+ *
+ * @param string $message
+ *
+ * @return void
+ */
+ public static function warning($message)
+ {
+ self::reportWarning($message, false);
+ }
+
+ /**
+ * Prints a deprecation warning message associated with the current `@import` or function call.
+ *
+ * This may only be called within a custom function or importer callback.
+ *
+ * @param string $message
+ *
+ * @return void
+ */
+ public static function deprecation($message)
+ {
+ self::reportWarning($message, true);
+ }
+
+ /**
+ * @param callable|null $callback
+ *
+ * @return callable|null The previous warn callback
+ *
+ * @phpstan-param (callable(string, bool): void)|null $callback
+ *
+ * @phpstan-return (callable(string, bool): void)|null
+ *
+ * @internal
+ */
+ public static function setCallback(callable $callback = null)
+ {
+ $previousCallback = self::$callback;
+ self::$callback = $callback;
+
+ return $previousCallback;
+ }
+
+ /**
+ * @param string $message
+ * @param bool $deprecation
+ *
+ * @return void
+ */
+ private static function reportWarning($message, $deprecation)
+ {
+ if (self::$callback === null) {
+ throw new \BadMethodCallException('The warning Reporter may only be called within a custom function or importer callback.');
+ }
+
+ \call_user_func(self::$callback, $message, $deprecation);
+ }
+}