From b4c6688b8653369cb7ce79a68687828826cdc5f7 Mon Sep 17 00:00:00 2001 From: Skylar Bolton Date: Thu, 5 Mar 2026 23:03:16 -0500 Subject: [PATCH 1/2] Upgrade scssphp to 1.13.0, fix PHP 8.4 deprecations MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replaces the embedded scssphp library with the latest 1.x release (v1.13.0, Aug 2023), which is the highest version before the breaking 2.0.0 rewrite. Also applies explicit nullable type fixes (Type $x = null → ?Type $x = null) to suppress PHP 8.4 deprecation notices. --- scssphp/composer.json | 11 +- scssphp/scss.inc.php | 4 +- scssphp/src/Base/Range.php | 4 +- scssphp/src/Block/AtRootBlock.php | 2 +- scssphp/src/Block/CallableBlock.php | 5 +- scssphp/src/Block/ContentBlock.php | 2 +- scssphp/src/Block/DirectiveBlock.php | 5 +- scssphp/src/Block/EachBlock.php | 5 +- scssphp/src/Block/ElseBlock.php | 2 +- scssphp/src/Block/ElseifBlock.php | 5 +- scssphp/src/Block/ForBlock.php | 7 +- scssphp/src/Block/IfBlock.php | 5 +- scssphp/src/Block/MediaBlock.php | 5 +- scssphp/src/Block/NestedPropertyBlock.php | 2 +- scssphp/src/Block/WhileBlock.php | 2 +- scssphp/src/Cache.php | 1 + scssphp/src/Colors.php | 23 +- scssphp/src/CompilationResult.php | 13 +- scssphp/src/Compiler.php | 2037 +++++++++++------ scssphp/src/Compiler/CachedResult.php | 8 +- scssphp/src/Compiler/Environment.php | 2 +- scssphp/src/Exception/CompilerException.php | 2 +- scssphp/src/Exception/ParserException.php | 11 +- scssphp/src/Exception/RangeException.php | 2 +- scssphp/src/Exception/SassException.php | 12 +- scssphp/src/Exception/SassScriptException.php | 11 +- scssphp/src/Exception/ServerException.php | 26 + scssphp/src/Formatter.php | 60 +- scssphp/src/Formatter/Compact.php | 52 + scssphp/src/Formatter/Compressed.php | 6 +- scssphp/src/Formatter/Crunched.php | 87 + scssphp/src/Formatter/Debug.php | 127 + scssphp/src/Formatter/Expanded.php | 6 +- scssphp/src/Formatter/Nested.php | 238 ++ scssphp/src/Formatter/OutputBlock.php | 2 +- scssphp/src/Logger/LoggerInterface.php | 4 +- scssphp/src/Logger/QuietLogger.php | 11 +- scssphp/src/Logger/StreamLogger.php | 10 +- scssphp/src/Node/Number.php | 30 +- scssphp/src/OutputStyle.php | 53 + scssphp/src/Parser.php | 397 ++-- scssphp/src/SourceMap/Base64.php | 2 +- scssphp/src/SourceMap/Base64VLQ.php | 2 +- scssphp/src/SourceMap/SourceMapGenerator.php | 67 +- scssphp/src/Type.php | 20 +- scssphp/src/Util.php | 115 +- scssphp/src/Util/Path.php | 52 +- scssphp/src/ValueConverter.php | 2 +- scssphp/src/Version.php | 4 +- scssphp/src/Warn.php | 8 +- 50 files changed, 2474 insertions(+), 1095 deletions(-) create mode 100644 scssphp/src/Exception/ServerException.php create mode 100644 scssphp/src/Formatter/Compact.php create mode 100644 scssphp/src/Formatter/Crunched.php create mode 100644 scssphp/src/Formatter/Debug.php create mode 100644 scssphp/src/Formatter/Nested.php diff --git a/scssphp/composer.json b/scssphp/composer.json index dbb5497..d17ffb9 100644 --- a/scssphp/composer.json +++ b/scssphp/composer.json @@ -26,11 +26,9 @@ "psr-4": { "ScssPhp\\ScssPhp\\Tests\\": "tests/" } }, "require": { - "php": ">=7.2", - "ext-ctype": "*", + "php": ">=5.6.0", "ext-json": "*", - "league/uri": "^6.4.0", - "league/uri-interfaces": "^2.3" + "ext-ctype": "*" }, "suggest": { "ext-mbstring": "For best performance, mbstring should be installed as it is faster than ext-iconv", @@ -38,14 +36,14 @@ }, "require-dev": { "bamarni/composer-bin-plugin": "^1.4", - "phpunit/phpunit": "^8.5 || ^9.5", + "phpunit/phpunit": "^5.7 || ^6.5 || ^7.5 || ^8.3 || ^9.4", "sass/sass-spec": "*", "squizlabs/php_codesniffer": "~3.5", "symfony/phpunit-bridge": "^5.1", "thoughtbot/bourbon": "^7.0", "twbs/bootstrap": "~5.0", "twbs/bootstrap4": "4.6.1", - "zurb/foundation": "~6.5" + "zurb/foundation": "~6.7.0" }, "repositories": [ { @@ -103,6 +101,7 @@ } } ], + "bin": ["bin/pscss"], "config": { "sort-packages": true, "allow-plugins": { diff --git a/scssphp/scss.inc.php b/scssphp/scss.inc.php index f303daa..4598378 100644 --- a/scssphp/scss.inc.php +++ b/scssphp/scss.inc.php @@ -1,7 +1,7 @@ = $this->first && $value <= $this->last; } diff --git a/scssphp/src/Block/AtRootBlock.php b/scssphp/src/Block/AtRootBlock.php index fbe225a..41842c2 100644 --- a/scssphp/src/Block/AtRootBlock.php +++ b/scssphp/src/Block/AtRootBlock.php @@ -18,7 +18,7 @@ /** * @internal */ -final class AtRootBlock extends Block +class AtRootBlock extends Block { /** * @var array|null diff --git a/scssphp/src/Block/CallableBlock.php b/scssphp/src/Block/CallableBlock.php index 2fdbcc2..9b32d8c 100644 --- a/scssphp/src/Block/CallableBlock.php +++ b/scssphp/src/Block/CallableBlock.php @@ -14,11 +14,12 @@ use ScssPhp\ScssPhp\Block; use ScssPhp\ScssPhp\Compiler\Environment; +use ScssPhp\ScssPhp\Node\Number; /** * @internal */ -final class CallableBlock extends Block +class CallableBlock extends Block { /** * @var string @@ -26,7 +27,7 @@ final class CallableBlock extends Block public $name; /** - * @var array|null + * @var list|null */ public $args; diff --git a/scssphp/src/Block/ContentBlock.php b/scssphp/src/Block/ContentBlock.php index cd26cb1..8708498 100644 --- a/scssphp/src/Block/ContentBlock.php +++ b/scssphp/src/Block/ContentBlock.php @@ -19,7 +19,7 @@ /** * @internal */ -final class ContentBlock extends Block +class ContentBlock extends Block { /** * @var array|null diff --git a/scssphp/src/Block/DirectiveBlock.php b/scssphp/src/Block/DirectiveBlock.php index 88b5598..22b346e 100644 --- a/scssphp/src/Block/DirectiveBlock.php +++ b/scssphp/src/Block/DirectiveBlock.php @@ -13,12 +13,13 @@ namespace ScssPhp\ScssPhp\Block; use ScssPhp\ScssPhp\Block; +use ScssPhp\ScssPhp\Node\Number; use ScssPhp\ScssPhp\Type; /** * @internal */ -final class DirectiveBlock extends Block +class DirectiveBlock extends Block { /** * @var string|array @@ -26,7 +27,7 @@ final class DirectiveBlock extends Block public $name; /** - * @var string|array|null + * @var array|Number|null */ public $value; diff --git a/scssphp/src/Block/EachBlock.php b/scssphp/src/Block/EachBlock.php index 0e0d8bd..1217994 100644 --- a/scssphp/src/Block/EachBlock.php +++ b/scssphp/src/Block/EachBlock.php @@ -13,12 +13,13 @@ namespace ScssPhp\ScssPhp\Block; use ScssPhp\ScssPhp\Block; +use ScssPhp\ScssPhp\Node\Number; use ScssPhp\ScssPhp\Type; /** * @internal */ -final class EachBlock extends Block +class EachBlock extends Block { /** * @var string[] @@ -26,7 +27,7 @@ final class EachBlock extends Block public $vars = []; /** - * @var array + * @var array|Number */ public $list; diff --git a/scssphp/src/Block/ElseBlock.php b/scssphp/src/Block/ElseBlock.php index 33172f0..6abb4d7 100644 --- a/scssphp/src/Block/ElseBlock.php +++ b/scssphp/src/Block/ElseBlock.php @@ -18,7 +18,7 @@ /** * @internal */ -final class ElseBlock extends Block +class ElseBlock extends Block { public function __construct() { diff --git a/scssphp/src/Block/ElseifBlock.php b/scssphp/src/Block/ElseifBlock.php index ccc3f71..f732c2d 100644 --- a/scssphp/src/Block/ElseifBlock.php +++ b/scssphp/src/Block/ElseifBlock.php @@ -13,15 +13,16 @@ namespace ScssPhp\ScssPhp\Block; use ScssPhp\ScssPhp\Block; +use ScssPhp\ScssPhp\Node\Number; use ScssPhp\ScssPhp\Type; /** * @internal */ -final class ElseifBlock extends Block +class ElseifBlock extends Block { /** - * @var array + * @var array|Number */ public $cond; diff --git a/scssphp/src/Block/ForBlock.php b/scssphp/src/Block/ForBlock.php index a7030b6..9629441 100644 --- a/scssphp/src/Block/ForBlock.php +++ b/scssphp/src/Block/ForBlock.php @@ -13,12 +13,13 @@ namespace ScssPhp\ScssPhp\Block; use ScssPhp\ScssPhp\Block; +use ScssPhp\ScssPhp\Node\Number; use ScssPhp\ScssPhp\Type; /** * @internal */ -final class ForBlock extends Block +class ForBlock extends Block { /** * @var string @@ -26,12 +27,12 @@ final class ForBlock extends Block public $var; /** - * @var array + * @var array|Number */ public $start; /** - * @var array + * @var array|Number */ public $end; diff --git a/scssphp/src/Block/IfBlock.php b/scssphp/src/Block/IfBlock.php index 3d36e4a..659c7c2 100644 --- a/scssphp/src/Block/IfBlock.php +++ b/scssphp/src/Block/IfBlock.php @@ -13,15 +13,16 @@ namespace ScssPhp\ScssPhp\Block; use ScssPhp\ScssPhp\Block; +use ScssPhp\ScssPhp\Node\Number; use ScssPhp\ScssPhp\Type; /** * @internal */ -final class IfBlock extends Block +class IfBlock extends Block { /** - * @var array + * @var array|Number */ public $cond; diff --git a/scssphp/src/Block/MediaBlock.php b/scssphp/src/Block/MediaBlock.php index c3980df..ab975c7 100644 --- a/scssphp/src/Block/MediaBlock.php +++ b/scssphp/src/Block/MediaBlock.php @@ -13,15 +13,16 @@ namespace ScssPhp\ScssPhp\Block; use ScssPhp\ScssPhp\Block; +use ScssPhp\ScssPhp\Node\Number; use ScssPhp\ScssPhp\Type; /** * @internal */ -final class MediaBlock extends Block +class MediaBlock extends Block { /** - * @var string|array|null + * @var string|array|Number|null */ public $value; diff --git a/scssphp/src/Block/NestedPropertyBlock.php b/scssphp/src/Block/NestedPropertyBlock.php index f1eb998..1ea4a6c 100644 --- a/scssphp/src/Block/NestedPropertyBlock.php +++ b/scssphp/src/Block/NestedPropertyBlock.php @@ -18,7 +18,7 @@ /** * @internal */ -final class NestedPropertyBlock extends Block +class NestedPropertyBlock extends Block { /** * @var bool diff --git a/scssphp/src/Block/WhileBlock.php b/scssphp/src/Block/WhileBlock.php index 37fb9d4..ac18d4e 100644 --- a/scssphp/src/Block/WhileBlock.php +++ b/scssphp/src/Block/WhileBlock.php @@ -18,7 +18,7 @@ /** * @internal */ -final class WhileBlock extends Block +class WhileBlock extends Block { /** * @var array diff --git a/scssphp/src/Cache.php b/scssphp/src/Cache.php index 1a1bc00..9731c60 100644 --- a/scssphp/src/Cache.php +++ b/scssphp/src/Cache.php @@ -13,6 +13,7 @@ namespace ScssPhp\ScssPhp; use Exception; +use ScssPhp\ScssPhp\Version; /** * The scss cache manager. diff --git a/scssphp/src/Colors.php b/scssphp/src/Colors.php index cd4a875..2df3999 100644 --- a/scssphp/src/Colors.php +++ b/scssphp/src/Colors.php @@ -12,8 +12,6 @@ namespace ScssPhp\ScssPhp; -use ScssPhp\ScssPhp\Value\SassColor; - /** * CSS Colors * @@ -21,7 +19,7 @@ * * @internal */ -final class Colors +class Colors { /** * CSS Colors @@ -30,7 +28,7 @@ final class Colors * * @var array */ - private static $cssColors = [ + protected static $cssColors = [ 'aliceblue' => '240,248,255', 'antiquewhite' => '250,235,215', 'aqua' => '0,255,255', @@ -182,17 +180,6 @@ final class Colors 'transparent' => '0,0,0,0', ]; - public static function colorNameToColor(string $colorName): ?SassColor - { - $rgba = self::colorNameToRGBa($colorName); - - if ($rgba === null) { - return null; - } - - return SassColor::rgb($rgba[0], $rgba[1], $rgba[2], $rgba[3] ?? null); - } - /** * Convert named color in a [r,g,b[,a]] array * @@ -202,8 +189,8 @@ public static function colorNameToColor(string $colorName): ?SassColor */ public static function colorNameToRGBa($colorName) { - if (\is_string($colorName) && isset(self::$cssColors[$colorName])) { - $rgba = explode(',', self::$cssColors[$colorName]); + if (\is_string($colorName) && isset(static::$cssColors[$colorName])) { + $rgba = explode(',', static::$cssColors[$colorName]); // only case with opacity is transparent, with opacity=0, so we can intval on opacity also $rgba = array_map('intval', $rgba); @@ -239,7 +226,7 @@ public static function RGBaToColorName($r, $g, $b, $a = 1) if (\is_null($reverseColorTable)) { $reverseColorTable = []; - foreach (self::$cssColors as $name => $rgb_str) { + foreach (static::$cssColors as $name => $rgb_str) { $rgb_str = explode(',', $rgb_str); if ( diff --git a/scssphp/src/CompilationResult.php b/scssphp/src/CompilationResult.php index 48934e6..36adb0d 100644 --- a/scssphp/src/CompilationResult.php +++ b/scssphp/src/CompilationResult.php @@ -12,7 +12,7 @@ namespace ScssPhp\ScssPhp; -final class CompilationResult +class CompilationResult { /** * @var string @@ -34,14 +34,17 @@ final class CompilationResult * @param string|null $sourceMap * @param string[] $includedFiles */ - public function __construct(string $css, ?string $sourceMap, array $includedFiles) + public function __construct($css, $sourceMap, array $includedFiles) { $this->css = $css; $this->sourceMap = $sourceMap; $this->includedFiles = $includedFiles; } - public function getCss(): string + /** + * @return string + */ + public function getCss() { return $this->css; } @@ -49,7 +52,7 @@ public function getCss(): string /** * @return string[] */ - public function getIncludedFiles(): array + public function getIncludedFiles() { return $this->includedFiles; } @@ -59,7 +62,7 @@ public function getIncludedFiles(): array * * @return null|string */ - public function getSourceMap(): ?string + public function getSourceMap() { return $this->sourceMap; } diff --git a/scssphp/src/Compiler.php b/scssphp/src/Compiler.php index 078a77e..c7c1443 100644 --- a/scssphp/src/Compiler.php +++ b/scssphp/src/Compiler.php @@ -70,9 +70,37 @@ * SCSS compiler * * @author Leaf Corcoran + * + * @final Extending the Compiler is deprecated */ -final class Compiler +class Compiler { + /** + * @deprecated + */ + const LINE_COMMENTS = 1; + /** + * @deprecated + */ + const DEBUG_INFO = 2; + + /** + * @deprecated + */ + const WITH_RULE = 1; + /** + * @deprecated + */ + const WITH_MEDIA = 2; + /** + * @deprecated + */ + const WITH_SUPPORTS = 4; + /** + * @deprecated + */ + const WITH_ALL = 7; + const SOURCE_MAP_NONE = 0; const SOURCE_MAP_INLINE = 1; const SOURCE_MAP_FILE = 2; @@ -80,7 +108,7 @@ final class Compiler /** * @var array */ - private static $operatorNames = [ + protected static $operatorNames = [ '+' => 'add', '-' => 'sub', '*' => 'mul', @@ -99,7 +127,7 @@ final class Compiler /** * @var array */ - private static $namespaces = [ + protected static $namespaces = [ 'special' => '%', 'mixin' => '@', 'function' => '^', @@ -107,39 +135,63 @@ final class Compiler public static $true = [Type::T_KEYWORD, 'true']; public static $false = [Type::T_KEYWORD, 'false']; + /** @deprecated */ + public static $NaN = [Type::T_KEYWORD, 'NaN']; + /** @deprecated */ + public static $Infinity = [Type::T_KEYWORD, 'Infinity']; public static $null = [Type::T_NULL]; + /** + * @internal + */ public static $nullString = [Type::T_STRING, '', []]; + /** + * @internal + */ public static $defaultValue = [Type::T_KEYWORD, '']; + /** + * @internal + */ public static $selfSelector = [Type::T_SELF]; public static $emptyList = [Type::T_LIST, '', []]; public static $emptyMap = [Type::T_MAP, [], []]; public static $emptyString = [Type::T_STRING, '"', []]; + /** + * @internal + */ public static $with = [Type::T_KEYWORD, 'with']; + /** + * @internal + */ public static $without = [Type::T_KEYWORD, 'without']; private static $emptyArgumentList = [Type::T_LIST, '', [], []]; /** * @var array */ - private $importPaths = []; + protected $importPaths = []; /** * @var array */ - private $importCache = []; + protected $importCache = []; + + /** + * @var string[] + */ + protected $importedFiles = []; /** * @var array * @phpstan-var array */ - private $userFunctions = []; + protected $userFunctions = []; /** * @var array */ - private $registeredVars = []; + protected $registeredVars = []; /** * @var array */ - private $registeredFeatures = [ + protected $registeredFeatures = [ 'extend-selector-pseudoclass' => false, 'at-error' => true, 'units-level-3' => true, @@ -147,16 +199,26 @@ final class Compiler ]; /** - * @var int - * @phpstan-var self::SOURCE_MAP_* + * @var string|null + */ + protected $encoding = null; + /** + * @var null + * @deprecated */ - private $sourceMap = self::SOURCE_MAP_NONE; + protected $lineNumberStyle = null; + + /** + * @var int|SourceMapGenerator + * @phpstan-var self::SOURCE_MAP_*|SourceMapGenerator + */ + protected $sourceMap = self::SOURCE_MAP_NONE; /** * @var array * @phpstan-var array{sourceRoot?: string, sourceMapFilename?: string|null, sourceMapURL?: string|null, sourceMapWriteTo?: string|null, outputSourceFiles?: bool, sourceMapRootpath?: string, sourceMapBasepath?: string} */ - private $sourceMapOptions = []; + protected $sourceMapOptions = []; /** * @var bool @@ -164,95 +226,110 @@ final class Compiler private $charset = true; /** - * @var string - * @phpstan-var OutputStyle::* + * @var Formatter */ - private $outputStyle = OutputStyle::EXPANDED; + protected $formatter; /** - * @var Formatter + * @var string + * @phpstan-var class-string */ - private $formatter; + private $configuredFormatter = Expanded::class; /** * @var Environment */ - private $rootEnv; + protected $rootEnv; /** * @var OutputBlock|null */ - private $rootBlock; + protected $rootBlock; /** - * @var Environment + * @var \ScssPhp\ScssPhp\Compiler\Environment */ - private $env; + protected $env; /** * @var OutputBlock|null */ - private $scope; + protected $scope; /** * @var Environment|null */ - private $storeEnv; + protected $storeEnv; + /** + * @var bool|null + * + * @deprecated + */ + protected $charsetSeen; /** * @var array */ - private $sourceNames; + protected $sourceNames; /** * @var Cache|null */ - private $cache; + protected $cache; /** * @var bool */ - private $cacheCheckImportResolutions = false; + protected $cacheCheckImportResolutions = false; /** * @var int */ - private $indentLevel; + protected $indentLevel; /** * @var array[] */ - private $extends; + protected $extends; /** * @var array */ - private $extendsMap; + protected $extendsMap; /** * @var array */ - private $parsedFiles = []; + protected $parsedFiles = []; /** * @var Parser|null */ - private $parser; + protected $parser; /** * @var int|null */ - private $sourceIndex; + protected $sourceIndex; /** * @var int|null */ - private $sourceLine; + protected $sourceLine; /** * @var int|null */ - private $sourceColumn; + protected $sourceColumn; /** * @var bool|null */ - private $shouldEvaluate; + protected $shouldEvaluate; + /** + * @var null + * @deprecated + */ + protected $ignoreErrors; + /** + * @var bool + */ + protected $ignoreCallStackMessage = false; /** * @var array[] */ - private $callStack = []; + protected $callStack = []; /** * @var array @@ -274,18 +351,28 @@ final class Compiler */ private $rootDirectory; + /** + * @var bool + */ + private $legacyCwdImportPath = true; + /** * @var LoggerInterface */ private $logger; + /** + * @var array + */ + private $warnedChildFunctions = []; + /** * Constructor * * @param array|null $cacheOptions * @phpstan-param array{cacheDir?: string, prefix?: string, forceRefresh?: string, checkImportResolutions?: bool}|null $cacheOptions */ - public function __construct(?array $cacheOptions = null) + public function __construct($cacheOptions = null) { $this->sourceNames = []; @@ -303,15 +390,20 @@ public function __construct(?array $cacheOptions = null) * Get compiler options * * @return array + * + * @internal */ - private function getCompileOptions(): array + public function getCompileOptions() { $options = [ 'importPaths' => $this->importPaths, 'registeredVars' => $this->registeredVars, + 'registeredFeatures' => $this->registeredFeatures, + 'encoding' => $this->encoding, 'sourceMap' => serialize($this->sourceMap), 'sourceMapOptions' => $this->sourceMapOptions, - 'outputStyle' => $this->outputStyle, + 'formatter' => $this->configuredFormatter, + 'legacyImportPath' => $this->legacyCwdImportPath, ]; return $options; @@ -327,22 +419,93 @@ private function getCompileOptions(): array * * @return void */ - public function setLogger(LoggerInterface $logger): void + public function setLogger(LoggerInterface $logger) { $this->logger = $logger; } + /** + * Set an alternative error output stream, for testing purpose only + * + * @param resource $handle + * + * @return void + * + * @deprecated Use {@see setLogger} instead + */ + public function setErrorOuput($handle) + { + @trigger_error('The method "setErrorOuput" is deprecated. Use "setLogger" instead.', E_USER_DEPRECATED); + + $this->logger = new StreamLogger($handle); + } + /** * Compile scss * - * @param string $source + * @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) + { + @trigger_error(sprintf('The "%s" method is deprecated. Use "compileString" instead.', __METHOD__), E_USER_DEPRECATED); + + $result = $this->compileString($code, $path); + + $sourceMap = $result->getSourceMap(); + + if ($sourceMap !== null) { + if ($this->sourceMap instanceof SourceMapGenerator) { + $this->sourceMap->saveMap($sourceMap); + } elseif ($this->sourceMap === self::SOURCE_MAP_FILE) { + $sourceMapGenerator = new SourceMapGenerator($this->sourceMapOptions); + $sourceMapGenerator->saveMap($sourceMap); + } + } + + return $result->getCss(); + } + + /** + * Compiles the provided scss file into CSS. + * + * @param string $path + * * @return CompilationResult * * @throws SassException when the source fails to compile */ - public function compileString(string $source, ?string $path = null): CompilationResult + public function compileFile($path) + { + $source = file_get_contents($path); + + if ($source === false) { + throw new \RuntimeException('Could not read the file content'); + } + + return $this->compileString($source, $path); + } + + /** + * Compiles the provided scss source code into CSS. + * + * If provided, the path is considered to be the path from which the source code comes + * from, which will be used to resolve relative imports. + * + * @param string $source + * @param string|null $path The path for the source, used to resolve relative imports + * + * @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); @@ -364,7 +527,9 @@ public function compileString(string $source, ?string $path = null): Compilation $this->scope = null; $this->storeEnv = null; $this->shouldEvaluate = null; + $this->ignoreCallStackMessage = false; $this->parsedFiles = []; + $this->importedFiles = []; $this->resolvedImports = []; if (!\is_null($path) && is_file($path)) { @@ -381,7 +546,7 @@ public function compileString(string $source, ?string $path = null): Compilation $tree = $this->parser->parse($source); $this->parser = null; - $this->formatter = $this->outputStyle === OutputStyle::COMPRESSED ? new Compressed() : new Expanded(); + $this->formatter = new $this->configuredFormatter(); $this->rootBlock = null; $this->rootEnv = $this->pushEnv($tree); @@ -400,10 +565,14 @@ public function compileString(string $source, ?string $path = null): Compilation $sourceMapGenerator = null; - if ($this->sourceMap !== self::SOURCE_MAP_NONE) { - $sourceMapGenerator = new SourceMapGenerator($this->sourceMapOptions); + if ($this->sourceMap) { + if (\is_object($this->sourceMap) && $this->sourceMap instanceof SourceMapGenerator) { + $sourceMapGenerator = $this->sourceMap; + $this->sourceMap = self::SOURCE_MAP_FILE; + } elseif ($this->sourceMap !== self::SOURCE_MAP_NONE) { + $sourceMapGenerator = new SourceMapGenerator($this->sourceMapOptions); + } } - assert($this->scope !== null); $out = $this->formatter->format($this->scope, $sourceMapGenerator); @@ -411,17 +580,13 @@ public function compileString(string $source, ?string $path = null): Compilation $prefix = ''; if ($this->charset && strlen($out) !== Util::mbStrlen($out)) { - if ($this->outputStyle === OutputStyle::COMPRESSED) { - $prefix = "\u{FEFF}"; - } else { - $prefix = '@charset "UTF-8";' . "\n"; - } + $prefix = '@charset "UTF-8";' . "\n"; $out = $prefix . $out; } $sourceMap = null; - if (! empty($out) && $this->sourceMap !== self::SOURCE_MAP_NONE) { + if (! empty($out) && $this->sourceMap !== self::SOURCE_MAP_NONE && $this->sourceMap) { assert($sourceMapGenerator !== null); $sourceMap = $sourceMapGenerator->generateJson($prefix); $sourceMapUrl = null; @@ -459,8 +624,9 @@ public function compileString(string $source, ?string $path = null): Compilation } // Reset state to free memory - $this->parsedFiles = []; + // TODO in 2.0, reset parsedFiles as well when the getter is removed. $this->resolvedImports = []; + $this->importedFiles = []; return $result; } @@ -470,7 +636,7 @@ public function compileString(string $source, ?string $path = null): Compilation * * @return bool */ - private function isFreshCachedResult(CachedResult $result): bool + private function isFreshCachedResult(CachedResult $result) { // check if any dependency file changed since the result was compiled foreach ($result->getParsedFiles() as $file => $mtime) { @@ -506,9 +672,9 @@ private function isFreshCachedResult(CachedResult $result): bool * * @param string|null $path * - * @return Parser + * @return \ScssPhp\ScssPhp\Parser */ - private function parserFactory(?string $path): Parser + protected function parserFactory($path) { // https://sass-lang.com/documentation/at-rules/import // CSS files imported by Sass don’t allow any special Sass features. @@ -521,7 +687,7 @@ private function parserFactory(?string $path): Parser $cssOnly = true; } - $parser = new Parser($path, \count($this->sourceNames), $this->cache, $cssOnly, $this->logger); + $parser = new Parser($path, \count($this->sourceNames), $this->encoding, $this->cache, $cssOnly, $this->logger); $this->sourceNames[] = $path; $this->addParsedFile($path); @@ -537,7 +703,7 @@ private function parserFactory(?string $path): Parser * * @return bool */ - private function isSelfExtend(array $target, array $origin): bool + protected function isSelfExtend($target, $origin) { foreach ($origin as $sel) { if (\in_array($target, $sel)) { @@ -557,7 +723,7 @@ private function isSelfExtend(array $target, array $origin): bool * * @return void */ - private function pushExtends(array $target, array $origin, ?array $block): void + protected function pushExtends($target, $origin, $block) { $i = \count($this->extends); $this->extends[] = [$target, $origin, $block]; @@ -577,9 +743,9 @@ private function pushExtends(array $target, array $origin, ?array $block): void * @param string|null $type * @param string[]|null $selectors * - * @return OutputBlock + * @return \ScssPhp\ScssPhp\Formatter\OutputBlock */ - private function makeOutputBlock(?string $type, ?array $selectors = null): OutputBlock + protected function makeOutputBlock($type, $selectors = null) { $out = new OutputBlock(); $out->type = $type; @@ -605,11 +771,11 @@ private function makeOutputBlock(?string $type, ?array $selectors = null): Outpu /** * Compile root * - * @param Block $rootBlock + * @param \ScssPhp\ScssPhp\Block $rootBlock * * @return void */ - private function compileRoot(Block $rootBlock): void + protected function compileRoot(Block $rootBlock) { $this->rootBlock = $this->scope = $this->makeOutputBlock(Type::T_ROOT); @@ -624,7 +790,7 @@ private function compileRoot(Block $rootBlock): void * * @return void */ - private function missingSelectors(): void + protected function missingSelectors() { foreach ($this->extends as $extend) { if (isset($extend[3])) { @@ -649,12 +815,12 @@ private function missingSelectors(): void /** * Flatten selectors * - * @param OutputBlock $block - * @param string|int|null $parentKey + * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $block + * @param string $parentKey * * @return void */ - private function flattenSelectors(OutputBlock $block, $parentKey = null): void + protected function flattenSelectors(OutputBlock $block, $parentKey = null) { if ($block->selectors) { $selectors = []; @@ -715,7 +881,7 @@ private function flattenSelectors(OutputBlock $block, $parentKey = null): void * * @return array */ - private function glueFunctionSelectors(array $parts): array + protected function glueFunctionSelectors($parts) { $new = []; @@ -753,7 +919,7 @@ private function glueFunctionSelectors(array $parts): array * * @return void */ - private function matchExtends(array $selector, &$out, int $from = 0, bool $initial = true): void + protected function matchExtends($selector, &$out, $from = 0, $initial = true) { static $partsPile = []; $selector = $this->glueFunctionSelectors($selector); @@ -884,7 +1050,7 @@ private function matchExtends(array $selector, &$out, int $from = 0, bool $initi * * @return bool */ - private function isPseudoSelector(string $part, &$matches): bool + protected function isPseudoSelector($part, &$matches) { if ( strpos($part, ':') === 0 && @@ -908,7 +1074,7 @@ private function isPseudoSelector(string $part, &$matches): bool * * @return void */ - private function pushOrMergeExtentedSelector(&$out, array $extended): void + protected function pushOrMergeExtentedSelector(&$out, $extended) { if (\count($out) && \count($extended) === 1 && \count(reset($extended)) === 1) { $single = reset($extended); @@ -950,7 +1116,7 @@ private function pushOrMergeExtentedSelector(&$out, array $extended): void * * @return bool */ - private function matchExtendsSingle(array $rawSingle, &$outOrigin, bool $initial = true): bool + protected function matchExtendsSingle($rawSingle, &$outOrigin, $initial = true) { $counts = []; $single = []; @@ -1080,7 +1246,7 @@ private function matchExtendsSingle(array $rawSingle, &$outOrigin, bool $initial * * @return array The selector without the relationship fragment if any, the relationship fragment. */ - private function extractRelationshipFromFragment(array $fragment): array + protected function extractRelationshipFromFragment(array $fragment) { $parents = []; $children = []; @@ -1110,7 +1276,7 @@ private function extractRelationshipFromFragment(array $fragment): array * * @return array */ - private function combineSelectorSingle(array $base, array $other): array + protected function combineSelectorSingle($base, $other) { $tag = []; $out = []; @@ -1157,11 +1323,11 @@ private function combineSelectorSingle(array $base, array $other): array /** * Compile media * - * @param Block $media + * @param \ScssPhp\ScssPhp\Block $media * * @return void */ - private function compileMedia(Block $media): void + protected function compileMedia(Block $media) { assert($media instanceof MediaBlock); $this->pushEnv($media); @@ -1222,11 +1388,11 @@ private function compileMedia(Block $media): void /** * Media parent * - * @param OutputBlock $scope + * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $scope * - * @return OutputBlock + * @return \ScssPhp\ScssPhp\Formatter\OutputBlock */ - private function mediaParent(OutputBlock $scope): OutputBlock + protected function mediaParent(OutputBlock $scope) { while (! empty($scope->parent)) { if (! empty($scope->type) && $scope->type !== Type::T_MEDIA) { @@ -1242,12 +1408,12 @@ private function mediaParent(OutputBlock $scope): OutputBlock /** * Compile directive * - * @param DirectiveBlock|array $directive - * @param OutputBlock $out + * @param DirectiveBlock|array $directive + * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $out * * @return void */ - private function compileDirective($directive, OutputBlock $out): void + protected function compileDirective($directive, OutputBlock $out) { if (\is_array($directive)) { $directiveName = $this->compileDirectiveName($directive[0]); @@ -1290,7 +1456,7 @@ private function compileDirective($directive, OutputBlock $out): void * @return string * @throws CompilerException */ - private function compileDirectiveName($directiveName): string + protected function compileDirectiveName($directiveName) { if (is_string($directiveName)) { return $directiveName; @@ -1302,11 +1468,11 @@ private function compileDirectiveName($directiveName): string /** * Compile at-root * - * @param Block $block + * @param \ScssPhp\ScssPhp\Block $block * * @return void */ - private function compileAtRoot(Block $block): void + protected function compileAtRoot(Block $block) { assert($block instanceof AtRootBlock); $env = $this->pushEnv($block); @@ -1361,13 +1527,13 @@ private function compileAtRoot(Block $block): void /** * Filter at-root scope depending on with/without option * - * @param OutputBlock $scope - * @param array $with - * @param array $without + * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $scope + * @param array $with + * @param array $without * * @return OutputBlock */ - private function filterScopeWithWithout(OutputBlock $scope, array $with, array $without): OutputBlock + protected function filterScopeWithWithout($scope, $with, $without) { $filteredScopes = []; $childStash = []; @@ -1380,6 +1546,7 @@ private function filterScopeWithWithout(OutputBlock $scope, array $with, array $ // start from the root while ($scope->parent && $scope->parent->type !== Type::T_ROOT) { array_unshift($childStash, $scope); + \assert($scope->parent !== null); $scope = $scope->parent; } @@ -1436,12 +1603,12 @@ private function filterScopeWithWithout(OutputBlock $scope, array $with, array $ * found missing selector from a at-root compilation in the previous scope * (if at-root is just enclosing a property, the selector is in the parent tree) * - * @param OutputBlock $scope - * @param OutputBlock $previousScope + * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $scope + * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $previousScope * * @return OutputBlock */ - private function completeScope(OutputBlock $scope, OutputBlock $previousScope): OutputBlock + protected function completeScope($scope, $previousScope) { if (! $scope->type && ! $scope->selectors && \count($scope->lines)) { $scope->selectors = $this->findScopeSelectors($previousScope, $scope->depth); @@ -1459,12 +1626,12 @@ private function completeScope(OutputBlock $scope, OutputBlock $previousScope): /** * Find a selector by the depth node in the scope * - * @param OutputBlock $scope - * @param int $depth + * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $scope + * @param int $depth * * @return array */ - private function findScopeSelectors(OutputBlock $scope, int $depth): array + protected function findScopeSelectors($scope, $depth) { if ($scope->depth === $depth && $scope->selectors) { return $scope->selectors; @@ -1490,7 +1657,7 @@ private function findScopeSelectors(OutputBlock $scope, int $depth): array * * @phpstan-return array{array, array} */ - private function compileWith(?array $withCondition): array + protected function compileWith($withCondition) { // just compile what we have in 2 lists $with = []; @@ -1504,11 +1671,12 @@ private function compileWith(?array $withCondition): array $parser = $this->parserFactory(__METHOD__); if ($parser->parseValue($buffer, $reParsedWith)) { + \assert(\is_array($reParsedWith)); $withCondition = $reParsedWith; } } - $withConfig = $this->mapGet($withCondition, self::$with); + $withConfig = $this->mapGet($withCondition, static::$with); if ($withConfig !== null) { $without = []; // cancel the default $list = $this->coerceList($withConfig); @@ -1520,7 +1688,7 @@ private function compileWith(?array $withCondition): array } } - $withoutConfig = $this->mapGet($withCondition, self::$without); + $withoutConfig = $this->mapGet($withCondition, static::$without); if ($withoutConfig !== null) { $without = []; // cancel the default $list = $this->coerceList($withoutConfig); @@ -1547,7 +1715,7 @@ private function compileWith(?array $withCondition): array * * @phpstan-param non-empty-array $envs */ - private function filterWithWithout(array $envs, array $with, array $without): Environment + protected function filterWithWithout($envs, $with, $without) { $filtered = []; @@ -1569,13 +1737,13 @@ private function filterWithWithout(array $envs, array $with, array $without): En /** * Filter WITH rules * - * @param Block|OutputBlock $block - * @param array $with - * @param array $without + * @param \ScssPhp\ScssPhp\Block|\ScssPhp\ScssPhp\Formatter\OutputBlock $block + * @param array $with + * @param array $without * * @return bool */ - private function isWith($block, array $with, array $without): bool + protected function isWith($block, $with, $without) { if (isset($block->type)) { if ($block->type === Type::T_MEDIA) { @@ -1622,7 +1790,7 @@ private function isWith($block, array $with, array $without): bool * @return bool * true if the block should be kept, false to reject */ - private function testWithWithout(string $what, array $with, array $without): bool + protected function testWithWithout($what, $with, $without) { // if without, reject only if in the list (or 'all' is in the list) if (\count($without)) { @@ -1637,12 +1805,12 @@ private function testWithWithout(string $what, array $with, array $without): boo /** * Compile keyframe block * - * @param Block $block - * @param string[] $selectors + * @param \ScssPhp\ScssPhp\Block $block + * @param string[] $selectors * * @return void */ - private function compileKeyframeBlock(Block $block, array $selectors): void + protected function compileKeyframeBlock(Block $block, $selectors) { $env = $this->pushEnv($block); @@ -1669,12 +1837,12 @@ private function compileKeyframeBlock(Block $block, array $selectors): void /** * Compile nested properties lines * - * @param Block $block - * @param OutputBlock $out + * @param \ScssPhp\ScssPhp\Block $block + * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $out * * @return void */ - private function compileNestedPropertiesBlock(Block $block, OutputBlock $out): void + protected function compileNestedPropertiesBlock(Block $block, OutputBlock $out) { assert($block instanceof NestedPropertyBlock); $prefix = $this->compileValue($block->prefix) . '-'; @@ -1707,12 +1875,12 @@ private function compileNestedPropertiesBlock(Block $block, OutputBlock $out): v /** * Compile nested block * - * @param Block $block - * @param string[] $selectors + * @param \ScssPhp\ScssPhp\Block $block + * @param string[] $selectors * * @return void */ - private function compileNestedBlock(Block $block, array $selectors): void + protected function compileNestedBlock(Block $block, $selectors) { $this->pushEnv($block); @@ -1773,11 +1941,11 @@ private function compileNestedBlock(Block $block, array $selectors): void * * @see Compiler::compileChild() * - * @param Block $block + * @param \ScssPhp\ScssPhp\Block $block * * @return void */ - private function compileBlock(Block $block): void + protected function compileBlock(Block $block) { $env = $this->pushEnv($block); assert($block->selectors !== null); @@ -1820,7 +1988,7 @@ private function compileBlock(Block $block): void * * @return string */ - private function compileCommentValue(array $value, bool $pushEnv = false) + protected function compileCommentValue($value, $pushEnv = false) { $c = $value[1]; @@ -1829,7 +1997,15 @@ private function compileCommentValue(array $value, bool $pushEnv = false) $this->pushEnv(); } - $c = $this->compileValue($value[2]); + try { + $c = $this->compileValue($value[2]); + } catch (SassScriptException $e) { + $this->logger->warn('Ignoring interpolation errors in multiline comments is deprecated and will be removed in ScssPhp 2.0. ' . $this->addLocationToMessage($e->getMessage()), true); + // ignore error in comment compilation which are only interpolation + } catch (SassException $e) { + $this->logger->warn('Ignoring interpolation errors in multiline comments is deprecated and will be removed in ScssPhp 2.0. ' . $e->getMessage(), true); + // ignore error in comment compilation which are only interpolation + } if ($pushEnv) { $this->popEnv(); @@ -1846,7 +2022,7 @@ private function compileCommentValue(array $value, bool $pushEnv = false) * * @return void */ - private function compileComment(array $block): void + protected function compileComment($block) { $out = $this->makeOutputBlock(Type::T_COMMENT); $out->lines[] = $this->compileCommentValue($block, true); @@ -1862,7 +2038,7 @@ private function compileComment(array $block): void * * @return array */ - private function evalSelectors(array $selectors): array + protected function evalSelectors($selectors) { $this->shouldEvaluate = false; @@ -1901,7 +2077,7 @@ private function evalSelectors(array $selectors): array * * @phpstan-impure */ - private function evalSelector(array $selector): array + protected function evalSelector($selector) { return array_map([$this, 'evalSelectorPart'], $selector); } @@ -1915,7 +2091,7 @@ private function evalSelector(array $selector): array * * @phpstan-impure */ - private function evalSelectorPart(array $part): array + protected function evalSelectorPart($part) { foreach ($part as &$p) { if (\is_array($p) && ($p[0] === Type::T_INTERPOLATE || $p[0] === Type::T_STRING)) { @@ -1944,7 +2120,7 @@ private function evalSelectorPart(array $part): array * * @return string */ - private function collapseSelectors(array $selectors): string + protected function collapseSelectors($selectors) { $parts = []; @@ -1954,6 +2130,11 @@ private function collapseSelectors(array $selectors): string foreach ($selector as $node) { $compound = ''; + if (!is_array($node)) { + $output[] = $node; + continue; + } + array_walk_recursive( $node, function ($value, $key) use (&$compound) { @@ -1977,7 +2158,7 @@ function ($value, $key) use (&$compound) { * * @return array */ - private function collapseSelectorsAsList(array $selectors): array + private function collapseSelectorsAsList($selectors) { $parts = []; @@ -1988,12 +2169,16 @@ private function collapseSelectorsAsList(array $selectors): array foreach ($selector as $node) { $compound = ''; - array_walk_recursive( - $node, - function ($value, $key) use (&$compound) { - $compound .= $value; - } - ); + if (!is_array($node)) { + $compound .= $node; + } else { + array_walk_recursive( + $node, + function ($value, $key) use (&$compound) { + $compound .= $value; + } + ); + } if ($this->isImmediateRelationshipCombinator($compound)) { if (\count($output)) { @@ -2029,7 +2214,7 @@ function ($value, $key) use (&$compound) { * * @return array */ - private function replaceSelfSelector(array $selectors, ?string $replace = null): array + protected function replaceSelfSelector($selectors, $replace = null) { foreach ($selectors as &$part) { if (\is_array($part)) { @@ -2055,7 +2240,7 @@ private function replaceSelfSelector(array $selectors, ?string $replace = null): * * @return array */ - private function flattenSelectorSingle(array $single): array + protected function flattenSelectorSingle($single) { $joined = []; @@ -2086,7 +2271,7 @@ private function flattenSelectorSingle(array $single): array * * @return string */ - private function compileSelector($selector): string + protected function compileSelector($selector) { if (! \is_array($selector)) { return $selector; // media and the like @@ -2108,7 +2293,7 @@ private function compileSelector($selector): string * * @return string */ - private function compileSelectorPart(array $piece): string + protected function compileSelectorPart($piece) { foreach ($piece as &$p) { if (! \is_array($p)) { @@ -2136,7 +2321,7 @@ private function compileSelectorPart(array $piece): string * * @return bool */ - private function hasSelectorPlaceholder($selector): bool + protected function hasSelectorPlaceholder($selector) { if (! \is_array($selector)) { return false; @@ -2158,7 +2343,7 @@ private function hasSelectorPlaceholder($selector): bool * * @return void */ - private function pushCallStack(string $name = ''): void + protected function pushCallStack($name = '') { $this->callStack[] = [ 'n' => $name, @@ -2180,7 +2365,7 @@ private function pushCallStack(string $name = ''): void /** * @return void */ - private function popCallStack(): void + protected function popCallStack() { array_pop($this->callStack); } @@ -2188,13 +2373,13 @@ private function popCallStack(): void /** * Compile children and return result * - * @param array $stms - * @param OutputBlock $out - * @param string $traceName + * @param array $stms + * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $out + * @param string $traceName * * @return array|Number|null */ - private function compileChildren(array $stms, OutputBlock $out, string $traceName = '') + protected function compileChildren($stms, OutputBlock $out, $traceName = '') { $this->pushCallStack($traceName); @@ -2217,15 +2402,15 @@ private function compileChildren(array $stms, OutputBlock $out, string $traceNam * Compile children and throw exception if unexpected at-return * * @param array[] $stms - * @param OutputBlock $out - * @param Block $selfParent - * @param string $traceName + * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $out + * @param \ScssPhp\ScssPhp\Block $selfParent + * @param string $traceName * * @return void * * @throws \Exception */ - private function compileChildrenNoReturn(array $stms, OutputBlock $out, $selfParent = null, string $traceName = ''): void + protected function compileChildrenNoReturn($stms, OutputBlock $out, $selfParent = null, $traceName = '') { $this->pushCallStack($traceName); @@ -2258,7 +2443,7 @@ private function compileChildrenNoReturn(array $stms, OutputBlock $out, $selfPar * * @return array */ - private function evaluateMediaQuery(array $queryList): array + protected function evaluateMediaQuery($queryList) { static $parser = null; @@ -2324,7 +2509,7 @@ private function evaluateMediaQuery(array $queryList): array * * @return string[] */ - private function compileMediaQuery(array $queryList): array + protected function compileMediaQuery($queryList) { $start = '@media '; $default = trim($start); @@ -2448,7 +2633,7 @@ private function compileMediaQuery(array $queryList): array * * @return array */ - private function mergeDirectRelationships(array $selectors1, array $selectors2): array + protected function mergeDirectRelationships($selectors1, $selectors2) { if (empty($selectors1) || empty($selectors2)) { return array_merge($selectors1, $selectors2); @@ -2492,7 +2677,7 @@ private function mergeDirectRelationships(array $selectors1, array $selectors2): * * @return array|null */ - private function mergeMediaTypes(array $type1, array $type2): ?array + protected function mergeMediaTypes($type1, $type2) { if (empty($type1)) { return $type2; @@ -2549,12 +2734,13 @@ private function mergeMediaTypes(array $type1, array $type2): ?array /** * Compile import; returns true if the value was something that could be imported * - * @param array $rawPath - * @param OutputBlock $out + * @param array $rawPath + * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $out + * @param bool $once * * @return bool */ - private function compileImport($rawPath, OutputBlock $out): bool + protected function compileImport($rawPath, OutputBlock $out, $once = false) { if ($rawPath[0] === Type::T_STRING) { $path = $this->compileStringContent($rawPath); @@ -2562,7 +2748,10 @@ private function compileImport($rawPath, OutputBlock $out): bool if (strpos($path, 'url(') !== 0 && $filePath = $this->findImport($path, $this->currentDirectory)) { $this->registerImport($this->currentDirectory, $path, $filePath); - $this->importFile($filePath, $out); + if (! $once || ! \in_array($filePath, $this->importedFiles)) { + $this->importFile($filePath, $out); + $this->importedFiles[] = $filePath; + } return true; } @@ -2587,7 +2776,7 @@ private function compileImport($rawPath, OutputBlock $out): bool } foreach ($rawPath[2] as $path) { - $this->compileImport($path, $out); + $this->compileImport($path, $out, $once); } return true; @@ -2603,7 +2792,7 @@ private function compileImport($rawPath, OutputBlock $out): bool * @return string * @throws CompilerException */ - private function compileImportPath($rawPath): string + protected function compileImportPath($rawPath) { $path = $this->compileValue($rawPath); @@ -2626,7 +2815,7 @@ private function compileImportPath($rawPath): string * @return array * @throws CompilerException */ - private function escapeImportPathString($path) + protected function escapeImportPathString($path) { switch ($path[0]) { case Type::T_LIST: @@ -2650,13 +2839,13 @@ private function escapeImportPathString($path) * Append a root directive like @import or @charset as near as the possible from the source code * (keeping before comments, @import and @charset coming before in the source code) * - * @param string $line - * @param OutputBlock $out - * @param string[] $allowed + * @param string $line + * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $out + * @param array $allowed * * @return void */ - private function appendRootDirective(string $line, OutputBlock $out, array $allowed = [Type::T_COMMENT]): void + protected function appendRootDirective($line, $out, $allowed = [Type::T_COMMENT]) { $root = $out; @@ -2700,13 +2889,13 @@ private function appendRootDirective(string $line, OutputBlock $out, array $allo * Append lines to the current output block: * directly to the block or through a child if necessary * - * @param OutputBlock $out - * @param string $type - * @param string $line + * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $out + * @param string $type + * @param string $line * * @return void */ - private function appendOutputLine(OutputBlock $out, string $type, string $line): void + protected function appendOutputLine(OutputBlock $out, $type, $line) { $outWrite = &$out; @@ -2736,16 +2925,16 @@ private function appendOutputLine(OutputBlock $out, string $type, string $line): /** * Compile child; returns a value to halt execution * - * @param array $child - * @param OutputBlock $out + * @param array $child + * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $out * * @return array|Number|null */ - private function compileChild($child, OutputBlock $out) + protected function compileChild($child, OutputBlock $out) { if (isset($child[Parser::SOURCE_LINE])) { $this->sourceIndex = isset($child[Parser::SOURCE_INDEX]) ? $child[Parser::SOURCE_INDEX] : null; - $this->sourceLine = isset($child[Parser::SOURCE_LINE]) ? $child[Parser::SOURCE_LINE] : -1; + $this->sourceLine = $child[Parser::SOURCE_LINE]; $this->sourceColumn = isset($child[Parser::SOURCE_COLUMN]) ? $child[Parser::SOURCE_COLUMN] : -1; } elseif (\is_array($child) && isset($child[1]->sourceLine) && $child[1] instanceof Block) { $this->sourceIndex = $child[1]->sourceIndex; @@ -2763,6 +2952,12 @@ private function compileChild($child, OutputBlock $out) } switch ($child[0]) { + case Type::T_SCSSPHP_IMPORT_ONCE: + $rawPath = $this->reduce($child[1]); + + $this->compileImport($rawPath, $out, true); + break; + case Type::T_IMPORT: $rawPath = $this->reduce($child[1]); @@ -2797,7 +2992,7 @@ private function compileChild($child, OutputBlock $out) if ($value[0] !== Type::T_NULL) { $value = $this->reduce($value); - if ($value[0] === Type::T_NULL || $value === self::$nullString) { + if ($value[0] === Type::T_NULL || $value === static::$nullString) { break; } } @@ -2827,7 +3022,7 @@ private function compileChild($child, OutputBlock $out) $shouldSet = $isDefault && (\is_null($result = $this->get($name[1], false)) || - $result === self::$null); + $result === static::$null); if (! $isDefault || $shouldSet) { $this->set($name[1], $this->reduce($value), true, null, $value); @@ -2919,7 +3114,7 @@ private function compileChild($child, OutputBlock $out) if ($value[0] !== Type::T_NULL) { $value = $this->reduce($value); - if ($value[0] === Type::T_NULL || $value === self::$nullString) { + if ($value[0] === Type::T_NULL || $value === static::$nullString) { break; } } @@ -2952,7 +3147,7 @@ private function compileChild($child, OutputBlock $out) assert($block instanceof CallableBlock); // the block need to be able to go up to it's parent env to resolve vars $block->parentEnv = $this->getStoreEnv(); - $this->set(self::$namespaces[$block->type] . $block->name, $block, true); + $this->set(static::$namespaces[$block->type] . $block->name, $block, true); break; case Type::T_EXTEND: @@ -2988,7 +3183,7 @@ private function compileChild($child, OutputBlock $out) on line $line of $fname: Compound selectors may no longer be extended. Consider `@extend $replacement` instead. -See https://sass-lang.com/d/extend-compound for details. +See http://bit.ly/ExtendCompound for details. EOL; $this->logger->warn($message); @@ -3032,7 +3227,7 @@ private function compileChild($child, OutputBlock $out) list(,, $values) = $this->coerceList($item); foreach ($each->vars as $i => $var) { - $this->set($var, isset($values[$i]) ? $values[$i] : self::$null, true); + $this->set($var, isset($values[$i]) ? $values[$i] : static::$null, true); } } @@ -3122,7 +3317,7 @@ private function compileChild($child, OutputBlock $out) // including a mixin list(, $name, $argValues, $content, $argUsing) = $child; - $mixin = $this->get(self::$namespaces['mixin'] . $name, false); + $mixin = $this->get(static::$namespaces['mixin'] . $name, false); if (! $mixin) { throw $this->error("Undefined mixin $name"); @@ -3163,16 +3358,16 @@ private function compileChild($child, OutputBlock $out) $copyContent = clone $content; $copyContent->scope = clone $callingScope; - $this->setRaw(self::$namespaces['special'] . 'content', $copyContent, $this->env); + $this->setRaw(static::$namespaces['special'] . 'content', $copyContent, $this->env); } else { - $this->setRaw(self::$namespaces['special'] . 'content', null, $this->env); + $this->setRaw(static::$namespaces['special'] . 'content', null, $this->env); } // save the "using" argument list for applying it to when "@content" is invoked if (isset($argUsing)) { - $this->setRaw(self::$namespaces['special'] . 'using', $argUsing, $this->env); + $this->setRaw(static::$namespaces['special'] . 'using', $argUsing, $this->env); } else { - $this->setRaw(self::$namespaces['special'] . 'using', null, $this->env); + $this->setRaw(static::$namespaces['special'] . 'using', null, $this->env); } if (isset($mixin->args)) { @@ -3194,8 +3389,8 @@ private function compileChild($child, OutputBlock $out) case Type::T_MIXIN_CONTENT: $env = isset($this->storeEnv) ? $this->storeEnv : $this->env; - $content = $this->get(self::$namespaces['special'] . 'content', false, $env); - $argUsing = $this->get(self::$namespaces['special'] . 'using', false, $env); + $content = $this->get(static::$namespaces['special'] . 'content', false, $env); + $argUsing = $this->get(static::$namespaces['special'] . 'using', false, $env); $argContent = $child[1]; if (! $content) { @@ -3268,7 +3463,7 @@ private function compileChild($child, OutputBlock $out) * * @return array */ - private function expToString(array $exp, bool $keepParens = false): array + protected function expToString($exp, $keepParens = false) { list(, $op, $left, $right, $inParens, $whiteLeft, $whiteRight) = $exp; @@ -3306,9 +3501,9 @@ private function expToString(array $exp, bool $keepParens = false): array * * @return bool */ - public function isTruthy($value): bool + public function isTruthy($value) { - return $value !== self::$false && $value !== self::$null; + return $value !== static::$false && $value !== static::$null; } /** @@ -3318,7 +3513,7 @@ public function isTruthy($value): bool * * @return bool */ - private function isImmediateRelationshipCombinator(string $value): bool + protected function isImmediateRelationshipCombinator($value) { return $value === '>' || $value === '+' || $value === '~'; } @@ -3330,7 +3525,7 @@ private function isImmediateRelationshipCombinator(string $value): bool * * @return bool */ - private function shouldEval($value): bool + protected function shouldEval($value) { switch ($value[0]) { case Type::T_EXPRESSION: @@ -3355,7 +3550,7 @@ private function shouldEval($value): bool * * @return array|Number */ - private function reduce($value, bool $inExp = false) + protected function reduce($value, $inExp = false) { if ($value instanceof Number) { return $value; @@ -3365,7 +3560,7 @@ private function reduce($value, bool $inExp = false) case Type::T_EXPRESSION: list(, $op, $left, $right, $inParens) = $value; - $opName = isset(self::$operatorNames[$op]) ? self::$operatorNames[$op] : $op; + $opName = isset(static::$operatorNames[$op]) ? static::$operatorNames[$op] : $op; $inExp = $inExp || $this->shouldEval($left) || $this->shouldEval($right); $left = $this->reduce($left, true); @@ -3432,11 +3627,11 @@ private function reduce($value, bool $inExp = false) if ($op === 'not') { if ($inExp || $inParens) { - if ($exp === self::$false || $exp === self::$null) { - return self::$true; + if ($exp === static::$false || $exp === static::$null) { + return static::$true; } - return self::$false; + return static::$false; } $op = $op . ' '; @@ -3514,14 +3709,14 @@ private function reduce($value, bool $inExp = false) * * @return array|Number */ - private function fncall($functionReference, $argValues) + protected function fncall($functionReference, $argValues) { // a string means this is a static hard reference coming from the parsing if (is_string($functionReference)) { $name = $functionReference; $functionReference = $this->getFunctionReference($name); - if ($functionReference === self::$null || $functionReference[0] !== Type::T_FUNCTION_REFERENCE) { + if ($functionReference === static::$null || $functionReference[0] !== Type::T_FUNCTION_REFERENCE) { $functionReference = [Type::T_FUNCTION, $name, [Type::T_LIST, ',', []]]; } } @@ -3540,8 +3735,8 @@ private function fncall($functionReference, $argValues) return [Type::T_FUNCTION, $functionReference[1], [Type::T_LIST, ',', $listArgs]]; } - if ($functionReference === self::$null || $functionReference[0] !== Type::T_FUNCTION_REFERENCE) { - return self::$defaultValue; + if ($functionReference === static::$null || $functionReference[0] !== Type::T_FUNCTION_REFERENCE) { + return static::$defaultValue; } @@ -3575,18 +3770,18 @@ private function fncall($functionReference, $argValues) return $returnValue; default: - return self::$defaultValue; + return static::$defaultValue; } } /** * @param array|Number $arg * @param string[] $allowed_function - * @param string|bool $inFunction + * @param bool $inFunction * * @return array|Number|false */ - private function cssValidArg($arg, array $allowed_function = [], $inFunction = false) + protected function cssValidArg($arg, $allowed_function = [], $inFunction = false) { if ($arg instanceof Number) { return $this->stringifyFncallArgs($arg); @@ -3616,7 +3811,7 @@ private function cssValidArg($arg, array $allowed_function = [], $inFunction = f } $cssArgs = []; foreach ($arg[2] as $argValue) { - if ($argValue === self::$null) { + if ($argValue === static::$null) { return false; } $cssArg = $this->cssValidArg($argValue[1], $allowed_function, $arg[1]); @@ -3678,7 +3873,7 @@ private function cssValidArg($arg, array $allowed_function = [], $inFunction = f * * @return array|Number */ - private function stringifyFncallArgs($arg) + protected function stringifyFncallArgs($arg) { if ($arg instanceof Number) { return $arg; @@ -3719,10 +3914,10 @@ private function stringifyFncallArgs($arg) * @param bool $safeCopy * @return array */ - private function getFunctionReference(string $name, bool $safeCopy = false): array + protected function getFunctionReference($name, $safeCopy = false) { // SCSS @function - if ($func = $this->get(self::$namespaces['function'] . $name, false)) { + if ($func = $this->get(static::$namespaces['function'] . $name, false)) { if ($safeCopy) { $func = clone $func; } @@ -3753,12 +3948,54 @@ private function getFunctionReference(string $name, bool $safeCopy = false): arr if (($f = $this->getBuiltinFunction($normalizedName)) && \is_callable($f)) { /** @var string $libName */ $libName = $f[1]; - $prototype = self::$$libName; + $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]; } - return self::$null; + return static::$null; } @@ -3769,7 +4006,7 @@ private function getFunctionReference(string $name, bool $safeCopy = false): arr * * @return string */ - private function normalizeName(string $name): string + protected function normalizeName($name) { return str_replace('-', '_', $name); } @@ -3777,11 +4014,13 @@ private function normalizeName(string $name): string /** * Normalize value * + * @internal + * * @param array|Number $value * * @return array|Number */ - private function normalizeValue($value) + public function normalizeValue($value) { $value = $this->coerceForExpression($this->reduce($value)); @@ -3830,7 +4069,7 @@ private function normalizeValue($value) * * @return Number */ - private function opAddNumberNumber(Number $left, Number $right): Number + protected function opAddNumberNumber(Number $left, Number $right) { return $left->plus($right); } @@ -3843,7 +4082,7 @@ private function opAddNumberNumber(Number $left, Number $right): Number * * @return Number */ - private function opMulNumberNumber(Number $left, Number $right): Number + protected function opMulNumberNumber(Number $left, Number $right) { return $left->times($right); } @@ -3856,7 +4095,7 @@ private function opMulNumberNumber(Number $left, Number $right): Number * * @return Number */ - private function opSubNumberNumber(Number $left, Number $right): Number + protected function opSubNumberNumber(Number $left, Number $right) { return $left->minus($right); } @@ -3869,7 +4108,7 @@ private function opSubNumberNumber(Number $left, Number $right): Number * * @return Number */ - private function opDivNumberNumber(Number $left, Number $right): Number + protected function opDivNumberNumber(Number $left, Number $right) { return $left->dividedBy($right); } @@ -3882,7 +4121,7 @@ private function opDivNumberNumber(Number $left, Number $right): Number * * @return Number */ - private function opModNumberNumber(Number $left, Number $right): Number + protected function opModNumberNumber(Number $left, Number $right) { return $left->modulo($right); } @@ -3890,12 +4129,12 @@ private function opModNumberNumber(Number $left, Number $right): Number /** * Add strings * - * @param array|Number $left - * @param array|Number $right + * @param array $left + * @param array $right * * @return array|null */ - private function opAdd($left, $right) + protected function opAdd($left, $right) { if ($strLeft = $this->coerceString($left)) { if ($right[0] === Type::T_STRING) { @@ -3929,11 +4168,11 @@ private function opAdd($left, $right) * * @return array|Number|null */ - private function opAnd($left, $right, bool $shouldEval) + protected function opAnd($left, $right, $shouldEval) { - $truthy = ($left === self::$null || $right === self::$null) || - ($left === self::$false || $left === self::$true) && - ($right === self::$false || $right === self::$true); + $truthy = ($left === static::$null || $right === static::$null) || + ($left === static::$false || $left === static::$true) && + ($right === static::$false || $right === static::$true); if (! $shouldEval) { if (! $truthy) { @@ -3941,7 +4180,7 @@ private function opAnd($left, $right, bool $shouldEval) } } - if ($left !== self::$false && $left !== self::$null) { + if ($left !== static::$false && $left !== static::$null) { return $this->reduce($right, true); } @@ -3957,11 +4196,11 @@ private function opAnd($left, $right, bool $shouldEval) * * @return array|Number|null */ - private function opOr($left, $right, bool $shouldEval) + protected function opOr($left, $right, $shouldEval) { - $truthy = ($left === self::$null || $right === self::$null) || - ($left === self::$false || $left === self::$true) && - ($right === self::$false || $right === self::$true); + $truthy = ($left === static::$null || $right === static::$null) || + ($left === static::$false || $left === static::$true) && + ($right === static::$false || $right === static::$true); if (! $shouldEval) { if (! $truthy) { @@ -3969,7 +4208,7 @@ private function opOr($left, $right, bool $shouldEval) } } - if ($left !== self::$false && $left !== self::$null) { + if ($left !== static::$false && $left !== static::$null) { return $left; } @@ -3985,21 +4224,70 @@ private function opOr($left, $right, bool $shouldEval) * * @return array */ - private function opColorColor(string $op, $left, $right) + protected function opColorColor($op, $left, $right) { - switch ($op) { - case '==': - return $this->opEq($left, $right); + if ($op !== '==' && $op !== '!=') { + $warning = "Color arithmetic is deprecated and will be an error in future versions.\n" + . "Consider using Sass's color functions instead."; + $fname = $this->getPrettyPath($this->sourceNames[$this->sourceIndex]); + $line = $this->sourceLine; - case '!=': - return $this->opNeq($left, $right); + Warn::deprecation("$warning\n on line $line of $fname"); + } - default: - $leftValue = $this->compileValue($left); - $rightValue = $this->compileValue($right); + $out = [Type::T_COLOR]; + + foreach ([1, 2, 3] as $i) { + $lval = isset($left[$i]) ? $left[$i] : 0; + $rval = isset($right[$i]) ? $right[$i] : 0; + + switch ($op) { + case '+': + $out[] = $lval + $rval; + break; + + case '-': + $out[] = $lval - $rval; + break; + + case '*': + $out[] = $lval * $rval; + break; + + case '%': + if ($rval == 0) { + throw $this->error("color: Can't take modulo by zero"); + } + + $out[] = $lval % $rval; + break; + + case '/': + if ($rval == 0) { + throw $this->error("color: Can't divide by zero"); + } - throw new SassScriptException("Unsupported operation \"$leftValue $op $rightValue\"."); + $out[] = (int) ($lval / $rval); + break; + + case '==': + return $this->opEq($left, $right); + + case '!=': + return $this->opNeq($left, $right); + + default: + throw $this->error("color: unknown op $op"); + } + } + + if (isset($left[4])) { + $out[4] = $left[4]; + } elseif (isset($right[4])) { + $out[4] = $right[4]; } + + return $this->fixColor($out); } /** @@ -4011,20 +4299,23 @@ private function opColorColor(string $op, $left, $right) * * @return array */ - private function opColorNumber(string $op, $left, Number $right) + protected function opColorNumber($op, $left, Number $right) { if ($op === '==') { - return self::$false; + return static::$false; } if ($op === '!=') { - return self::$true; + return static::$true; } - $leftValue = $this->compileValue($left); - $rightValue = $this->compileValue($right); + $value = $right->getDimension(); - throw new SassScriptException("Unsupported operation \"$leftValue $op $rightValue\"."); + return $this->opColorColor( + $op, + $left, + [Type::T_COLOR, $value, $value, $value] + ); } /** @@ -4036,20 +4327,23 @@ private function opColorNumber(string $op, $left, Number $right) * * @return array */ - private function opNumberColor(string $op, Number $left, $right) + protected function opNumberColor($op, Number $left, $right) { if ($op === '==') { - return self::$false; + return static::$false; } if ($op === '!=') { - return self::$true; + return static::$true; } - $leftValue = $this->compileValue($left); - $rightValue = $this->compileValue($right); + $value = $left->getDimension(); - throw new SassScriptException("Unsupported operation \"$leftValue $op $rightValue\"."); + return $this->opColorColor( + $op, + [Type::T_COLOR, $value, $value, $value], + $right + ); } /** @@ -4060,7 +4354,7 @@ private function opNumberColor(string $op, Number $left, $right) * * @return array */ - private function opEq($left, $right) + protected function opEq($left, $right) { if (($lStr = $this->coerceString($left)) && ($rStr = $this->coerceString($right))) { $lStr[1] = ''; @@ -4081,7 +4375,7 @@ private function opEq($left, $right) * * @return array */ - private function opNeq($left, $right) + protected function opNeq($left, $right) { if (($lStr = $this->coerceString($left)) && ($rStr = $this->coerceString($right))) { $lStr[1] = ''; @@ -4102,7 +4396,7 @@ private function opNeq($left, $right) * * @return array */ - private function opEqNumberNumber(Number $left, Number $right) + protected function opEqNumberNumber(Number $left, Number $right) { return $this->toBool($left->equals($right)); } @@ -4115,7 +4409,7 @@ private function opEqNumberNumber(Number $left, Number $right) * * @return array */ - private function opNeqNumberNumber(Number $left, Number $right) + protected function opNeqNumberNumber(Number $left, Number $right) { return $this->toBool(!$left->equals($right)); } @@ -4128,7 +4422,7 @@ private function opNeqNumberNumber(Number $left, Number $right) * * @return array */ - private function opGteNumberNumber(Number $left, Number $right) + protected function opGteNumberNumber(Number $left, Number $right) { return $this->toBool($left->greaterThanOrEqual($right)); } @@ -4141,7 +4435,7 @@ private function opGteNumberNumber(Number $left, Number $right) * * @return array */ - private function opGtNumberNumber(Number $left, Number $right) + protected function opGtNumberNumber(Number $left, Number $right) { return $this->toBool($left->greaterThan($right)); } @@ -4154,7 +4448,7 @@ private function opGtNumberNumber(Number $left, Number $right) * * @return array */ - private function opLteNumberNumber(Number $left, Number $right) + protected function opLteNumberNumber(Number $left, Number $right) { return $this->toBool($left->lessThanOrEqual($right)); } @@ -4167,7 +4461,7 @@ private function opLteNumberNumber(Number $left, Number $right) * * @return array */ - private function opLtNumberNumber(Number $left, Number $right) + protected function opLtNumberNumber(Number $left, Number $right) { return $this->toBool($left->lessThan($right)); } @@ -4175,24 +4469,28 @@ private function opLtNumberNumber(Number $left, Number $right) /** * Cast to boolean * + * @api + * * @param bool $thing * * @return array */ - public function toBool(bool $thing) + public function toBool($thing) { - return $thing ? self::$true : self::$false; + return $thing ? static::$true : static::$false; } /** * Escape non printable chars in strings output as in dart-sass * + * @internal + * * @param string $string * @param bool $inKeyword * * @return string */ - private function escapeNonPrintableChars(string $string, bool $inKeyword = false): string + public function escapeNonPrintableChars($string, $inKeyword = false) { static $replacement = []; if (empty($replacement[$inKeyword])) { @@ -4240,12 +4538,14 @@ private function escapeNonPrintableChars(string $string, bool $inKeyword = false * The input is expected to be reduced. This function will not work on * things like expressions and variables. * + * @api + * * @param array|Number $value * @param bool $quote * * @return string */ - public function compileValue($value, bool $quote = true): string + public function compileValue($value, $quote = true) { $value = $this->reduce($value); @@ -4278,8 +4578,10 @@ public function compileValue($value, bool $quote = true): string return $colorName; } - if (is_numeric($alpha)) { + if (\is_int($alpha) || \is_float($alpha)) { $a = new Number($alpha, ''); + } elseif (is_numeric($alpha)) { + $a = new Number((float) $alpha, ''); } else { $a = $alpha; } @@ -4521,7 +4823,7 @@ public function compileValue($value, bool $quote = true): string * * @return string */ - private function compileDebugValue($value): string + protected function compileDebugValue($value) { $value = $this->reduce($value, true); @@ -4538,6 +4840,22 @@ private function compileDebugValue($value): string } } + /** + * Flatten list + * + * @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 * @@ -4548,7 +4866,7 @@ private function compileDebugValue($value): string * * @return string */ - public function getStringText(array $value): 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"?'); @@ -4565,7 +4883,7 @@ public function getStringText(array $value): string * * @return string */ - private function compileStringContent($string, bool $quote = true): string + protected function compileStringContent($string, $quote = true) { $parts = []; @@ -4587,7 +4905,7 @@ private function compileStringContent($string, bool $quote = true): string * * @return array */ - private function extractInterpolation($list) + protected function extractInterpolation($list) { $items = $list[2]; @@ -4606,12 +4924,12 @@ private function extractInterpolation($list) /** * Find the final set of selectors * - * @param Environment $env - * @param Block $selfParent + * @param \ScssPhp\ScssPhp\Compiler\Environment $env + * @param \ScssPhp\ScssPhp\Block $selfParent * * @return array */ - private function multiplySelectors(Environment $env, ?Block $selfParent = null): array + protected function multiplySelectors(Environment $env, $selfParent = null) { $envs = $this->compactEnv($env); $selectors = []; @@ -4674,7 +4992,7 @@ private function multiplySelectors(Environment $env, ?Block $selfParent = null): * @return array */ - private function joinSelectors(array $parent, array $child, &$stillHasSelf, ?array $selfParentSelectors = null) + protected function joinSelectors($parent, $child, &$stillHasSelf, $selfParentSelectors = null) { $setSelf = false; $out = []; @@ -4684,11 +5002,11 @@ private function joinSelectors(array $parent, array $child, &$stillHasSelf, ?arr foreach ($part as $p) { // only replace & once and should be recalled to be able to make combinations - if ($p === self::$selfSelector && $setSelf) { + if ($p === static::$selfSelector && $setSelf) { $stillHasSelf = true; } - if ($p === self::$selfSelector && ! $setSelf) { + if ($p === static::$selfSelector && ! $setSelf) { $setSelf = true; if (\is_null($selfParentSelectors)) { @@ -4729,12 +5047,12 @@ private function joinSelectors(array $parent, array $child, &$stillHasSelf, ?arr /** * Multiply media * - * @param Environment $env - * @param array|null $childQueries + * @param \ScssPhp\ScssPhp\Compiler\Environment $env + * @param array $childQueries * * @return array */ - private function multiplyMedia(Environment $env = null, ?array $childQueries = null): array + protected function multiplyMedia(?Environment $env = null, $childQueries = null) { if ( ! isset($env) || @@ -4791,7 +5109,7 @@ private function multiplyMedia(Environment $env = null, ?array $childQueries = n * * @phpstan-return non-empty-array */ - private function compactEnv(Environment $env) + protected function compactEnv(Environment $env) { for ($envs = []; $env; $env = $env->parent) { $envs[] = $env; @@ -4809,7 +5127,7 @@ private function compactEnv(Environment $env) * * @phpstan-param non-empty-array $envs */ - private function extractEnv(array $envs): Environment + protected function extractEnv($envs) { for ($env = null; $e = array_pop($envs);) { $e->parent = $env; @@ -4822,11 +5140,11 @@ private function extractEnv(array $envs): Environment /** * Push environment * - * @param Block $block + * @param \ScssPhp\ScssPhp\Block $block * - * @return Environment + * @return \ScssPhp\ScssPhp\Compiler\Environment */ - private function pushEnv(Block $block = null): Environment + protected function pushEnv(?Block $block = null) { $env = new Environment(); $env->parent = $this->env; @@ -4846,7 +5164,7 @@ private function pushEnv(Block $block = null): Environment * * @return void */ - private function popEnv(): void + protected function popEnv() { $this->storeEnv = $this->env->parentStore; $this->env = $this->env->parent; @@ -4860,7 +5178,7 @@ private function popEnv(): void * * @return void */ - private function backPropagateEnv(array $store, ?array $excludedVars = null): void + protected function backPropagateEnv($store, $excludedVars = null) { foreach ($store as $key => $value) { if (empty($excludedVars) || ! \in_array($key, $excludedVars)) { @@ -4872,9 +5190,9 @@ private function backPropagateEnv(array $store, ?array $excludedVars = null): vo /** * Get store environment * - * @return Environment + * @return \ScssPhp\ScssPhp\Compiler\Environment */ - private function getStoreEnv(): Environment + protected function getStoreEnv() { return isset($this->storeEnv) ? $this->storeEnv : $this->env; } @@ -4885,12 +5203,12 @@ private function getStoreEnv(): Environment * @param string $name * @param mixed $value * @param bool $shadow - * @param Environment $env + * @param \ScssPhp\ScssPhp\Compiler\Environment $env * @param mixed $valueUnreduced * * @return void */ - private function set(string $name, $value, bool $shadow = false, Environment $env = null, $valueUnreduced = null): void + protected function set($name, $value, $shadow = false, ?Environment $env = null, $valueUnreduced = null) { $name = $this->normalizeName($name); @@ -4910,15 +5228,15 @@ private function set(string $name, $value, bool $shadow = false, Environment $en * * @param string $name * @param mixed $value - * @param Environment $env + * @param \ScssPhp\ScssPhp\Compiler\Environment $env * @param mixed $valueUnreduced * * @return void */ - private function setExisting(string $name, $value, Environment $env, $valueUnreduced = null): void + protected function setExisting($name, $value, Environment $env, $valueUnreduced = null) { $storeEnv = $env; - $specialContentKey = self::$namespaces['special'] . 'content'; + $specialContentKey = static::$namespaces['special'] . 'content'; $hasNamespace = $name[0] === '^' || $name[0] === '@' || $name[0] === '%'; @@ -4968,14 +5286,14 @@ private function setExisting(string $name, $value, Environment $env, $valueUnred /** * Set raw variable * - * @param string $name - * @param mixed $value - * @param Environment $env - * @param mixed $valueUnreduced + * @param string $name + * @param mixed $value + * @param \ScssPhp\ScssPhp\Compiler\Environment $env + * @param mixed $valueUnreduced * * @return void */ - private function setRaw(string $name, $value, Environment $env, $valueUnreduced = null): void + protected function setRaw($name, $value, Environment $env, $valueUnreduced = null) { $env->store[$name] = $value; @@ -4987,17 +5305,19 @@ private function setRaw(string $name, $value, Environment $env, $valueUnreduced /** * Get variable * - * @param string $name - * @param bool $shouldThrow - * @param Environment|null $env - * @param bool $unreduced + * @internal + * + * @param string $name + * @param bool $shouldThrow + * @param \ScssPhp\ScssPhp\Compiler\Environment $env + * @param bool $unreduced * * @return mixed|null */ - private function get(string $name, bool $shouldThrow = true, ?Environment $env = null, bool $unreduced = false) + public function get($name, $shouldThrow = true, ?Environment $env = null, $unreduced = false) { $normalizedName = $this->normalizeName($name); - $specialContentKey = self::$namespaces['special'] . 'content'; + $specialContentKey = static::$namespaces['special'] . 'content'; if (! isset($env)) { $env = $this->getStoreEnv(); @@ -5054,12 +5374,12 @@ private function get(string $name, bool $shouldThrow = true, ?Environment $env = /** * Has variable? * - * @param string $name - * @param Environment|null $env + * @param string $name + * @param \ScssPhp\ScssPhp\Compiler\Environment $env * * @return bool */ - private function has(string $name, Environment $env = null): bool + protected function has($name, ?Environment $env = null) { return ! \is_null($this->get($name, false, $env)); } @@ -5071,7 +5391,7 @@ private function has(string $name, Environment $env = null): bool * * @return void */ - private function injectVariables(array $args): void + protected function injectVariables(array $args) { if (empty($args)) { return; @@ -5099,7 +5419,7 @@ private function injectVariables(array $args): void * * @return void */ - public function replaceVariables(array $variables): void + public function replaceVariables(array $variables) { $this->registeredVars = []; $this->addVariables($variables); @@ -5112,25 +5432,51 @@ public function replaceVariables(array $variables): void * * @return void */ - public function addVariables(array $variables): void + public function addVariables(array $variables) { + $triggerWarning = false; + foreach ($variables as $name => $value) { if (!$value instanceof Number && !\is_array($value)) { - throw new \InvalidArgumentException('Passing raw values to as custom variables to the Compiler is not supported anymore. Use "\ScssPhp\ScssPhp\ValueConverter::parseValue" or "\ScssPhp\ScssPhp\ValueConverter::fromPhp" to convert them instead.'); + $triggerWarning = true; } $this->registeredVars[$name] = $value; } + + if ($triggerWarning) { + @trigger_error('Passing raw values to as custom variables to the Compiler is deprecated. Use "\ScssPhp\ScssPhp\ValueConverter::parseValue" or "\ScssPhp\ScssPhp\ValueConverter::fromPhp" to convert them instead.', E_USER_DEPRECATED); + } + } + + /** + * Set variables + * + * @api + * + * @param array $variables + * + * @return void + * + * @deprecated Use "addVariables" or "replaceVariables" instead. + */ + public function setVariables(array $variables) + { + @trigger_error('The method "setVariables" of the Compiler is deprecated. Use the "addVariables" method for the equivalent behavior or "replaceVariables" if merging with previous variables was not desired.'); + + $this->addVariables($variables); } /** * Unset variable * + * @api + * * @param string $name * * @return void */ - public function unsetVariable(string $name): void + public function unsetVariable($name) { unset($this->registeredVars[$name]); } @@ -5138,9 +5484,11 @@ public function unsetVariable(string $name): void /** * Returns list of variables * + * @api + * * @return array */ - public function getVariables(): array + public function getVariables() { return $this->registeredVars; } @@ -5148,25 +5496,41 @@ public function getVariables(): array /** * Adds to list of parsed files * + * @internal + * * @param string|null $path * * @return void */ - private function addParsedFile(?string $path): void + public function addParsedFile($path) { if (! \is_null($path) && is_file($path)) { $this->parsedFiles[realpath($path)] = filemtime($path); } } + /** + * Returns list of parsed files + * + * @deprecated + * @return array + */ + public function getParsedFiles() + { + @trigger_error('The method "getParsedFiles" of the Compiler is deprecated. Use the "getIncludedFiles" method on the CompilationResult instance returned by compileString() instead. Be careful that the signature of the method is different.', E_USER_DEPRECATED); + return $this->parsedFiles; + } + /** * Add import path * + * @api + * * @param string|callable $path * * @return void */ - public function addImportPath($path): void + public function addImportPath($path) { if (! \in_array($path, $this->importPaths)) { $this->importPaths[] = $path; @@ -5176,39 +5540,65 @@ public function addImportPath($path): void /** * Set import paths * + * @api + * * @param string|array $path * * @return void */ - public function setImportPaths($path): void + public function setImportPaths($path) { $paths = (array) $path; $actualImportPaths = array_filter($paths, function ($path) { return $path !== ''; }); - if (\count($actualImportPaths) !== \count($paths)) { - throw new \InvalidArgumentException('Passing an empty string in the import paths to refer to the current working directory is not supported anymore. 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.'); + $this->legacyCwdImportPath = \count($actualImportPaths) !== \count($paths); + + if ($this->legacyCwdImportPath) { + @trigger_error('Passing an empty string in the import paths to refer to the current working directory is deprecated. If that\'s the intended behavior, the value of "getcwd()" should be used directly instead. If this was used for resolving relative imports of the input alongside "chdir" with the source directory, the path of the input file should be passed to "compileString()" instead.', E_USER_DEPRECATED); } $this->importPaths = $actualImportPaths; } + /** + * Set number precision + * + * @api + * + * @param int $numberPrecision + * + * @return void + * + * @deprecated The number precision is not configurable anymore. The default is enough for all browsers. + */ + public function setNumberPrecision($numberPrecision) + { + @trigger_error('The number precision is not configurable anymore. ' + . 'The default is enough for all browsers.', E_USER_DEPRECATED); + } + /** * Sets the output style. * + * @api + * * @param string $style One of the OutputStyle constants * * @return void * * @phpstan-param OutputStyle::* $style */ - public function setOutputStyle(string $style): void + public function setOutputStyle($style) { switch ($style) { case OutputStyle::EXPANDED: + $this->configuredFormatter = Expanded::class; + break; + case OutputStyle::COMPRESSED: - $this->outputStyle = $style; + $this->configuredFormatter = Compressed::class; break; default: @@ -5216,6 +5606,46 @@ public function setOutputStyle(string $style): void } } + /** + * Set formatter + * + * @api + * + * @param string $formatterName + * + * @return void + * + * @deprecated Use {@see setOutputStyle} instead. + * + * @phpstan-param class-string $formatterName + */ + public function setFormatter($formatterName) + { + if (!\in_array($formatterName, [Expanded::class, Compressed::class], true)) { + @trigger_error('Formatters other than Expanded and Compressed are deprecated.', E_USER_DEPRECATED); + } + @trigger_error('The method "setFormatter" is deprecated. Use "setOutputStyle" instead.', E_USER_DEPRECATED); + + $this->configuredFormatter = $formatterName; + } + + /** + * Set line number style + * + * @api + * + * @param string $lineNumberStyle + * + * @return void + * + * @deprecated The line number output is not supported anymore. Use source maps instead. + */ + public function setLineNumberStyle($lineNumberStyle) + { + @trigger_error('The line number output is not supported anymore. ' + . 'Use source maps instead.', E_USER_DEPRECATED); + } + /** * Configures the handling of non-ASCII outputs. * @@ -5225,8 +5655,12 @@ public function setOutputStyle(string $style): void * byte-order mark. * * [byte-order mark]: https://en.wikipedia.org/wiki/Byte_order_mark#UTF-8 + * + * @param bool $charset + * + * @return void */ - public function setCharset(bool $charset): void + public function setCharset($charset) { $this->charset = $charset; } @@ -5234,13 +5668,15 @@ public function setCharset(bool $charset): void /** * Enable/disable source maps * + * @api + * * @param int $sourceMap * * @return void * * @phpstan-param self::SOURCE_MAP_* $sourceMap */ - public function setSourceMap(int $sourceMap): void + public function setSourceMap($sourceMap) { $this->sourceMap = $sourceMap; } @@ -5248,13 +5684,15 @@ public function setSourceMap(int $sourceMap): void /** * Set source map options * + * @api + * * @param array $sourceMapOptions * * @phpstan-param array{sourceRoot?: string, sourceMapFilename?: string|null, sourceMapURL?: string|null, sourceMapWriteTo?: string|null, outputSourceFiles?: bool, sourceMapRootpath?: string, sourceMapBasepath?: string} $sourceMapOptions * * @return void */ - public function setSourceMapOptions(array $sourceMapOptions): void + public function setSourceMapOptions($sourceMapOptions) { $this->sourceMapOptions = $sourceMapOptions; } @@ -5262,42 +5700,94 @@ public function setSourceMapOptions(array $sourceMapOptions): void /** * Register function * - * @param string $name - * @param callable $callback - * @param string[] $argumentDeclaration + * @api + * + * @param string $name + * @param callable $callback + * @param string[]|null $argumentDeclaration * * @return void */ - public function registerFunction(string $name, callable $callback, array $argumentDeclaration): void + public function registerFunction($name, $callback, $argumentDeclaration = null) { if (self::isNativeFunction($name)) { - throw new \InvalidArgumentException(sprintf('The "%s" function is a core sass function. Overriding it with a custom implementation through "%s" is not supported .', $name, __METHOD__)); + @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); + } + + if ($this->reflectCallable($callback)->getNumberOfRequiredParameters() > 1) { + @trigger_error('The second argument passed to the callback of custom functions is deprecated and won\'t be supported in ScssPhp 2.0 anymore. Register a callback accepting only 1 parameter instead.', E_USER_DEPRECATED); } $this->userFunctions[$this->normalizeName($name)] = [$callback, $argumentDeclaration]; } + /** + * @return \ReflectionFunctionAbstract + */ + private function reflectCallable(callable $c) + { + if (\is_object($c) && !$c instanceof \Closure) { + $c = [$c, '__invoke']; + } + + if (\is_string($c) && false !== strpos($c, '::')) { + $c = explode('::', $c, 2); + } + + if (\is_array($c)) { + return new \ReflectionMethod($c[0], $c[1]); + } + + \assert(\is_string($c) || $c instanceof \Closure); + + return new \ReflectionFunction($c); + } + /** * Unregister function * + * @api + * * @param string $name * * @return void */ - public function unregisterFunction(string $name): void + public function unregisterFunction($name) { unset($this->userFunctions[$this->normalizeName($name)]); } + /** + * Add feature + * + * @api + * + * @param string $name + * + * @return void + * + * @deprecated Registering additional features is deprecated. + */ + public function addFeature($name) + { + @trigger_error('Registering additional features is deprecated.', E_USER_DEPRECATED); + + $this->registeredFeatures[$name] = true; + } + /** * Import file * - * @param string $path - * @param OutputBlock $out + * @param string $path + * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $out * * @return void */ - private function importFile(string $path, OutputBlock $out): void + protected function importFile($path, OutputBlock $out) { $this->pushCallStack('import ' . $this->getPrettyPath($path)); // see if tree is cached @@ -5345,7 +5835,7 @@ private function importFile(string $path, OutputBlock $out): void * * @return void */ - private function registerImport(?string $currentDirectory, string $path, string $filePath): void + private function registerImport($currentDirectory, $path, $filePath) { $this->resolvedImports[] = ['currentDir' => $currentDirectory, 'path' => $path, 'filePath' => $filePath]; } @@ -5353,11 +5843,16 @@ private function registerImport(?string $currentDirectory, string $path, string /** * 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(string $url): bool + public static function isCssImport($url) { return 1 === preg_match('~\.css$|^https?://|^//~', $url); } @@ -5365,15 +5860,50 @@ public static function isCssImport(string $url): bool /** * Return the file path for an import url if it exists * + * @internal + * * @param string $url * @param string|null $currentDir * * @return string|null */ - private function findImport(string $url, ?string $currentDir = null): ?string + public function findImport($url, $currentDir = null) { // Vanilla css and external requests. These are not meant to be Sass imports. + // Callback importers are still called for BC. if (self::isCssImport($url)) { + foreach ($this->importPaths as $dir) { + if (\is_string($dir)) { + continue; + } + + if (\is_callable($dir)) { + // check custom callback for import path + $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; + } + } + } return null; } @@ -5402,6 +5932,16 @@ private function findImport(string $url, ?string $currentDir = null): ?string } } + if ($this->legacyCwdImportPath) { + $path = $this->resolveImportPath($url, getcwd()); + + if (!\is_null($path)) { + @trigger_error('Resolving imports relatively to the current working directory is deprecated. If that\'s the intended behavior, the value of "getcwd()" should be added as an import path explicitly instead. If this was used for resolving relative imports of the input alongside "chdir" with the source directory, the path of the input file should be passed to "compileString()" instead.', E_USER_DEPRECATED); + + return $path; + } + } + throw $this->error("`$url` file not found for @import"); } @@ -5411,7 +5951,7 @@ private function findImport(string $url, ?string $currentDir = null): ?string * * @return string|null */ - private function resolveImportPath(string $url, string $baseDir): ?string + private function resolveImportPath($url, $baseDir) { $path = Path::join($baseDir, $url); @@ -5435,7 +5975,7 @@ private function resolveImportPath(string $url, string $baseDir): ?string * * @return string|null */ - private function checkImportPathConflicts(array $paths): ?string + private function checkImportPathConflicts(array $paths) { if (\count($paths) === 0) { return null; @@ -5459,18 +5999,18 @@ private function checkImportPathConflicts(array $paths): ?string * * @return string[] */ - private function tryImportPathWithExtensions(string $path): array + private function tryImportPathWithExtensions($path) { $result = array_merge( - $this->tryImportPath($path.'.sass'), - $this->tryImportPath($path.'.scss') + $this->tryImportPath($path . '.sass'), + $this->tryImportPath($path . '.scss') ); if ($result) { return $result; } - return $this->tryImportPath($path.'.css'); + return $this->tryImportPath($path . '.css'); } /** @@ -5478,9 +6018,9 @@ private function tryImportPathWithExtensions(string $path): array * * @return string[] */ - private function tryImportPath(string $path): array + private function tryImportPath($path) { - $partial = dirname($path).'/_'.basename($path); + $partial = dirname($path) . '/_' . basename($path); $candidates = []; @@ -5500,13 +6040,13 @@ private function tryImportPath(string $path): array * * @return string|null */ - private function tryImportPathAsDirectory(string $path): ?string + private function tryImportPathAsDirectory($path) { if (!is_dir($path)) { return null; } - return $this->checkImportPathConflicts($this->tryImportPathWithExtensions($path.'/index')); + return $this->checkImportPathConflicts($this->tryImportPathWithExtensions($path . '/index')); } /** @@ -5514,14 +6054,14 @@ private function tryImportPathAsDirectory(string $path): ?string * * @return string */ - private function getPrettyPath(?string $path): string + private function getPrettyPath($path) { if ($path === null) { return '(unknown file)'; } $normalizedPath = $path; - $normalizedRootDirectory = $this->rootDirectory.'/'; + $normalizedRootDirectory = $this->rootDirectory . '/'; if (\DIRECTORY_SEPARATOR === '\\') { $normalizedRootDirectory = str_replace('\\', '/', $normalizedRootDirectory); @@ -5535,21 +6075,106 @@ private function getPrettyPath(?string $path): string return $path; } + /** + * Set encoding + * + * @api + * + * @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; + } + + /** + * Ignore errors? + * + * @api + * + * @param bool $ignoreErrors + * + * @return \ScssPhp\ScssPhp\Compiler + * + * @deprecated Ignoring Sass errors is not longer supported. + */ + public function setIgnoreErrors($ignoreErrors) + { + @trigger_error('Ignoring Sass errors is not longer supported.', E_USER_DEPRECATED); + + return $this; + } + + /** + * Get source position + * + * @api + * + * @return array + * + * @deprecated + */ + public function getSourcePosition() + { + @trigger_error(sprintf('The "%s" method is deprecated.', __METHOD__), E_USER_DEPRECATED); + + $sourceFile = isset($this->sourceNames[$this->sourceIndex]) ? $this->sourceNames[$this->sourceIndex] : ''; + + return [$sourceFile, $this->sourceLine, $this->sourceColumn]; + } + + /** + * Throw error (exception) + * + * @api + * + * @param string $msg Message with optional sprintf()-style vararg parameters + * + * @return never + * + * @throws \ScssPhp\ScssPhp\Exception\CompilerException + * + * @deprecated use "error" and throw the exception in the caller instead. + */ + public function throwError($msg) + { + @trigger_error( + 'The method "throwError" is deprecated. Use "error" and throw the exception in the caller instead', + E_USER_DEPRECATED + ); + + throw $this->error(...func_get_args()); + } + /** * Build an error (exception) * + * @internal + * * @param string $msg Message with optional sprintf()-style vararg parameters * @param bool|float|int|string|null ...$args * * @return CompilerException */ - private function error(string $msg, ...$args): CompilerException + public function error($msg, ...$args) { if ($args) { $msg = sprintf($msg, ...$args); } - $msg = $this->addLocationToMessage($msg); + if (! $this->ignoreCallStackMessage) { + $msg = $this->addLocationToMessage($msg); + } return new CompilerException($msg); } @@ -5559,7 +6184,7 @@ private function error(string $msg, ...$args): CompilerException * * @return string */ - private function addLocationToMessage(string $msg): string + private function addLocationToMessage($msg) { $line = $this->sourceLine; $column = $this->sourceColumn; @@ -5579,6 +6204,43 @@ private function addLocationToMessage(string $msg): string return $msg; } + /** + * @param string $functionName + * @param array $ExpectedArgs + * @param int $nbActual + * @return CompilerException + * + * @deprecated + */ + public function errorArgsNumber($functionName, $ExpectedArgs, $nbActual) + { + @trigger_error(sprintf('The "%s" method is deprecated.', __METHOD__), E_USER_DEPRECATED); + + $nbExpected = \count($ExpectedArgs); + + if ($nbActual > $nbExpected) { + return $this->error( + 'Error: Only %d arguments allowed in %s(), but %d were passed.', + $nbExpected, + $functionName, + $nbActual + ); + } else { + $missing = []; + + while (count($ExpectedArgs) && count($ExpectedArgs) > $nbActual) { + array_unshift($missing, array_pop($ExpectedArgs)); + } + + return $this->error( + 'Error: %s() argument%s %s missing.', + $functionName, + count($missing) > 1 ? 's' : '', + implode(', ', $missing) + ); + } + } + /** * Beautify call stack for output * @@ -5587,7 +6249,7 @@ private function addLocationToMessage(string $msg): string * * @return string */ - private function callStackMessage(bool $all = false, ?int $limit = null): string + protected function callStackMessage($all = false, $limit = null) { $callStackMsg = []; $ncall = 0; @@ -5622,7 +6284,7 @@ private function callStackMessage(bool $all = false, ?int $limit = null): string * * @throws \Exception */ - private function handleImportLoop($name): void + protected function handleImportLoop($name) { for ($env = $this->env; $env; $env = $env->parent) { if (! $env->block) { @@ -5649,10 +6311,10 @@ private function handleImportLoop($name): void * * @return array|Number */ - private function callScssFunction($func, $argValues) + protected function callScssFunction($func, $argValues) { if (! $func) { - return self::$defaultValue; + return static::$defaultValue; } $name = $func->name; @@ -5680,25 +6342,28 @@ private function callScssFunction($func, $argValues) $this->popEnv(); - return ! isset($ret) ? self::$defaultValue : $ret; + return ! isset($ret) ? static::$defaultValue : $ret; } /** * Call built-in and registered (PHP) functions * - * @param string $name + * @param string $name * @param callable $function - * @param array $prototype - * @param array $args + * @param array $prototype + * @param array $args * * @return array|Number|null */ - private function callNativeFunction(string $name, callable $function, array $prototype, array $args) + protected function callNativeFunction($name, $function, $prototype, $args) { $libName = (is_array($function) ? end($function) : null); $sorted_kwargs = $this->sortNativeFunctionArgs($libName, $prototype, $args); - list($sorted, $kwargs) = $sorted_kwargs; + if (\is_null($sorted_kwargs)) { + return null; + } + @list($sorted, $kwargs) = $sorted_kwargs; if ($name !== 'if') { foreach ($sorted as &$val) { @@ -5718,7 +6383,9 @@ private function callNativeFunction(string $name, callable $function, array $pro return $returnValue; } - throw new \UnexpectedValueException(sprintf('The custom function "%s" must return a sass value.', $name)); + @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); } /** @@ -5726,39 +6393,24 @@ private function callNativeFunction(string $name, callable $function, array $pro * * @param string $name Normalized name * - * @return array|null + * @return array */ - private function getBuiltinFunction(string $name): ?array + protected function getBuiltinFunction($name) { - // All core functions have lowercase names, and they are case-sensitive. - if (strtolower($name) !== $name) { - return null; - } - $libName = self::normalizeNativeFunctionName($name); - - // All core functions have a prototype defined. Not finding the - // prototype can mean 2 things: - // - the function does not exist at all (handled by the caller) - // - the function exists 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 (!isset(self::$$libName)) { - return null; - } - return [$this, $libName]; } /** * Normalize native function name * + * @internal + * * @param string $name * * @return string */ - private static function normalizeNativeFunctionName(string $name): string + public static function normalizeNativeFunctionName($name) { $name = str_replace("-", "_", $name); $libName = 'lib' . preg_replace_callback( @@ -5780,28 +6432,44 @@ function ($m) { * * @return bool */ - public static function isNativeFunction(string $name): bool + public static function isNativeFunction($name) { - if (strtolower($name) !== $name) { - return false; - } - - $libName = self::normalizeNativeFunctionName($name); - - return method_exists(Compiler::class, $libName) && isset(self::$$libName); + return method_exists(Compiler::class, self::normalizeNativeFunctionName($name)); } /** * Sorts keyword arguments * - * @param string|null $functionName - * @param array $prototypes - * @param array $args + * @param string $functionName + * @param array|null $prototypes + * @param array $args * - * @return array + * @return array|null */ - private function sortNativeFunctionArgs(?string $functionName, array $prototypes, array $args): array + protected function sortNativeFunctionArgs($functionName, $prototypes, $args) { + if (! isset($prototypes)) { + $keyArgs = []; + $posArgs = []; + + if (\is_array($args) && \count($args) && \end($args) === static::$null) { + array_pop($args); + } + + // separate positional and keyword arguments + foreach ($args as $arg) { + list($key, $value) = $arg; + + if (empty($key) or empty($key[1])) { + $posArgs[] = empty($arg[2]) ? $value : $arg; + } else { + $keyArgs[$key[1]] = $value; + } + } + + return [$posArgs, $keyArgs]; + } + // specific cases ? if (\in_array($functionName, ['libRgb', 'libRgba', 'libHsl', 'libHsla'])) { // notation 100 127 255 / 0 is in fact a simple list of 4 values @@ -5876,7 +6544,7 @@ private function sortNativeFunctionArgs(?string $functionName, array $prototypes * @return array * @phpstan-return array{arguments: list, rest_argument: string|null} */ - private function parseFunctionPrototype(array $prototype): array + private function parseFunctionPrototype(array $prototype) { static $parser = null; @@ -5896,7 +6564,7 @@ private function parseFunctionPrototype(array $prototype): array $defaultSource = trim($p[1]); if ($defaultSource === 'null') { - // differentiate this null from the self::$null + // differentiate this null from the static::$null $default = [Type::T_KEYWORD, 'null']; } else { if (\is_null($parser)) { @@ -5933,10 +6601,10 @@ private function parseFunctionPrototype(array $prototype): array * * @return array * - * @phpstan-param non-empty-list, rest_argument: string|null}> $prototypes + * @phpstan-param non-empty-array, rest_argument: string|null}> $prototypes * @phpstan-return array{arguments: list, rest_argument: string|null} */ - private function selectFunctionPrototype(array $prototypes, int $positional, array $names): array + private function selectFunctionPrototype(array $prototypes, $positional, array $names) { $fuzzyMatch = null; $minMismatchDistance = null; @@ -5983,7 +6651,7 @@ private function selectFunctionPrototype(array $prototypes, int $positional, arr * * @phpstan-param array{arguments: list, rest_argument: string|null} $prototype */ - private function checkPrototypeMatches(array $prototype, int $positional, array $names): bool + private function checkPrototypeMatches(array $prototype, $positional, array $names) { $nameUsed = 0; @@ -6030,7 +6698,7 @@ private function checkPrototypeMatches(array $prototype, int $positional, array * * @phpstan-param array{arguments: list, rest_argument: string|null} $prototype */ - private function verifyPrototype(array $prototype, int $positional, array $names, bool $hasSplat): void + private function verifyPrototype(array $prototype, $positional, array $names, $hasSplat) { $nameUsed = 0; @@ -6102,10 +6770,10 @@ private function verifyPrototype(array $prototype, int $positional, array $names * * @phpstan-return array{0: list, 1: array, 2: array, 3: string|null, 4: bool} */ - private function evaluateArguments(array $args, bool $reduce = true): array + private function evaluateArguments(array $args, $reduce = true) { // this represents trailing commas - if (\count($args) && end($args) === self::$null) { + if (\count($args) && end($args) === static::$null) { array_pop($args); } @@ -6200,7 +6868,7 @@ private function evaluateArguments(array $args, bool $reduce = true): array * * @return array|Number */ - private function maybeReduce(bool $reduce, $value) + private function maybeReduce($reduce, $value) { if ($reduce) { return $this->reduce($value, true); @@ -6223,7 +6891,7 @@ private function maybeReduce(bool $reduce, $value) * * @throws \Exception */ - private function applyArguments(array $argDef, ?array $argValues, bool $storeInEnv = true, bool $reduce = true) + protected function applyArguments($argDef, $argValues, $storeInEnv = true, $reduce = true) { $output = []; @@ -6330,7 +6998,7 @@ private function applyArguments(array $argDef, ?array $argValues, bool $storeInE * * @phpstan-param array{arguments: list, rest_argument: string|null} $prototype */ - private function applyArgumentsToDeclaration(array $prototype, array $positionalArgs, array $namedArgs, ?string $splatSeparator): array + private function applyArgumentsToDeclaration(array $prototype, array $positionalArgs, array $namedArgs, $splatSeparator) { $output = []; $minLength = min(\count($positionalArgs), \count($prototype['arguments'])); @@ -6377,7 +7045,7 @@ private function applyArgumentsToDeclaration(array $prototype, array $positional * * @return array|Number */ - private function coerceValue($value) + protected function coerceValue($value) { if (\is_array($value) || $value instanceof Number) { return $value; @@ -6388,15 +7056,19 @@ private function coerceValue($value) } if (\is_null($value)) { - return self::$null; + return static::$null; } - if (is_numeric($value)) { + if (\is_int($value) || \is_float($value)) { return new Number($value, ''); } + if (is_numeric($value)) { + return new Number((float) $value, ''); + } + if ($value === '') { - return self::$emptyString; + return static::$emptyString; } $value = [Type::T_KEYWORD, $value]; @@ -6430,7 +7102,7 @@ private function tryMap($item) $item[0] === Type::T_LIST && $item[2] === [] ) { - return self::$emptyMap; + return static::$emptyMap; } return null; @@ -6463,7 +7135,7 @@ protected function coerceMap($item) * * @return array */ - private function coerceList($item, string $delim = ',', bool $removeTrailingNull = false): array + protected function coerceList($item, $delim = ',', $removeTrailingNull = false) { if ($item instanceof Number) { return [Type::T_LIST, '', [$item]]; @@ -6471,7 +7143,7 @@ private function coerceList($item, string $delim = ',', bool $removeTrailingNull if ($item[0] === Type::T_LIST) { // remove trailing null from the list - if ($removeTrailingNull && end($item[2]) === self::$null) { + if ($removeTrailingNull && end($item[2]) === static::$null) { array_pop($item[2]); } @@ -6507,7 +7179,7 @@ private function coerceList($item, string $delim = ',', bool $removeTrailingNull * * @return array|Number */ - private function coerceForExpression($value) + protected function coerceForExpression($value) { if ($color = $this->coerceColor($value)) { return $color; @@ -6524,7 +7196,7 @@ private function coerceForExpression($value) * * @return array|null */ - private function coerceColor($value, bool $inRGBFunction = false) + protected function coerceColor($value, $inRGBFunction = false) { if ($value instanceof Number) { return null; @@ -6643,7 +7315,7 @@ private function coerceColor($value, bool $inRGBFunction = false) * * @return int|mixed */ - private function compileRGBAValue($value, bool $isAlpha = false) + protected function compileRGBAValue($value, $isAlpha = false) { if ($isAlpha) { return $this->compileColorPartValue($value, 0, 1, false); @@ -6660,7 +7332,7 @@ private function compileRGBAValue($value, bool $isAlpha = false) * * @return int|mixed */ - private function compileColorPartValue($value, $min, $max, bool $isInt = true) + protected function compileColorPartValue($value, $min, $max, $isInt = true) { if (! is_numeric($value)) { if (\is_array($value)) { @@ -6706,7 +7378,7 @@ private function compileColorPartValue($value, $min, $max, bool $isInt = true) * * @return array */ - private function coerceString($value): array + protected function coerceString($value) { if ($value[0] === Type::T_STRING) { assert(\is_array($value)); @@ -6725,6 +7397,8 @@ private function coerceString($value): array * other types. * The returned value is always using the T_STRING type. * + * @api + * * @param array|Number $value * @param string|null $varName * @@ -6732,7 +7406,7 @@ private function coerceString($value): array * * @throws SassScriptException */ - public function assertString($value, ?string $varName = null) + public function assertString($value, $varName = null) { // case of url(...) parsed a a function if ($value[0] === Type::T_FUNCTION) { @@ -6747,9 +7421,35 @@ public function assertString($value, ?string $varName = null) return $this->coerceString($value); } + /** + * Coerce value to a percentage + * + * @param array|Number $value + * + * @return int|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; + } + + return $value->getDimension(); + } + + return 0; + } + /** * Assert value is a map * + * @api + * * @param array|Number $value * @param string|null $varName * @@ -6757,7 +7457,7 @@ public function assertString($value, ?string $varName = null) * * @throws SassScriptException */ - public function assertMap($value, ?string $varName = null) + public function assertMap($value, $varName = null) { $map = $this->tryMap($value); @@ -6773,6 +7473,8 @@ public function assertMap($value, ?string $varName = null) /** * Assert value is a list * + * @api + * * @param array|Number $value * * @return array @@ -6801,7 +7503,7 @@ public function assertList($value) * * @return array */ - public function getArgumentListKeywords($value): array + public function getArgumentListKeywords($value) { if ($value[0] !== Type::T_LIST || !isset($value[3]) || !\is_array($value[3])) { throw new \InvalidArgumentException('The argument is not a sass argument list.'); @@ -6813,6 +7515,8 @@ public function getArgumentListKeywords($value): array /** * Assert value is a color * + * @api + * * @param array|Number $value * @param string|null $varName * @@ -6820,7 +7524,7 @@ public function getArgumentListKeywords($value): array * * @throws SassScriptException */ - public function assertColor($value, ?string $varName = null) + public function assertColor($value, $varName = null) { if ($color = $this->coerceColor($value)) { return $color; @@ -6834,6 +7538,8 @@ public function assertColor($value, ?string $varName = null) /** * Assert value is a number * + * @api + * * @param array|Number $value * @param string|null $varName * @@ -6841,7 +7547,7 @@ public function assertColor($value, ?string $varName = null) * * @throws SassScriptException */ - public function assertNumber($value, ?string $varName = null): Number + public function assertNumber($value, $varName = null) { if (!$value instanceof Number) { $value = $this->compileValue($value); @@ -6854,6 +7560,8 @@ public function assertNumber($value, ?string $varName = null): Number /** * Assert value is a integer * + * @api + * * @param array|Number $value * @param string|null $varName * @@ -6861,7 +7569,7 @@ public function assertNumber($value, ?string $varName = null): Number * * @throws SassScriptException */ - public function assertInteger($value, ?string $varName = null): int + public function assertInteger($value, $varName = null) { $value = $this->assertNumber($value, $varName)->getDimension(); if (round($value - \intval($value), Number::PRECISION) > 0) { @@ -6878,7 +7586,7 @@ public function assertInteger($value, ?string $varName = null): int * @param array $args * @return array */ - private function extractSlashAlphaInColorFunction(array $args): array + private function extractSlashAlphaInColorFunction($args) { $last = end($args); if (\count($args) === 3 && $last[0] === Type::T_EXPRESSION && $last[1] === '/') { @@ -6897,7 +7605,7 @@ private function extractSlashAlphaInColorFunction(array $args): array * * @return array */ - private function fixColor(array $c): array + protected function fixColor($c) { foreach ([1, 2, 3] as $i) { if ($c[$i] < 0) { @@ -6919,13 +7627,15 @@ private function fixColor(array $c): array /** * Convert RGB to HSL * + * @internal + * * @param int $red * @param int $green * @param int $blue * * @return array */ - private function toHSL($red, $green, $blue): array + public function toHSL($red, $green, $blue) { $min = min($red, $green, $blue); $max = max($red, $green, $blue); @@ -6963,7 +7673,7 @@ private function toHSL($red, $green, $blue): array * * @return float */ - private function hueToRGB($m1, $m2, $h) + protected function hueToRGB($m1, $m2, $h) { if ($h < 0) { $h += 1; @@ -6989,13 +7699,15 @@ private function hueToRGB($m1, $m2, $h) /** * Convert HSL to RGB * + * @internal + * * @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 */ - private function toRGB($hue, $saturation, $lightness) + public function toRGB($hue, $saturation, $lightness) { if ($hue < 0) { $hue += 360; @@ -7021,6 +7733,8 @@ private function toRGB($hue, $saturation, $lightness) * Convert HWB to RGB * https://www.w3.org/TR/css-color-4/#hwb-to-rgb * + * @api + * * @param int|float $hue H from 0 to 360 * @param int|float $whiteness W from 0 to 100 * @param int|float $blackness B from 0 to 100 @@ -7040,9 +7754,9 @@ private function HWBtoRGB($hue, $whiteness, $blackness) $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); + for ($i = 1; $i < 4; $i++) { + $rgb[$i] *= (1.0 - $w - $b); + $rgb[$i] = round($rgb[$i] + 255 * $w + 0.0001); } return $rgb; @@ -7051,6 +7765,8 @@ private function HWBtoRGB($hue, $whiteness, $blackness) /** * Convert RGB to HWB * + * @api + * * @param int $red * @param int $green * @param int $blue @@ -7067,7 +7783,6 @@ private function RGBtoHWB($red, $green, $blue) if ((int) $d === 0) { $h = 0; } else { - if ($red == $max) { $h = 60 * ($green - $blue) / $d; } elseif ($green == $max) { @@ -7077,14 +7792,14 @@ private function RGBtoHWB($red, $green, $blue) } } - return [Type::T_HWB, fmod($h, 360), $min / 255 * 100, 100 - $max / 255 *100]; + return [Type::T_HWB, fmod($h, 360), $min / 255 * 100, 100 - $max / 255 * 100]; } // Built in functions - private static $libCall = ['function', 'args...']; - private function libCall($args) + protected static $libCall = ['function', 'args...']; + protected function libCall($args) { $functionReference = $args[0]; @@ -7096,8 +7811,8 @@ private function libCall($args) $functionReference = $this->libGetFunction([$this->assertString($functionReference, 'function')]); } - if ($functionReference === self::$null) { - return self::$null; + if ($functionReference === static::$null) { + return static::$null; } if (! in_array($functionReference[0], [Type::T_FUNCTION_REFERENCE, Type::T_FUNCTION])) { @@ -7112,18 +7827,18 @@ private function libCall($args) } - private static $libGetFunction = [ + protected static $libGetFunction = [ ['name'], ['name', 'css'] ]; - private function libGetFunction($args) + protected function libGetFunction($args) { $name = $this->compileStringContent($this->assertString(array_shift($args), 'name')); $isCss = false; if (count($args)) { $isCss = array_shift($args); - $isCss = (($isCss === self::$true) ? true : false); + $isCss = (($isCss === static::$true) ? true : false); } if ($isCss) { @@ -7133,8 +7848,8 @@ private function libGetFunction($args) return $this->getFunctionReference($name, true); } - private static $libIf = ['condition', 'if-true', 'if-false:']; - private function libIf($args) + protected static $libIf = ['condition', 'if-true', 'if-false:']; + protected function libIf($args) { list($cond, $t, $f) = $args; @@ -7145,8 +7860,8 @@ private function libIf($args) return $this->reduce($t, true); } - private static $libIndex = ['list', 'value']; - private function libIndex($args) + protected static $libIndex = ['list', 'value']; + protected function libIndex($args) { list($list, $value) = $args; @@ -7160,7 +7875,7 @@ private function libIndex($args) } if ($list[0] !== Type::T_LIST) { - return self::$null; + return static::$null; } // Numbers are represented with value objects, for which the PHP equality operator does not @@ -7176,7 +7891,7 @@ private function libIndex($args) return new Number($key, ''); } } - return self::$null; + return static::$null; } $values = []; @@ -7187,10 +7902,10 @@ private function libIndex($args) $key = array_search($this->normalizeValue($value), $values); - return false === $key ? self::$null : new Number($key + 1, ''); + return false === $key ? static::$null : new Number($key + 1, ''); } - private static $libRgb = [ + protected static $libRgb = [ ['color'], ['color', 'alpha'], ['channels'], @@ -7204,7 +7919,7 @@ private function libIndex($args) * * @return array */ - private function libRgb($args, $kwargs, $funcName = 'rgb') + protected function libRgb($args, $kwargs, $funcName = 'rgb') { switch (\count($args)) { case 1: @@ -7251,13 +7966,13 @@ private function libRgb($args, $kwargs, $funcName = 'rgb') return $color; } - private static $libRgba = [ + protected static $libRgba = [ ['color'], ['color', 'alpha'], ['channels'], ['red', 'green', 'blue'], ['red', 'green', 'blue', 'alpha'] ]; - private function libRgba($args, $kwargs) + protected function libRgba($args, $kwargs) { return $this->libRgb($args, $kwargs, 'rgba'); } @@ -7273,7 +7988,7 @@ private function libRgba($args, $kwargs) * * @phpstan-param callable(float|int, float|int|null, float|int): (float|int) $fn */ - private function alterColor(array $args, string $operation, callable $fn): array + protected function alterColor(array $args, $operation, $fn) { $color = $this->assertColor($args[0], 'color'); @@ -7286,7 +8001,13 @@ private function alterColor(array $args, string $operation, callable $fn): array $scale = $operation === 'scale'; $change = $operation === 'change'; - /** @phpstan-var callable(string, float|int, bool=, bool=): (float|int|null) $getParam */ + /** + * @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; @@ -7403,8 +8124,8 @@ private function alterColor(array $args, string $operation, callable $fn): array return $color; } - private static $libAdjustColor = ['color', 'kwargs...']; - private function libAdjustColor($args) + protected static $libAdjustColor = ['color', 'kwargs...']; + protected function libAdjustColor($args) { return $this->alterColor($args, 'adjust', function ($base, $alter, $max) { if ($alter === null) { @@ -7425,10 +8146,10 @@ private function libAdjustColor($args) }); } - private static $libChangeColor = ['color', 'kwargs...']; - private function libChangeColor($args) + protected static $libChangeColor = ['color', 'kwargs...']; + protected function libChangeColor($args) { - return $this->alterColor($args,'change', function ($base, $alter, $max) { + return $this->alterColor($args, 'change', function ($base, $alter, $max) { if ($alter === null) { return $base; } @@ -7437,8 +8158,8 @@ private function libChangeColor($args) }); } - private static $libScaleColor = ['color', 'kwargs...']; - private function libScaleColor($args) + protected static $libScaleColor = ['color', 'kwargs...']; + protected function libScaleColor($args) { return $this->alterColor($args, 'scale', function ($base, $scale, $max) { if ($scale === null) { @@ -7455,8 +8176,8 @@ private function libScaleColor($args) }); } - private static $libIeHexStr = ['color']; - private function libIeHexStr($args) + protected static $libIeHexStr = ['color']; + protected function libIeHexStr($args) { $color = $this->coerceColor($args[0]); @@ -7469,8 +8190,8 @@ private function libIeHexStr($args) return [Type::T_STRING, '', [sprintf('#%02X%02X%02X%02X', $color[4], $color[1], $color[2], $color[3])]]; } - private static $libRed = ['color']; - private function libRed($args) + protected static $libRed = ['color']; + protected function libRed($args) { $color = $this->coerceColor($args[0]); @@ -7481,8 +8202,8 @@ private function libRed($args) return new Number((int) $color[1], ''); } - private static $libGreen = ['color']; - private function libGreen($args) + protected static $libGreen = ['color']; + protected function libGreen($args) { $color = $this->coerceColor($args[0]); @@ -7493,8 +8214,8 @@ private function libGreen($args) return new Number((int) $color[2], ''); } - private static $libBlue = ['color']; - private function libBlue($args) + protected static $libBlue = ['color']; + protected function libBlue($args) { $color = $this->coerceColor($args[0]); @@ -7505,8 +8226,8 @@ private function libBlue($args) return new Number((int) $color[3], ''); } - private static $libAlpha = ['color']; - private function libAlpha($args) + protected static $libAlpha = ['color']; + protected function libAlpha($args) { if ($color = $this->coerceColor($args[0])) { return new Number(isset($color[4]) ? $color[4] : 1, ''); @@ -7516,8 +8237,8 @@ private function libAlpha($args) return null; } - private static $libOpacity = ['color']; - private function libOpacity($args) + protected static $libOpacity = ['color']; + protected function libOpacity($args) { $value = $args[0]; @@ -7529,11 +8250,11 @@ private function libOpacity($args) } // mix two colors - private static $libMix = [ + protected static $libMix = [ ['color1', 'color2', 'weight:50%'], ['color-1', 'color-2', 'weight:50%'] ]; - private function libMix($args) + protected function libMix($args) { list($first, $second, $weight) = $args; @@ -7564,7 +8285,7 @@ private function libMix($args) return $this->fixColor($new); } - private static $libHsl = [ + protected static $libHsl = [ ['channels'], ['hue', 'saturation'], ['hue', 'saturation', 'lightness'], @@ -7577,7 +8298,7 @@ private function libMix($args) * * @return array|null */ - private function libHsl($args, $kwargs, $funcName = 'hsl') + protected function libHsl($args, $kwargs, $funcName = 'hsl') { $args_to_check = $args; @@ -7658,18 +8379,18 @@ private function libHsl($args, $kwargs, $funcName = 'hsl') return $color; } - private static $libHsla = [ + protected static $libHsla = [ ['channels'], ['hue', 'saturation'], ['hue', 'saturation', 'lightness'], ['hue', 'saturation', 'lightness', 'alpha']]; - private function libHsla($args, $kwargs) + protected function libHsla($args, $kwargs) { return $this->libHsl($args, $kwargs, 'hsla'); } - private static $libHue = ['color']; - private function libHue($args) + protected static $libHue = ['color']; + protected function libHue($args) { $color = $this->assertColor($args[0], 'color'); $hsl = $this->toHSL($color[1], $color[2], $color[3]); @@ -7677,8 +8398,8 @@ private function libHue($args) return new Number($hsl[1], 'deg'); } - private static $libSaturation = ['color']; - private function libSaturation($args) + protected static $libSaturation = ['color']; + protected function libSaturation($args) { $color = $this->assertColor($args[0], 'color'); $hsl = $this->toHSL($color[1], $color[2], $color[3]); @@ -7686,8 +8407,8 @@ private function libSaturation($args) return new Number($hsl[2], '%'); } - private static $libLightness = ['color']; - private function libLightness($args) + protected static $libLightness = ['color']; + protected function libLightness($args) { $color = $this->assertColor($args[0], 'color'); $hsl = $this->toHSL($color[1], $color[2], $color[3]); @@ -7697,11 +8418,11 @@ private function libLightness($args) /* * Todo : a integrer dans le futur module color - private static $libHwb = [ + protected static $libHwb = [ ['channels'], ['hue', 'whiteness', 'blackness'], ['hue', 'whiteness', 'blackness', 'alpha'] ]; - private function libHwb($args, $kwargs, $funcName = 'hwb') + protected function libHwb($args, $kwargs, $funcName = 'hwb') { $args_to_check = $args; @@ -7802,8 +8523,8 @@ private function libHwb($args, $kwargs, $funcName = 'hwb') return $color; } - private static $libWhiteness = ['color']; - private function libWhiteness($args, $kwargs, $funcName = 'whiteness') { + 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]); @@ -7811,8 +8532,8 @@ private function libWhiteness($args, $kwargs, $funcName = 'whiteness') { return new Number($hwb[2], '%'); } - private static $libBlackness = ['color']; - private function libBlackness($args, $kwargs, $funcName = 'blackness') { + 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]); @@ -7828,7 +8549,7 @@ private function libBlackness($args, $kwargs, $funcName = 'blackness') { * * @return array */ - private function adjustHsl(array $color, int $idx, $amount): array + protected function adjustHsl($color, $idx, $amount) { $hsl = $this->toHSL($color[1], $color[2], $color[3]); $hsl[$idx] += $amount; @@ -7847,8 +8568,8 @@ private function adjustHsl(array $color, int $idx, $amount): array return $out; } - private static $libAdjustHue = ['color', 'degrees']; - private function libAdjustHue($args) + protected static $libAdjustHue = ['color', 'degrees']; + protected function libAdjustHue($args) { $color = $this->assertColor($args[0], 'color'); $degrees = $this->assertNumber($args[1], 'degrees')->getDimension(); @@ -7856,8 +8577,8 @@ private function libAdjustHue($args) return $this->adjustHsl($color, 1, $degrees); } - private static $libLighten = ['color', 'amount']; - private function libLighten($args) + protected static $libLighten = ['color', 'amount']; + protected function libLighten($args) { $color = $this->assertColor($args[0], 'color'); $amount = Util::checkRange('amount', new Range(0, 100), $args[1], '%'); @@ -7865,8 +8586,8 @@ private function libLighten($args) return $this->adjustHsl($color, 3, $amount); } - private static $libDarken = ['color', 'amount']; - private function libDarken($args) + protected static $libDarken = ['color', 'amount']; + protected function libDarken($args) { $color = $this->assertColor($args[0], 'color'); $amount = Util::checkRange('amount', new Range(0, 100), $args[1], '%'); @@ -7874,8 +8595,8 @@ private function libDarken($args) return $this->adjustHsl($color, 3, -$amount); } - private static $libSaturate = [['color', 'amount'], ['amount']]; - private function libSaturate($args) + protected static $libSaturate = [['color', 'amount'], ['amount']]; + protected function libSaturate($args) { $value = $args[0]; @@ -7891,8 +8612,8 @@ private function libSaturate($args) return $this->adjustHsl($color, 2, $amount->valueInRange(0, 100, 'amount')); } - private static $libDesaturate = ['color', 'amount']; - private function libDesaturate($args) + protected static $libDesaturate = ['color', 'amount']; + protected function libDesaturate($args) { $color = $this->assertColor($args[0], 'color'); $amount = $this->assertNumber($args[1], 'amount'); @@ -7900,8 +8621,8 @@ private function libDesaturate($args) return $this->adjustHsl($color, 2, -$amount->valueInRange(0, 100, 'amount')); } - private static $libGrayscale = ['color']; - private function libGrayscale($args) + protected static $libGrayscale = ['color']; + protected function libGrayscale($args) { $value = $args[0]; @@ -7912,14 +8633,14 @@ private function libGrayscale($args) return $this->adjustHsl($this->assertColor($value, 'color'), 2, -100); } - private static $libComplement = ['color']; - private function libComplement($args) + protected static $libComplement = ['color']; + protected function libComplement($args) { return $this->adjustHsl($this->assertColor($args[0], 'color'), 1, 180); } - private static $libInvert = ['color', 'weight:100%']; - private function libInvert($args) + protected static $libInvert = ['color', 'weight:100%']; + protected function libInvert($args) { $value = $args[0]; @@ -7943,8 +8664,8 @@ private function libInvert($args) } // increases opacity by amount - private static $libOpacify = ['color', 'amount']; - private function libOpacify($args) + protected static $libOpacify = ['color', 'amount']; + protected function libOpacify($args) { $color = $this->assertColor($args[0], 'color'); $amount = $this->assertNumber($args[1], 'amount'); @@ -7955,15 +8676,15 @@ private function libOpacify($args) return $color; } - private static $libFadeIn = ['color', 'amount']; - private function libFadeIn($args) + protected static $libFadeIn = ['color', 'amount']; + protected function libFadeIn($args) { return $this->libOpacify($args); } // decreases opacity by amount - private static $libTransparentize = ['color', 'amount']; - private function libTransparentize($args) + protected static $libTransparentize = ['color', 'amount']; + protected function libTransparentize($args) { $color = $this->assertColor($args[0], 'color'); $amount = $this->assertNumber($args[1], 'amount'); @@ -7974,14 +8695,14 @@ private function libTransparentize($args) return $color; } - private static $libFadeOut = ['color', 'amount']; - private function libFadeOut($args) + protected static $libFadeOut = ['color', 'amount']; + protected function libFadeOut($args) { return $this->libTransparentize($args); } - private static $libUnquote = ['string']; - private function libUnquote($args) + protected static $libUnquote = ['string']; + protected function libUnquote($args) { try { $str = $this->assertString($args[0], 'string'); @@ -8003,8 +8724,8 @@ private function libUnquote($args) return $str; } - private static $libQuote = ['string']; - private function libQuote($args) + protected static $libQuote = ['string']; + protected function libQuote($args) { $value = $this->assertString($args[0], 'string'); @@ -8013,8 +8734,8 @@ private function libQuote($args) return $value; } - private static $libPercentage = ['number']; - private function libPercentage($args) + protected static $libPercentage = ['number']; + protected function libPercentage($args) { $num = $this->assertNumber($args[0], 'number'); $num->assertNoUnits('number'); @@ -8022,40 +8743,40 @@ private function libPercentage($args) return new Number($num->getDimension() * 100, '%'); } - private static $libRound = ['number']; - private function libRound($args) + protected static $libRound = ['number']; + protected function libRound($args) { $num = $this->assertNumber($args[0], 'number'); return new Number(round($num->getDimension()), $num->getNumeratorUnits(), $num->getDenominatorUnits()); } - private static $libFloor = ['number']; - private function libFloor($args) + protected static $libFloor = ['number']; + protected function libFloor($args) { $num = $this->assertNumber($args[0], 'number'); return new Number(floor($num->getDimension()), $num->getNumeratorUnits(), $num->getDenominatorUnits()); } - private static $libCeil = ['number']; - private function libCeil($args) + protected static $libCeil = ['number']; + protected function libCeil($args) { $num = $this->assertNumber($args[0], 'number'); return new Number(ceil($num->getDimension()), $num->getNumeratorUnits(), $num->getDenominatorUnits()); } - private static $libAbs = ['number']; - private function libAbs($args) + protected static $libAbs = ['number']; + protected function libAbs($args) { $num = $this->assertNumber($args[0], 'number'); return new Number(abs($num->getDimension()), $num->getNumeratorUnits(), $num->getDenominatorUnits()); } - private static $libMin = ['numbers...']; - private function libMin($args) + protected static $libMin = ['numbers...']; + protected function libMin($args) { /** * @var Number|null @@ -8077,8 +8798,8 @@ private function libMin($args) throw $this->error('At least one argument must be passed.'); } - private static $libMax = ['numbers...']; - private function libMax($args) + protected static $libMax = ['numbers...']; + protected function libMax($args) { /** * @var Number|null @@ -8100,16 +8821,16 @@ private function libMax($args) throw $this->error('At least one argument must be passed.'); } - private static $libLength = ['list']; - private function libLength($args) + protected static $libLength = ['list']; + protected function libLength($args) { $list = $this->coerceList($args[0], ',', true); return new Number(\count($list[2]), ''); } - private static $libListSeparator = ['list']; - private function libListSeparator($args) + protected static $libListSeparator = ['list']; + protected function libListSeparator($args) { if (! \in_array($args[0][0], [Type::T_LIST, Type::T_MAP])) { return [Type::T_KEYWORD, 'space']; @@ -8132,8 +8853,8 @@ private function libListSeparator($args) return [Type::T_KEYWORD, 'space']; } - private static $libNth = ['list', 'n']; - private function libNth($args) + protected static $libNth = ['list', 'n']; + protected function libNth($args) { $list = $this->coerceList($args[0], ',', false); $n = $this->assertInteger($args[1]); @@ -8144,11 +8865,11 @@ private function libNth($args) $n += \count($list[2]); } - return isset($list[2][$n]) ? $list[2][$n] : self::$defaultValue; + return isset($list[2][$n]) ? $list[2][$n] : static::$defaultValue; } - private static $libSetNth = ['list', 'n', 'value']; - private function libSetNth($args) + protected static $libSetNth = ['list', 'n', 'value']; + protected function libSetNth($args) { $list = $this->coerceList($args[0]); $n = $this->assertInteger($args[1]); @@ -8168,8 +8889,8 @@ private function libSetNth($args) return $list; } - private static $libMapGet = ['map', 'key', 'keys...']; - private function libMapGet($args) + protected static $libMapGet = ['map', 'key', 'keys...']; + protected function libMapGet($args) { $map = $this->assertMap($args[0], 'map'); if (!isset($args[2])) { @@ -8177,17 +8898,17 @@ private function libMapGet($args) $args[2] = self::$emptyArgumentList; } $keys = array_merge([$args[1]], $args[2][2]); - $value = self::$null; + $value = static::$null; foreach ($keys as $key) { if (!\is_array($map) || $map[0] !== Type::T_MAP) { - return self::$null; + return static::$null; } $map = $this->mapGet($map, $key); if ($map === null) { - return self::$null; + return static::$null; } $value = $map; @@ -8236,8 +8957,8 @@ private function mapGetEntryIndex(array $map, $key) return null; } - private static $libMapKeys = ['map']; - private function libMapKeys($args) + protected static $libMapKeys = ['map']; + protected function libMapKeys($args) { $map = $this->assertMap($args[0], 'map'); $keys = $map[1]; @@ -8245,8 +8966,8 @@ private function libMapKeys($args) return [Type::T_LIST, ',', $keys]; } - private static $libMapValues = ['map']; - private function libMapValues($args) + protected static $libMapValues = ['map']; + protected function libMapValues($args) { $map = $this->assertMap($args[0], 'map'); $values = $map[2]; @@ -8254,11 +8975,11 @@ private function libMapValues($args) return [Type::T_LIST, ',', $values]; } - private static $libMapRemove = [ + protected static $libMapRemove = [ ['map'], ['map', 'key', 'keys...'], ]; - private function libMapRemove($args) + protected function libMapRemove($args) { $map = $this->assertMap($args[0], 'map'); @@ -8283,8 +9004,8 @@ private function libMapRemove($args) return $map; } - private static $libMapHasKey = ['map', 'key', 'keys...']; - private function libMapHasKey($args) + protected static $libMapHasKey = ['map', 'key', 'keys...']; + protected function libMapHasKey($args) { $map = $this->assertMap($args[0], 'map'); if (!isset($args[2])) { @@ -8325,12 +9046,12 @@ private function mapHasKey(array $map, $keyValue) return false; } - private static $libMapMerge = [ + protected static $libMapMerge = [ ['map1', 'map2'], ['map-1', 'map-2'], ['map1', 'args...'] ]; - private function libMapMerge($args) + protected function libMapMerge($args) { $map1 = $this->assertMap($args[0], 'map1'); $map2 = $args[1]; @@ -8454,8 +9175,8 @@ private function mergeMaps(array $map1, array $map2) return $map1; } - private static $libKeywords = ['args']; - private function libKeywords($args) + protected static $libKeywords = ['args']; + protected function libKeywords($args) { $value = $args[0]; @@ -8476,8 +9197,8 @@ private function libKeywords($args) return [Type::T_MAP, $keys, $values]; } - private static $libIsBracketed = ['list']; - private function libIsBracketed($args) + protected static $libIsBracketed = ['list']; + protected function libIsBracketed($args) { $list = $args[0]; $this->coerceList($list, ' '); @@ -8498,7 +9219,7 @@ private function libIsBracketed($args) * * @deprecated */ - private function listSeparatorForJoin(array $list1, $sep): string + protected function listSeparatorForJoin($list1, $sep) { @trigger_error(sprintf('The "%s" method is deprecated.', __METHOD__), E_USER_DEPRECATED); @@ -8518,8 +9239,8 @@ private function listSeparatorForJoin(array $list1, $sep): string } } - private static $libJoin = ['list1', 'list2', 'separator:auto', 'bracketed:auto']; - private function libJoin($args) + protected static $libJoin = ['list1', 'list2', 'separator:auto', 'bracketed:auto']; + protected function libJoin($args) { list($list1, $list2, $sep, $bracketed) = $args; @@ -8553,13 +9274,13 @@ private function libJoin($args) throw SassScriptException::forArgument('Must be "space", "comma", "slash", or "auto".', 'separator'); } - if ($bracketed === self::$true) { + if ($bracketed === static::$true) { $bracketed = true; - } elseif ($bracketed === self::$false) { + } elseif ($bracketed === static::$false) { $bracketed = false; } elseif ($bracketed === [Type::T_KEYWORD, 'auto']) { $bracketed = 'auto'; - } elseif ($bracketed === self::$null) { + } elseif ($bracketed === static::$null) { $bracketed = false; } else { $bracketed = $this->compileValue($bracketed); @@ -8587,8 +9308,8 @@ private function libJoin($args) return $res; } - private static $libAppend = ['list', 'val', 'separator:auto']; - private function libAppend($args) + protected static $libAppend = ['list', 'val', 'separator:auto']; + protected function libAppend($args) { list($list1, $value, $sep) = $args; @@ -8624,8 +9345,8 @@ private function libAppend($args) return $res; } - private static $libZip = ['lists...']; - private function libZip($args) + protected static $libZip = ['lists...']; + protected function libZip($args) { $argLists = []; foreach ($args[0][2] as $arg) { @@ -8659,8 +9380,8 @@ private function libZip($args) return $result; } - private static $libTypeOf = ['value']; - private function libTypeOf($args) + protected static $libTypeOf = ['value']; + protected function libTypeOf($args) { $value = $args[0]; @@ -8676,7 +9397,7 @@ private function getTypeOf($value) { switch ($value[0]) { case Type::T_KEYWORD: - if ($value === self::$true || $value === self::$false) { + if ($value === static::$true || $value === static::$false) { return 'bool'; } @@ -8702,27 +9423,27 @@ private function getTypeOf($value) } } - private static $libUnit = ['number']; - private function libUnit($args) + protected static $libUnit = ['number']; + protected function libUnit($args) { $num = $this->assertNumber($args[0], 'number'); return [Type::T_STRING, '"', [$num->unitStr()]]; } - private static $libUnitless = ['number']; - private function libUnitless($args) + protected static $libUnitless = ['number']; + protected function libUnitless($args) { $value = $this->assertNumber($args[0], 'number'); return $this->toBool($value->unitless()); } - private static $libComparable = [ + protected static $libComparable = [ ['number1', 'number2'], ['number-1', 'number-2'] ]; - private function libComparable($args) + protected function libComparable($args) { list($number1, $number2) = $args; @@ -8736,8 +9457,8 @@ private function libComparable($args) return $this->toBool($number1->isComparableTo($number2)); } - private static $libStrIndex = ['string', 'substring']; - private function libStrIndex($args) + protected static $libStrIndex = ['string', 'substring']; + protected function libStrIndex($args) { $string = $this->assertString($args[0], 'string'); $stringContent = $this->compileStringContent($string); @@ -8751,11 +9472,11 @@ private function libStrIndex($args) $result = Util::mbStrpos($stringContent, $substringContent); } - return $result === false ? self::$null : new Number($result + 1, ''); + return $result === false ? static::$null : new Number($result + 1, ''); } - private static $libStrInsert = ['string', 'insert', 'index']; - private function libStrInsert($args) + protected static $libStrInsert = ['string', 'insert', 'index']; + protected function libStrInsert($args) { $string = $this->assertString($args[0], 'string'); $stringContent = $this->compileStringContent($string); @@ -8780,8 +9501,8 @@ private function libStrInsert($args) return $string; } - private static $libStrLength = ['string']; - private function libStrLength($args) + protected static $libStrLength = ['string']; + protected function libStrLength($args) { $string = $this->assertString($args[0], 'string'); $stringContent = $this->compileStringContent($string); @@ -8789,8 +9510,8 @@ private function libStrLength($args) return new Number(Util::mbStrlen($stringContent), ''); } - private static $libStrSlice = ['string', 'start-at', 'end-at:-1']; - private function libStrSlice($args) + protected static $libStrSlice = ['string', 'start-at', 'end-at:-1']; + protected function libStrSlice($args) { $string = $this->assertString($args[0], 'string'); $stringContent = $this->compileStringContent($string); @@ -8827,8 +9548,8 @@ private function libStrSlice($args) return $string; } - private static $libToLowerCase = ['string']; - private function libToLowerCase($args) + protected static $libToLowerCase = ['string']; + protected function libToLowerCase($args) { $string = $this->assertString($args[0], 'string'); $stringContent = $this->compileStringContent($string); @@ -8838,8 +9559,8 @@ private function libToLowerCase($args) return $string; } - private static $libToUpperCase = ['string']; - private function libToUpperCase($args) + protected static $libToUpperCase = ['string']; + protected function libToUpperCase($args) { $string = $this->assertString($args[0], 'string'); $stringContent = $this->compileStringContent($string); @@ -8857,7 +9578,7 @@ private function libToUpperCase($args) * @param callable $filter * @return string */ - private function stringTransformAsciiOnly(string $stringContent, callable $filter): string + protected function stringTransformAsciiOnly($stringContent, $filter) { $mblength = Util::mbStrlen($stringContent); if ($mblength === strlen($stringContent)) { @@ -8876,8 +9597,8 @@ private function stringTransformAsciiOnly(string $stringContent, callable $filte return $filteredString; } - private static $libFeatureExists = ['feature']; - private function libFeatureExists($args) + protected static $libFeatureExists = ['feature']; + protected function libFeatureExists($args) { $string = $this->assertString($args[0], 'feature'); $name = $this->compileStringContent($string); @@ -8887,14 +9608,14 @@ private function libFeatureExists($args) ); } - private static $libFunctionExists = ['name']; - private function libFunctionExists($args) + protected static $libFunctionExists = ['name']; + protected function libFunctionExists($args) { $string = $this->assertString($args[0], 'name'); $name = $this->compileStringContent($string); // user defined functions - if ($this->has(self::$namespaces['function'] . $name)) { + if ($this->has(static::$namespaces['function'] . $name)) { return self::$true; } @@ -8907,11 +9628,11 @@ private function libFunctionExists($args) // built-in functions $f = $this->getBuiltinFunction($name); - return $this->toBool($f !== null && \is_callable($f)); + return $this->toBool(\is_callable($f)); } - private static $libGlobalVariableExists = ['name']; - private function libGlobalVariableExists($args) + protected static $libGlobalVariableExists = ['name']; + protected function libGlobalVariableExists($args) { $string = $this->assertString($args[0], 'name'); $name = $this->compileStringContent($string); @@ -8919,17 +9640,17 @@ private function libGlobalVariableExists($args) return $this->toBool($this->has($name, $this->rootEnv)); } - private static $libMixinExists = ['name']; - private function libMixinExists($args) + protected static $libMixinExists = ['name']; + protected function libMixinExists($args) { $string = $this->assertString($args[0], 'name'); $name = $this->compileStringContent($string); - return $this->toBool($this->has(self::$namespaces['mixin'] . $name)); + return $this->toBool($this->has(static::$namespaces['mixin'] . $name)); } - private static $libVariableExists = ['name']; - private function libVariableExists($args) + protected static $libVariableExists = ['name']; + protected function libVariableExists($args) { $string = $this->assertString($args[0], 'name'); $name = $this->compileStringContent($string); @@ -8937,10 +9658,25 @@ private function libVariableExists($args) return $this->toBool($this->has($name)); } - private static $libRandom = ['limit:null']; - private function libRandom($args) + protected static $libCounter = ['args...']; + /** + * Workaround IE7's content counter bug. + * + * @param array $args + * + * @return array + */ + protected function libCounter($args) { - if (isset($args[0]) && $args[0] !== self::$null) { + $list = array_map([$this, 'compileValue'], $args[0][2]); + + return [Type::T_STRING, '', ['counter(' . implode(',', $list) . ')']]; + } + + protected static $libRandom = ['limit:null']; + protected function libRandom($args) + { + if (isset($args[0]) && $args[0] !== static::$null) { $limit = $this->assertNumber($args[0], 'limit'); if ($limit->hasUnits()) { @@ -8972,8 +9708,8 @@ private function libRandom($args) return new Number(mt_rand(0, $max - 1) / $max, ''); } - private static $libUniqueId = []; - private function libUniqueId() + protected static $libUniqueId = []; + protected function libUniqueId() { static $id; @@ -8994,9 +9730,9 @@ private function libUniqueId() * * @return array */ - private function inspectFormatValue($value, $force_enclosing_display = false) + protected function inspectFormatValue($value, $force_enclosing_display = false) { - if ($value === self::$null) { + if ($value === static::$null) { $value = [Type::T_KEYWORD, 'null']; } @@ -9007,7 +9743,7 @@ private function inspectFormatValue($value, $force_enclosing_display = false) } if ($value[0] === Type::T_LIST) { - if (end($value[2]) === self::$null) { + if (end($value[2]) === static::$null) { array_pop($value[2]); $value[2][] = [Type::T_STRING, '', ['']]; $force_enclosing_display = true; @@ -9035,8 +9771,8 @@ private function inspectFormatValue($value, $force_enclosing_display = false) return [Type::T_STRING, '', $stringValue]; } - private static $libInspect = ['value']; - private function libInspect($args) + protected static $libInspect = ['value']; + protected function libInspect($args) { $value = $args[0]; @@ -9052,7 +9788,7 @@ private function libInspect($args) * * @return array */ - private function getSelectorArg($arg, ?string $varname = null, bool $allowParent = false) + protected function getSelectorArg($arg, $varname = null, $allowParent = false) { static $parser = null; @@ -9080,7 +9816,7 @@ private function getSelectorArg($arg, ?string $varname = null, bool $allowParent if (! $allowParent) { foreach ($gluedSelector as $selector) { foreach ($selector as $s) { - if (in_array(self::$selfSelector, $s)) { + if (in_array(static::$selfSelector, $s)) { throw SassScriptException::forArgument("Parent selectors aren't allowed here.", $varname); } } @@ -9099,7 +9835,7 @@ private function getSelectorArg($arg, ?string $varname = null, bool $allowParent * @param int $maxDepth * @return bool */ - private function checkSelectorArgType($arg, int $maxDepth = 2): bool + protected function checkSelectorArgType($arg, $maxDepth = 2) { if ($arg[0] === Type::T_LIST && $maxDepth > 0) { foreach ($arg[2] as $elt) { @@ -9122,15 +9858,15 @@ private function checkSelectorArgType($arg, int $maxDepth = 2): bool * * @return array */ - private function formatOutputSelector(array $selectors): array + protected function formatOutputSelector($selectors) { $selectors = $this->collapseSelectorsAsList($selectors); return $selectors; } - private static $libIsSuperselector = ['super', 'sub']; - private function libIsSuperselector($args) + protected static $libIsSuperselector = ['super', 'sub']; + protected function libIsSuperselector($args) { list($super, $sub) = $args; @@ -9148,7 +9884,7 @@ private function libIsSuperselector($args) * * @return bool */ - private function isSuperSelector(array $super, array $sub): bool + protected function isSuperSelector($super, $sub) { // one and only one selector for each arg if (! $super) { @@ -9229,7 +9965,7 @@ function ($value, $key) use (&$compound) { * * @return bool */ - private function isSuperPart(array $superParts, array $subParts): bool + protected function isSuperPart($superParts, $subParts) { $i = 0; @@ -9248,8 +9984,8 @@ private function isSuperPart(array $superParts, array $subParts): bool return true; } - private static $libSelectorAppend = ['selector...']; - private function libSelectorAppend($args) + protected static $libSelectorAppend = ['selector...']; + protected function libSelectorAppend($args) { // get the selector... list $args = reset($args); @@ -9276,7 +10012,7 @@ private function libSelectorAppend($args) * * @throws \ScssPhp\ScssPhp\Exception\CompilerException */ - private function selectorAppend(array $selectors): array + protected function selectorAppend($selectors) { $lastSelectors = array_pop($selectors); @@ -9315,11 +10051,11 @@ private function selectorAppend(array $selectors): array return $lastSelectors; } - private static $libSelectorExtend = [ + protected static $libSelectorExtend = [ ['selector', 'extendee', 'extender'], ['selectors', 'extendee', 'extender'] ]; - private function libSelectorExtend($args) + protected function libSelectorExtend($args) { list($selectors, $extendee, $extender) = $args; @@ -9336,11 +10072,11 @@ private function libSelectorExtend($args) return $this->formatOutputSelector($extended); } - private static $libSelectorReplace = [ + protected static $libSelectorReplace = [ ['selector', 'original', 'replacement'], ['selectors', 'original', 'replacement'] ]; - private function libSelectorReplace($args) + protected function libSelectorReplace($args) { list($selectors, $original, $replacement) = $args; @@ -9368,7 +10104,7 @@ private function libSelectorReplace($args) * * @return array */ - private function extendOrReplaceSelectors(array $selectors, array $extendee, array $extender, bool $replace = false): array + protected function extendOrReplaceSelectors($selectors, $extendee, $extender, $replace = false) { $saveExtends = $this->extends; $saveExtendsMap = $this->extendsMap; @@ -9408,8 +10144,8 @@ private function extendOrReplaceSelectors(array $selectors, array $extendee, arr return $extended; } - private static $libSelectorNest = ['selector...']; - private function libSelectorNest($args) + protected static $libSelectorNest = ['selector...']; + protected function libSelectorNest($args) { // get the selector... list $args = reset($args); @@ -9442,11 +10178,11 @@ private function libSelectorNest($args) return $this->formatOutputSelector($outputSelectors); } - private static $libSelectorParse = [ + protected static $libSelectorParse = [ ['selector'], ['selectors'] ]; - private function libSelectorParse($args) + protected function libSelectorParse($args) { $selectors = reset($args); $selectors = $this->getSelectorArg($selectors, 'selector'); @@ -9454,8 +10190,8 @@ private function libSelectorParse($args) return $this->formatOutputSelector($selectors); } - private static $libSelectorUnify = ['selectors1', 'selectors2']; - private function libSelectorUnify($args) + protected static $libSelectorUnify = ['selectors1', 'selectors2']; + protected function libSelectorUnify($args) { list($selectors1, $selectors2) = $args; @@ -9485,7 +10221,7 @@ private function libSelectorUnify($args) * * @return array */ - private function unifyCompoundSelectors(array $compound1, array $compound2): array + protected function unifyCompoundSelectors($compound1, $compound2) { if (! \count($compound1)) { return $compound2; @@ -9578,7 +10314,7 @@ private function unifyCompoundSelectors(array $compound1, array $compound2): arr * * @return array */ - private function prependSelectors(array $selectors, array $parts): array + protected function prependSelectors($selectors, $parts) { $new = []; @@ -9601,7 +10337,7 @@ private function prependSelectors(array $selectors, array $parts): array * * @return array|false */ - private function matchPartInCompound(array $part, array $compound) + protected function matchPartInCompound($part, $compound) { $partTag = $this->findTagName($part); $before = $compound; @@ -9647,7 +10383,7 @@ private function matchPartInCompound(array $part, array $compound) * * @return array */ - private function mergeParts(array $parts1, array $parts2): array + protected function mergeParts($parts1, $parts2) { $tag1 = $this->findTagName($parts1); $tag2 = $this->findTagName($parts2); @@ -9696,7 +10432,7 @@ private function mergeParts(array $parts1, array $parts2): array * * @return array|false */ - private function checkCompatibleTags(string $tag1, string $tag2) + protected function checkCompatibleTags($tag1, $tag2) { $tags = [$tag1, $tag2]; $tags = array_unique($tags); @@ -9721,7 +10457,7 @@ private function checkCompatibleTags(string $tag1, string $tag2) * * @return string */ - private function findTagName(array $parts): string + protected function findTagName($parts) { foreach ($parts as $part) { if (! preg_match('/^[\[.:#%_-]/', $part)) { @@ -9732,8 +10468,8 @@ private function findTagName(array $parts): string return ''; } - private static $libSimpleSelectors = ['selector']; - private function libSimpleSelectors($args) + protected static $libSimpleSelectors = ['selector']; + protected function libSimpleSelectors($args) { $selector = reset($args); $selector = $this->getSelectorArg($selector, 'selector'); @@ -9752,4 +10488,27 @@ private function libSimpleSelectors($args) return [Type::T_LIST, ',', $listParts]; } + + protected static $libScssphpGlob = ['pattern']; + protected function libScssphpGlob($args) + { + @trigger_error(sprintf('The "scssphp-glob" function is deprecated an will be removed in ScssPhp 2.0. Register your own alternative through "%s::registerFunction', __CLASS__), E_USER_DEPRECATED); + + $this->logger->warn('The "scssphp-glob" function is deprecated an will be removed in ScssPhp 2.0.', true); + + $string = $this->assertString($args[0], 'pattern'); + $pattern = $this->compileStringContent($string); + $matches = glob($pattern); + $listParts = []; + + foreach ($matches as $match) { + if (! is_file($match)) { + continue; + } + + $listParts[] = [Type::T_STRING, '"', [$match]]; + } + + return [Type::T_LIST, ',', $listParts]; + } } diff --git a/scssphp/src/Compiler/CachedResult.php b/scssphp/src/Compiler/CachedResult.php index 5ee844d..a662919 100644 --- a/scssphp/src/Compiler/CachedResult.php +++ b/scssphp/src/Compiler/CachedResult.php @@ -17,7 +17,7 @@ /** * @internal */ -final class CachedResult +class CachedResult { /** * @var CompilationResult @@ -52,7 +52,7 @@ public function __construct(CompilationResult $result, array $parsedFiles, array /** * @return CompilationResult */ - public function getResult(): CompilationResult + public function getResult() { return $this->result; } @@ -60,7 +60,7 @@ public function getResult(): CompilationResult /** * @return array */ - public function getParsedFiles(): array + public function getParsedFiles() { return $this->parsedFiles; } @@ -70,7 +70,7 @@ public function getParsedFiles(): array * * @phpstan-return list */ - public function getResolvedImports(): array + public function getResolvedImports() { return $this->resolvedImports; } diff --git a/scssphp/src/Compiler/Environment.php b/scssphp/src/Compiler/Environment.php index 47d4d0c..b205a07 100644 --- a/scssphp/src/Compiler/Environment.php +++ b/scssphp/src/Compiler/Environment.php @@ -19,7 +19,7 @@ * * @internal */ -final class Environment +class Environment { /** * @var \ScssPhp\ScssPhp\Block|null diff --git a/scssphp/src/Exception/CompilerException.php b/scssphp/src/Exception/CompilerException.php index c1c51ab..0b00cf5 100644 --- a/scssphp/src/Exception/CompilerException.php +++ b/scssphp/src/Exception/CompilerException.php @@ -19,6 +19,6 @@ * * @internal */ -final class CompilerException extends \Exception implements SassException +class CompilerException extends \Exception implements SassException { } diff --git a/scssphp/src/Exception/ParserException.php b/scssphp/src/Exception/ParserException.php index 038e97f..f072669 100644 --- a/scssphp/src/Exception/ParserException.php +++ b/scssphp/src/Exception/ParserException.php @@ -19,7 +19,7 @@ * * @internal */ -final class ParserException extends \Exception implements SassException +class ParserException extends \Exception implements SassException { /** * @var array|null @@ -30,9 +30,12 @@ final class ParserException extends \Exception implements SassException /** * Get source position * + * @api + * + * @return array|null * @phpstan-return array{string, int, int}|null */ - public function getSourcePosition(): ?array + public function getSourcePosition() { return $this->sourcePosition; } @@ -40,13 +43,15 @@ public function getSourcePosition(): ?array /** * Set source position * + * @api + * * @param array $sourcePosition * * @return void * * @phpstan-param array{string, int, int} $sourcePosition */ - public function setSourcePosition(array $sourcePosition): void + public function setSourcePosition($sourcePosition) { $this->sourcePosition = $sourcePosition; } diff --git a/scssphp/src/Exception/RangeException.php b/scssphp/src/Exception/RangeException.php index 1f3f3ed..4be4dee 100644 --- a/scssphp/src/Exception/RangeException.php +++ b/scssphp/src/Exception/RangeException.php @@ -19,6 +19,6 @@ * * @internal */ -final class RangeException extends \Exception implements SassException +class RangeException extends \Exception implements SassException { } diff --git a/scssphp/src/Exception/SassException.php b/scssphp/src/Exception/SassException.php index c9764e3..9f62b3c 100644 --- a/scssphp/src/Exception/SassException.php +++ b/scssphp/src/Exception/SassException.php @@ -1,17 +1,7 @@ + * + * @deprecated The Scssphp server should define its own exception instead. + */ +class ServerException extends \Exception implements SassException +{ +} diff --git a/scssphp/src/Formatter.php b/scssphp/src/Formatter.php index 70f47d4..c477e6f 100644 --- a/scssphp/src/Formatter.php +++ b/scssphp/src/Formatter.php @@ -65,32 +65,34 @@ abstract class Formatter public $keepSemicolons; /** - * @var OutputBlock + * @var \ScssPhp\ScssPhp\Formatter\OutputBlock */ - private $currentBlock; + protected $currentBlock; /** * @var int */ - private $currentLine; + protected $currentLine; /** * @var int */ - private $currentColumn; + protected $currentColumn; /** - * @var SourceMapGenerator|null + * @var \ScssPhp\ScssPhp\SourceMap\SourceMapGenerator|null */ - private $sourceMapGenerator; + protected $sourceMapGenerator; /** * @var string */ - private $strippedSemicolon; + protected $strippedSemicolon; /** * Initialize formatter + * + * @api */ abstract public function __construct(); @@ -99,7 +101,7 @@ abstract public function __construct(); * * @return string */ - protected function indentStr(): string + protected function indentStr() { return ''; } @@ -107,12 +109,14 @@ protected function indentStr(): string /** * Return property assignment * + * @api + * * @param string $name - * @param string $value + * @param mixed $value * * @return string */ - public function property(string $name, string $value): string + public function property($name, $value) { return rtrim($name) . $this->assignSeparator . $value . ';'; } @@ -121,12 +125,14 @@ public function property(string $name, string $value): string * Return custom property assignment * differs in that you have to keep spaces in the value as is * + * @api + * * @param string $name - * @param string $value + * @param mixed $value * * @return string */ - public function customProperty(string $name, string $value): string + public function customProperty($name, $value) { return rtrim($name) . trim($this->assignSeparator) . $value . ';'; } @@ -134,11 +140,11 @@ public function customProperty(string $name, string $value): string /** * Output lines inside a block * - * @param OutputBlock $block + * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $block * * @return void */ - protected function blockLines(OutputBlock $block): void + protected function blockLines(OutputBlock $block) { $inner = $this->indentStr(); $glue = $this->break . $inner; @@ -153,11 +159,11 @@ protected function blockLines(OutputBlock $block): void /** * Output block selectors * - * @param OutputBlock $block + * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $block * * @return void */ - protected function blockSelectors(OutputBlock $block): void + protected function blockSelectors(OutputBlock $block) { assert(! empty($block->selectors)); @@ -171,11 +177,11 @@ protected function blockSelectors(OutputBlock $block): void /** * Output block children * - * @param OutputBlock $block + * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $block * * @return void */ - private function blockChildren(OutputBlock $block) + protected function blockChildren(OutputBlock $block) { foreach ($block->children as $child) { $this->block($child); @@ -185,11 +191,11 @@ private function blockChildren(OutputBlock $block) /** * Output non-empty block * - * @param OutputBlock $block + * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $block * * @return void */ - private function block(OutputBlock $block) + protected function block(OutputBlock $block) { if (empty($block->lines) && empty($block->children)) { return; @@ -231,11 +237,11 @@ private function block(OutputBlock $block) /** * Test and clean safely empty children * - * @param OutputBlock $block + * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $block * * @return bool */ - private function testEmptyChildren(OutputBlock $block): bool + protected function testEmptyChildren($block) { $isEmpty = empty($block->lines); @@ -259,12 +265,14 @@ private function testEmptyChildren(OutputBlock $block): bool /** * Entry point to formatting a block * - * @param OutputBlock $block An abstract syntax tree - * @param SourceMapGenerator|null $sourceMapGenerator Optional source map generator + * @api + * + * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $block An abstract syntax tree + * @param \ScssPhp\ScssPhp\SourceMap\SourceMapGenerator|null $sourceMapGenerator Optional source map generator * * @return string */ - public function format(OutputBlock $block, SourceMapGenerator $sourceMapGenerator = null): string + public function format(OutputBlock $block, ?SourceMapGenerator $sourceMapGenerator = null) { $this->sourceMapGenerator = null; @@ -301,7 +309,7 @@ public function format(OutputBlock $block, SourceMapGenerator $sourceMapGenerato * * @return void */ - protected function write(string $str): void + protected function write($str) { if (! empty($this->strippedSemicolon)) { echo $this->strippedSemicolon; diff --git a/scssphp/src/Formatter/Compact.php b/scssphp/src/Formatter/Compact.php new file mode 100644 index 0000000..22f2268 --- /dev/null +++ b/scssphp/src/Formatter/Compact.php @@ -0,0 +1,52 @@ + + * + * @deprecated since 1.4.0. Use the Compressed formatter instead. + * + * @internal + */ +class Compact extends Formatter +{ + /** + * {@inheritdoc} + */ + public function __construct() + { + @trigger_error('The Compact formatter is deprecated since 1.4.0. Use the Compressed formatter instead.', E_USER_DEPRECATED); + + $this->indentLevel = 0; + $this->indentChar = ''; + $this->break = ''; + $this->open = ' {'; + $this->close = "}\n\n"; + $this->tagSeparator = ','; + $this->assignSeparator = ':'; + $this->keepSemicolons = true; + } + + /** + * {@inheritdoc} + */ + public function indentStr() + { + return ' '; + } +} diff --git a/scssphp/src/Formatter/Compressed.php b/scssphp/src/Formatter/Compressed.php index a205999..58ebe3f 100644 --- a/scssphp/src/Formatter/Compressed.php +++ b/scssphp/src/Formatter/Compressed.php @@ -21,7 +21,7 @@ * * @internal */ -final class Compressed extends Formatter +class Compressed extends Formatter { /** * {@inheritdoc} @@ -41,7 +41,7 @@ public function __construct() /** * {@inheritdoc} */ - protected function blockLines(OutputBlock $block): void + public function blockLines(OutputBlock $block) { $inner = $this->indentStr(); @@ -65,7 +65,7 @@ protected function blockLines(OutputBlock $block): void * * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $block */ - protected function blockSelectors(OutputBlock $block): void + protected function blockSelectors(OutputBlock $block) { assert(! empty($block->selectors)); diff --git a/scssphp/src/Formatter/Crunched.php b/scssphp/src/Formatter/Crunched.php new file mode 100644 index 0000000..2bc1e92 --- /dev/null +++ b/scssphp/src/Formatter/Crunched.php @@ -0,0 +1,87 @@ + + * + * @deprecated since 1.4.0. Use the Compressed formatter instead. + * + * @internal + */ +class Crunched extends Formatter +{ + /** + * {@inheritdoc} + */ + public function __construct() + { + @trigger_error('The Crunched formatter is deprecated since 1.4.0. Use the Compressed formatter instead.', E_USER_DEPRECATED); + + $this->indentLevel = 0; + $this->indentChar = ' '; + $this->break = ''; + $this->open = '{'; + $this->close = '}'; + $this->tagSeparator = ','; + $this->assignSeparator = ':'; + $this->keepSemicolons = false; + } + + /** + * {@inheritdoc} + */ + public function blockLines(OutputBlock $block) + { + $inner = $this->indentStr(); + + $glue = $this->break . $inner; + + foreach ($block->lines as $index => $line) { + if (substr($line, 0, 2) === '/*') { + unset($block->lines[$index]); + } + } + + $this->write($inner . implode($glue, $block->lines)); + + if (! empty($block->children)) { + $this->write($this->break); + } + } + + /** + * Output block selectors + * + * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $block + */ + protected function blockSelectors(OutputBlock $block) + { + assert(! empty($block->selectors)); + + $inner = $this->indentStr(); + + $this->write( + $inner + . implode( + $this->tagSeparator, + str_replace([' > ', ' + ', ' ~ '], ['>', '+', '~'], $block->selectors) + ) + . $this->open . $this->break + ); + } +} diff --git a/scssphp/src/Formatter/Debug.php b/scssphp/src/Formatter/Debug.php new file mode 100644 index 0000000..b3f4422 --- /dev/null +++ b/scssphp/src/Formatter/Debug.php @@ -0,0 +1,127 @@ + + * + * @deprecated since 1.4.0. + * + * @internal + */ +class Debug extends Formatter +{ + /** + * {@inheritdoc} + */ + public function __construct() + { + @trigger_error('The Debug formatter is deprecated since 1.4.0.', E_USER_DEPRECATED); + + $this->indentLevel = 0; + $this->indentChar = ''; + $this->break = "\n"; + $this->open = ' {'; + $this->close = ' }'; + $this->tagSeparator = ', '; + $this->assignSeparator = ': '; + $this->keepSemicolons = true; + } + + /** + * {@inheritdoc} + */ + protected function indentStr() + { + return str_repeat(' ', $this->indentLevel); + } + + /** + * {@inheritdoc} + */ + protected function blockLines(OutputBlock $block) + { + $indent = $this->indentStr(); + + if (empty($block->lines)) { + $this->write("{$indent}block->lines: []\n"); + + return; + } + + foreach ($block->lines as $index => $line) { + $this->write("{$indent}block->lines[{$index}]: $line\n"); + } + } + + /** + * {@inheritdoc} + */ + protected function blockSelectors(OutputBlock $block) + { + $indent = $this->indentStr(); + + if (empty($block->selectors)) { + $this->write("{$indent}block->selectors: []\n"); + + return; + } + + foreach ($block->selectors as $index => $selector) { + $this->write("{$indent}block->selectors[{$index}]: $selector\n"); + } + } + + /** + * {@inheritdoc} + */ + protected function blockChildren(OutputBlock $block) + { + $indent = $this->indentStr(); + + if (empty($block->children)) { + $this->write("{$indent}block->children: []\n"); + + return; + } + + $this->indentLevel++; + + foreach ($block->children as $i => $child) { + $this->block($child); + } + + $this->indentLevel--; + } + + /** + * {@inheritdoc} + */ + protected function block(OutputBlock $block) + { + $indent = $this->indentStr(); + + $this->write("{$indent}block->type: {$block->type}\n" . + "{$indent}block->depth: {$block->depth}\n"); + + $this->currentBlock = $block; + + $this->blockSelectors($block); + $this->blockLines($block); + $this->blockChildren($block); + } +} diff --git a/scssphp/src/Formatter/Expanded.php b/scssphp/src/Formatter/Expanded.php index 4f73560..6eb4a0c 100644 --- a/scssphp/src/Formatter/Expanded.php +++ b/scssphp/src/Formatter/Expanded.php @@ -21,7 +21,7 @@ * * @internal */ -final class Expanded extends Formatter +class Expanded extends Formatter { /** * {@inheritdoc} @@ -41,7 +41,7 @@ public function __construct() /** * {@inheritdoc} */ - protected function indentStr(): string + protected function indentStr() { return str_repeat($this->indentChar, $this->indentLevel); } @@ -49,7 +49,7 @@ protected function indentStr(): string /** * {@inheritdoc} */ - protected function blockLines(OutputBlock $block): void + protected function blockLines(OutputBlock $block) { $inner = $this->indentStr(); diff --git a/scssphp/src/Formatter/Nested.php b/scssphp/src/Formatter/Nested.php new file mode 100644 index 0000000..d5ed85c --- /dev/null +++ b/scssphp/src/Formatter/Nested.php @@ -0,0 +1,238 @@ + + * + * @deprecated since 1.4.0. Use the Expanded formatter instead. + * + * @internal + */ +class Nested extends Formatter +{ + /** + * @var int + */ + private $depth; + + /** + * {@inheritdoc} + */ + public function __construct() + { + @trigger_error('The Nested formatter is deprecated since 1.4.0. Use the Expanded formatter instead.', E_USER_DEPRECATED); + + $this->indentLevel = 0; + $this->indentChar = ' '; + $this->break = "\n"; + $this->open = ' {'; + $this->close = ' }'; + $this->tagSeparator = ', '; + $this->assignSeparator = ': '; + $this->keepSemicolons = true; + } + + /** + * {@inheritdoc} + */ + protected function indentStr() + { + $n = $this->depth - 1; + + return str_repeat($this->indentChar, max($this->indentLevel + $n, 0)); + } + + /** + * {@inheritdoc} + */ + protected function blockLines(OutputBlock $block) + { + $inner = $this->indentStr(); + $glue = $this->break . $inner; + + foreach ($block->lines as $index => $line) { + if (substr($line, 0, 2) === '/*') { + $replacedLine = preg_replace('/\r\n?|\n|\f/', $this->break, $line); + assert($replacedLine !== null); + $block->lines[$index] = $replacedLine; + } + } + + $this->write($inner . implode($glue, $block->lines)); + } + + /** + * {@inheritdoc} + */ + protected function block(OutputBlock $block) + { + static $depths; + static $downLevel; + static $closeBlock; + static $previousEmpty; + static $previousHasSelector; + + if ($block->type === 'root') { + $depths = [ 0 ]; + $downLevel = ''; + $closeBlock = ''; + $this->depth = 0; + $previousEmpty = false; + $previousHasSelector = false; + } + + $isMediaOrDirective = \in_array($block->type, [Type::T_DIRECTIVE, Type::T_MEDIA]); + $isSupport = ($block->type === Type::T_DIRECTIVE + && $block->selectors && strpos(implode('', $block->selectors), '@supports') !== false); + + while ($block->depth < end($depths) || ($block->depth == 1 && end($depths) == 1)) { + array_pop($depths); + $this->depth--; + + if ( + ! $this->depth && ($block->depth <= 1 || (! $this->indentLevel && $block->type === Type::T_COMMENT)) && + (($block->selectors && ! $isMediaOrDirective) || $previousHasSelector) + ) { + $downLevel = $this->break; + } + + if (empty($block->lines) && empty($block->children)) { + $previousEmpty = true; + } + } + + if (empty($block->lines) && empty($block->children)) { + return; + } + + $this->currentBlock = $block; + + if (! empty($block->lines) || (! empty($block->children) && ($this->depth < 1 || $isSupport))) { + if ($block->depth > end($depths)) { + if (! $previousEmpty || $this->depth < 1) { + $this->depth++; + + $depths[] = $block->depth; + } else { + // keep the current depth unchanged but take the block depth as a new reference for following blocks + array_pop($depths); + + $depths[] = $block->depth; + } + } + } + + $previousEmpty = ($block->type === Type::T_COMMENT); + $previousHasSelector = false; + + if (! empty($block->selectors)) { + if ($closeBlock) { + $this->write($closeBlock); + $closeBlock = ''; + } + + if ($downLevel) { + $this->write($downLevel); + $downLevel = ''; + } + + $this->blockSelectors($block); + + $this->indentLevel++; + } + + if (! empty($block->lines)) { + if ($closeBlock) { + $this->write($closeBlock); + $closeBlock = ''; + } + + if ($downLevel) { + $this->write($downLevel); + $downLevel = ''; + } + + $this->blockLines($block); + + $closeBlock = $this->break; + } + + if (! empty($block->children)) { + if ($this->depth > 0 && ($isMediaOrDirective || ! $this->hasFlatChild($block))) { + array_pop($depths); + + $this->depth--; + $this->blockChildren($block); + $this->depth++; + + $depths[] = $block->depth; + } else { + $this->blockChildren($block); + } + } + + // reclear to not be spoiled by children if T_DIRECTIVE + if ($block->type === Type::T_DIRECTIVE) { + $previousHasSelector = false; + } + + if (! empty($block->selectors)) { + $this->indentLevel--; + + if (! $this->keepSemicolons) { + $this->strippedSemicolon = ''; + } + + $this->write($this->close); + + $closeBlock = $this->break; + + if ($this->depth > 1 && ! empty($block->children)) { + array_pop($depths); + $this->depth--; + } + + if (! $isMediaOrDirective) { + $previousHasSelector = true; + } + } + + if ($block->type === 'root') { + $this->write($this->break); + } + } + + /** + * Block has flat child + * + * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $block + * + * @return bool + */ + private function hasFlatChild($block) + { + foreach ($block->children as $child) { + if (empty($child->selectors)) { + return true; + } + } + + return false; + } +} diff --git a/scssphp/src/Formatter/OutputBlock.php b/scssphp/src/Formatter/OutputBlock.php index 8287bd6..2799656 100644 --- a/scssphp/src/Formatter/OutputBlock.php +++ b/scssphp/src/Formatter/OutputBlock.php @@ -19,7 +19,7 @@ * * @internal */ -final class OutputBlock +class OutputBlock { /** * @var string|null diff --git a/scssphp/src/Logger/LoggerInterface.php b/scssphp/src/Logger/LoggerInterface.php index b7abc20..7c0a2f7 100644 --- a/scssphp/src/Logger/LoggerInterface.php +++ b/scssphp/src/Logger/LoggerInterface.php @@ -35,7 +35,7 @@ interface LoggerInterface * * @return void */ - public function warn(string $message, bool $deprecation = false); + public function warn($message, $deprecation = false); /** * Emits a debugging message. @@ -44,5 +44,5 @@ public function warn(string $message, bool $deprecation = false); * * @return void */ - public function debug(string $message); + public function debug($message); } diff --git a/scssphp/src/Logger/QuietLogger.php b/scssphp/src/Logger/QuietLogger.php index a9e58ea..ad7c075 100644 --- a/scssphp/src/Logger/QuietLogger.php +++ b/scssphp/src/Logger/QuietLogger.php @@ -12,19 +12,18 @@ namespace ScssPhp\ScssPhp\Logger; -use ScssPhp\ScssPhp\SourceSpan\FileSpan; -use ScssPhp\ScssPhp\StackTrace\Trace; - /** * A logger that silently ignores all messages. + * + * @final */ -final class QuietLogger implements LocationAwareLoggerInterface +class QuietLogger implements LoggerInterface { - public function warn(string $message, bool $deprecation = false, ?FileSpan $span = null, ?Trace $trace = null): void + public function warn($message, $deprecation = false) { } - public function debug(string $message, FileSpan $span = null): void + public function debug($message) { } } diff --git a/scssphp/src/Logger/StreamLogger.php b/scssphp/src/Logger/StreamLogger.php index a5ba9ca..7db7cc1 100644 --- a/scssphp/src/Logger/StreamLogger.php +++ b/scssphp/src/Logger/StreamLogger.php @@ -15,9 +15,9 @@ /** * A logger that prints to a PHP stream (for instance stderr) * - * TODO implement LocationAwareLoggerInterface once the compiler is migrated to actually provide the location + * @final */ -final class StreamLogger implements LoggerInterface +class StreamLogger implements LoggerInterface { private $stream; private $closeOnDestruct; @@ -26,7 +26,7 @@ final class StreamLogger implements LoggerInterface * @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, bool $closeOnDestruct = false) + public function __construct($stream, $closeOnDestruct = false) { $this->stream = $stream; $this->closeOnDestruct = $closeOnDestruct; @@ -45,7 +45,7 @@ public function __destruct() /** * @inheritDoc */ - public function warn(string $message, bool $deprecation = false) + public function warn($message, $deprecation = false) { $prefix = ($deprecation ? 'DEPRECATION ' : '') . 'WARNING: '; @@ -55,7 +55,7 @@ public function warn(string $message, bool $deprecation = false) /** * @inheritDoc */ - public function debug(string $message) + public function debug($message) { fwrite($this->stream, $message . "\n"); } diff --git a/scssphp/src/Node/Number.php b/scssphp/src/Node/Number.php index e12c82a..a38ba5f 100644 --- a/scssphp/src/Node/Number.php +++ b/scssphp/src/Node/Number.php @@ -33,17 +33,23 @@ * * @template-implements \ArrayAccess */ -final class Number extends Node implements \ArrayAccess +class Number extends Node implements \ArrayAccess, \JsonSerializable { const PRECISION = 10; + /** + * @var int + * @deprecated use {Number::PRECISION} instead to read the precision. Configuring it is not supported anymore. + */ + public static $precision = self::PRECISION; + /** * @see http://www.w3.org/TR/2012/WD-css3-values-20120308/ * * @var array * @phpstan-var array> */ - private static $unitTable = [ + protected static $unitTable = [ 'in' => [ 'in' => 1, 'pc' => 6, @@ -125,7 +131,7 @@ public function getDimension() } /** - * @return string[] + * @return list */ public function getNumeratorUnits() { @@ -133,13 +139,23 @@ public function getNumeratorUnits() } /** - * @return string[] + * @return list */ public function getDenominatorUnits() { return $this->denominatorUnits; } + /** + * @return mixed + */ + #[\ReturnTypeWillChange] + public function jsonSerialize() + { + // Passing a compiler instance makes the method output a Sass representation instead of a CSS one, supporting full units. + return $this->output(new Compiler()); + } + /** * @return bool */ @@ -548,7 +564,7 @@ public function equals(Number $other) try { return $this->coerceUnits($other, function ($num1, $num2) { - return round($num1,self::PRECISION) == round($num2, self::PRECISION); + return round($num1, self::PRECISION) == round($num2, self::PRECISION); }); } catch (SassScriptException $e) { return false; @@ -562,7 +578,7 @@ public function equals(Number $other) * * @return string */ - public function output(Compiler $compiler = null) + public function output(?Compiler $compiler = null) { $dimension = round($this->dimension, self::PRECISION); @@ -789,7 +805,7 @@ private static function getConversionFactor($unit1, $unit2) return 1; } - foreach (self::$unitTable as $unitVariants) { + foreach (static::$unitTable as $unitVariants) { if (isset($unitVariants[$unit1]) && isset($unitVariants[$unit2])) { return $unitVariants[$unit1] / $unitVariants[$unit2]; } diff --git a/scssphp/src/OutputStyle.php b/scssphp/src/OutputStyle.php index c284639..a1d8b42 100644 --- a/scssphp/src/OutputStyle.php +++ b/scssphp/src/OutputStyle.php @@ -1,9 +1,62 @@ */ - private static $precedence = [ + protected static $precedence = [ '=' => 0, 'or' => 1, 'and' => 2, @@ -65,20 +65,20 @@ final class Parser /** * @var string */ - private static $commentPattern; + protected static $commentPattern; /** * @var string */ - private static $operatorPattern; + protected static $operatorPattern; /** * @var string */ - private static $whitePattern; + protected static $whitePattern; /** * @var Cache|null */ - private $cache; + protected $cache; private $sourceName; private $sourceIndex; @@ -113,6 +113,7 @@ final class Parser * @var string */ private $buffer; + private $utf8; /** * @var string|null */ @@ -130,31 +131,37 @@ final class Parser /** * Constructor * + * @api + * * @param string|null $sourceName * @param int $sourceIndex + * @param string|null $encoding * @param Cache|null $cache * @param bool $cssOnly * @param LoggerInterface|null $logger */ - public function __construct(?string $sourceName, int $sourceIndex = 0, Cache $cache = null, bool $cssOnly = false, LoggerInterface $logger = null) + public function __construct($sourceName, $sourceIndex = 0, $encoding = 'utf-8', ?Cache $cache = null, $cssOnly = false, ?LoggerInterface $logger = null) { $this->sourceName = $sourceName ?: '(stdin)'; $this->sourceIndex = $sourceIndex; - $this->patternModifiers = 'Aisu'; + $this->utf8 = ! $encoding || strtolower($encoding) === 'utf-8'; + $this->patternModifiers = $this->utf8 ? 'Aisu' : 'Ais'; $this->commentsSeen = []; $this->allowVars = true; $this->cssOnly = $cssOnly; $this->logger = $logger ?: new QuietLogger(); - if (empty(self::$operatorPattern)) { - self::$operatorPattern = '([*\/%+-]|[!=]\=|\>\=?|\<\=?|and|or)'; + if (empty(static::$operatorPattern)) { + static::$operatorPattern = '([*\/%+-]|[!=]\=|\>\=?|\<\=?|and|or)'; $commentSingle = '\/\/'; $commentMultiLeft = '\/\*'; $commentMultiRight = '\*\/'; - self::$commentPattern = $commentMultiLeft . '.*?' . $commentMultiRight; - self::$whitePattern = '/' . $commentSingle . '[^\n]*\s*|(' . self::$commentPattern . ')\s*|\s+/AisuS'; + static::$commentPattern = $commentMultiLeft . '.*?' . $commentMultiRight; + static::$whitePattern = $this->utf8 + ? '/' . $commentSingle . '[^\n]*\s*|(' . static::$commentPattern . ')\s*|\s+/AisuS' + : '/' . $commentSingle . '[^\n]*\s*|(' . static::$commentPattern . ')\s*|\s+/AisS'; } $this->cache = $cache; @@ -163,23 +170,50 @@ public function __construct(?string $sourceName, int $sourceIndex = 0, Cache $ca /** * Get source file name * + * @api + * * @return string */ - public function getSourceName(): string + public function getSourceName() { return $this->sourceName; } + /** + * Throw parser error + * + * @api + * + * @param string $msg + * + * @phpstan-return never-return + * + * @throws ParserException + * + * @deprecated use "parseError" and throw the exception in the caller instead. + */ + public function throwParseError($msg = 'parse error') + { + @trigger_error( + 'The method "throwParseError" is deprecated. Use "parseError" and throw the exception in the caller instead', + E_USER_DEPRECATED + ); + + throw $this->parseError($msg); + } + /** * Creates a parser error * + * @api + * * @param string $msg * * @return ParserException */ - public function parseError(string $msg = 'parse error'): ParserException + public function parseError($msg = 'parse error') { - [$line, $column] = $this->getSourcePosition($this->count); + list($line, $column) = $this->getSourcePosition($this->count); $loc = empty($this->sourceName) ? "line: $line, column: $column" @@ -205,15 +239,19 @@ public function parseError(string $msg = 'parse error'): ParserException /** * Parser buffer * + * @api + * * @param string $buffer * * @return Block */ - public function parse(string $buffer): Block + public function parse($buffer) { if ($this->cache) { $cacheKey = $this->sourceName . ':' . md5($buffer); - $parseOptions = []; + $parseOptions = [ + 'utf8' => $this->utf8, + ]; $v = $this->cache->getCache('parse', $cacheKey, $parseOptions); if (! \is_null($v)) { @@ -235,6 +273,11 @@ public function parse(string $buffer): Block $this->saveEncoding(); $this->extractLineNumbers($buffer); + if ($this->utf8 && !preg_match('//u', $buffer)) { + $message = $this->sourceName ? 'Invalid UTF-8 file: ' . $this->sourceName : 'Invalid UTF-8 file'; + throw new ParserException($message); + } + $this->pushBlock(null); // root block $this->whitespace(); $this->pushBlock(null); @@ -265,24 +308,34 @@ public function parse(string $buffer): Block /** * Parse a value or value list * - * @param string $buffer - * @param string|array $out + * @api + * + * @param string $buffer + * @param mixed $out + * @param-out array|Number $out * * @return bool */ - public function parseValue(string $buffer, &$out): bool + public function parseValue($buffer, &$out) { $this->count = 0; $this->env = null; $this->inParens = false; $this->eatWhiteDefault = true; - $this->buffer = $buffer; + $this->buffer = (string) $buffer; $this->saveEncoding(); $this->extractLineNumbers($this->buffer); $list = $this->valueList($out); + if ($this->count !== \strlen($this->buffer)) { + $error = $this->parseError('Expected end of value'); + $message = 'Passing trailing content after the expression when parsing a value is deprecated since Scssphp 1.12.0 and will be an error in 2.0. ' . $error->getMessage(); + + @trigger_error($message, E_USER_DEPRECATED); + } + $this->restoreEncoding(); return $list; @@ -291,19 +344,21 @@ public function parseValue(string $buffer, &$out): bool /** * Parse a selector or selector list * - * @param string $buffer - * @param string|array $out - * @param bool $shouldValidate + * @api + * + * @param string $buffer + * @param array $out + * @param bool $shouldValidate * * @return bool */ - public function parseSelector(string $buffer, &$out, bool $shouldValidate = true): bool + public function parseSelector($buffer, &$out, $shouldValidate = true) { $this->count = 0; $this->env = null; $this->inParens = false; $this->eatWhiteDefault = true; - $this->buffer = $buffer; + $this->buffer = (string) $buffer; $this->saveEncoding(); $this->extractLineNumbers($this->buffer); @@ -327,22 +382,27 @@ public function parseSelector(string $buffer, &$out, bool $shouldValidate = true /** * Parse a media Query * + * @api + * * @param string $buffer * @param array $out * * @return bool */ - public function parseMediaQueryList(string $buffer, &$out): bool + public function parseMediaQueryList($buffer, &$out) { $this->count = 0; $this->env = null; $this->inParens = false; $this->eatWhiteDefault = true; - $this->buffer = $buffer; + $this->buffer = (string) $buffer; + $this->discardComments = true; $this->saveEncoding(); $this->extractLineNumbers($this->buffer); + $this->whitespace(); + $isMediaQuery = $this->mediaQueryList($out); $this->restoreEncoding(); @@ -389,7 +449,7 @@ public function parseMediaQueryList(string $buffer, &$out): bool * * @return bool */ - private function parseChunk(): bool + protected function parseChunk() { $s = $this->count; @@ -483,6 +543,24 @@ private function parseChunk(): bool $this->seek($s); + if ( + $this->literal('@scssphp-import-once', 20) && + $this->valueList($importPath) && + $this->end() + ) { + ! $this->cssOnly || $this->assertPlainCssValid(false, $s); + + list($line, $column) = $this->getSourcePosition($s); + $file = $this->sourceName; + $this->logger->warn("The \"@scssphp-import-once\" directive is deprecated and will be removed in ScssPhp 2.0, in \"$file\", line $line, column $column.", true); + + $this->append([Type::T_SCSSPHP_IMPORT_ONCE, $importPath], $s); + + return true; + } + + $this->seek($s); + if ( $this->literal('@import', 7) && $this->valueList($importPath) && @@ -725,7 +803,7 @@ private function parseChunk(): bool $last = $this->last(); if (isset($last) && $last[0] === Type::T_IF) { - [, $if] = $last; + list(, $if) = $last; assert($if instanceof IfBlock); if ($this->literal('@else', 5)) { @@ -1038,7 +1116,7 @@ private function parseChunk(): bool * * @return Block */ - private function pushBlock(?array $selectors, int $pos = 0): Block + protected function pushBlock($selectors, $pos = 0) { $b = new Block(); $b->selectors = $selectors; @@ -1056,7 +1134,7 @@ private function pushBlock(?array $selectors, int $pos = 0): Block */ private function registerPushedBlock(Block $b, $pos) { - [$line, $column] = $this->getSourcePosition($pos); + list($line, $column) = $this->getSourcePosition($pos); $b->sourceName = $this->sourceName; $b->sourceLine = $line; @@ -1099,7 +1177,7 @@ private function registerPushedBlock(Block $b, $pos) * * @return Block */ - private function pushSpecialBlock(string $type, int $pos): Block + protected function pushSpecialBlock($type, $pos) { $block = $this->pushBlock(null, $pos); $block->type = $type; @@ -1114,7 +1192,7 @@ private function pushSpecialBlock(string $type, int $pos): Block * * @throws \Exception */ - private function popBlock(): Block + protected function popBlock() { assert($this->env !== null); @@ -1151,7 +1229,7 @@ private function popBlock(): Block * * @return int */ - private function peek(string $regex, &$out, ?int $from = null): int + protected function peek($regex, &$out, $from = null) { if (! isset($from)) { $from = $this->count; @@ -1170,7 +1248,7 @@ private function peek(string $regex, &$out, ?int $from = null): int * * @return void */ - private function seek(int $where): void + protected function seek($where) { $this->count = $where; } @@ -1178,14 +1256,14 @@ private function seek(int $where): void /** * Assert a parsed part is plain CSS Valid * - * @param array|false $parsed + * @param array|Number|false $parsed * @param int $startPos * - * @return array + * @return array|Number * * @throws ParserException */ - private function assertPlainCssValid($parsed, ?int $startPos = null) + protected function assertPlainCssValid($parsed, $startPos = null) { $type = ''; if ($parsed) { @@ -1211,18 +1289,22 @@ private function assertPlainCssValid($parsed, ?int $startPos = null) /** * Check a parsed element is plain CSS Valid * - * @param array|string $parsed + * @param array|Number|string $parsed * @param bool $allowExpression * - * @return false|array|string + * @return ($parsed is string ? string : ($parsed is Number ? Number : array|false)) */ - private function isPlainCssValidElement($parsed, bool $allowExpression = false) + protected function isPlainCssValidElement($parsed, $allowExpression = false) { // keep string as is if (is_string($parsed)) { return $parsed; } + if ($parsed instanceof Number) { + return $parsed; + } + if ( \in_array($parsed[0], [Type::T_FUNCTION, Type::T_FUNCTION_CALL]) && !\in_array($parsed[1], [ @@ -1322,7 +1404,7 @@ private function isPlainCssValidElement($parsed, bool $allowExpression = false) return $parsed; case Type::T_EXPRESSION: - [ ,$op, $lhs, $rhs, $inParens, $whiteBefore, $whiteAfter] = $parsed; + list( ,$op, $lhs, $rhs, $inParens, $whiteBefore, $whiteAfter) = $parsed; if (! $allowExpression && ! \in_array($op, ['and', 'or', '/'])) { return false; } @@ -1397,7 +1479,7 @@ private function isPlainCssValidElement($parsed, bool $allowExpression = false) * * @phpstan-impure */ - private function matchString(&$m, string $delim): bool + protected function matchString(&$m, $delim) { $token = null; @@ -1439,7 +1521,7 @@ private function matchString(&$m, string $delim): bool * * @phpstan-impure */ - private function match(string $regex, &$out, ?bool $eatWhitespace = null) + protected function match($regex, &$out, $eatWhitespace = null) { $r = '/' . $regex . '/' . $this->patternModifiers; @@ -1470,7 +1552,7 @@ private function match(string $regex, &$out, ?bool $eatWhitespace = null) * * @phpstan-impure */ - private function matchChar(string $char, ?bool $eatWhitespace = null): bool + protected function matchChar($char, $eatWhitespace = null) { if (! isset($this->buffer[$this->count]) || $this->buffer[$this->count] !== $char) { return false; @@ -1500,7 +1582,7 @@ private function matchChar(string $char, ?bool $eatWhitespace = null): bool * * @phpstan-impure */ - private function literal(string $what, int $len, ?bool $eatWhitespace = null): bool + protected function literal($what, $len, $eatWhitespace = null) { if (strcasecmp(substr($this->buffer, $this->count, $len), $what) !== 0) { return false; @@ -1526,11 +1608,11 @@ private function literal(string $what, int $len, ?bool $eatWhitespace = null): b * * @phpstan-impure */ - private function whitespace(): bool + protected function whitespace() { $gotWhite = false; - while (preg_match(self::$whitePattern, $this->buffer, $m, 0, $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 = []; @@ -1558,8 +1640,10 @@ private function whitespace(): bool $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) { - throw $this->parseError('Unterminated interpolation'); + $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); @@ -1581,7 +1665,7 @@ private function whitespace(): bool $commentStatement = [Type::T_COMMENT, $staticComment, [Type::T_STRING, '', $comment]]; } - [$line, $column] = $this->getSourcePosition($startCommentCount); + list($line, $column) = $this->getSourcePosition($startCommentCount); $commentStatement[self::SOURCE_LINE] = $line; $commentStatement[self::SOURCE_COLUMN] = $column; $commentStatement[self::SOURCE_INDEX] = $this->sourceIndex; @@ -1612,11 +1696,11 @@ private function whitespace(): bool * * @return void */ - private function appendComment(array $comment): void + protected function appendComment($comment) { - assert($this->env !== null); - if (! $this->discardComments) { + assert($this->env !== null); + $this->env->comments[] = $comment; } } @@ -1629,7 +1713,7 @@ private function appendComment(array $comment): void * * @return void */ - private function append(?array $statement, ?int $pos = null): void + protected function append($statement, $pos = null) { assert($this->env !== null); @@ -1637,11 +1721,11 @@ private function append(?array $statement, ?int $pos = null): void ! $this->cssOnly || ($statement = $this->assertPlainCssValid($statement, $pos)); if (! \is_null($pos)) { - [$line, $column] = $this->getSourcePosition($pos); + list($line, $column) = $this->getSourcePosition($pos); - $statement[self::SOURCE_LINE] = $line; - $statement[self::SOURCE_COLUMN] = $column; - $statement[self::SOURCE_INDEX] = $this->sourceIndex; + $statement[static::SOURCE_LINE] = $line; + $statement[static::SOURCE_COLUMN] = $column; + $statement[static::SOURCE_INDEX] = $this->sourceIndex; } $this->env->children[] = $statement; @@ -1660,7 +1744,7 @@ private function append(?array $statement, ?int $pos = null): void * * @return array|null */ - private function last(): ?array + protected function last() { assert($this->env !== null); @@ -1680,7 +1764,7 @@ private function last(): ?array * * @return bool */ - private function mediaQueryList(&$out): bool + protected function mediaQueryList(&$out) { return $this->genericList($out, 'mediaQuery', ',', false); } @@ -1692,7 +1776,7 @@ private function mediaQueryList(&$out): bool * * @return bool */ - private function mediaQuery(&$out): bool + protected function mediaQuery(&$out) { $expressions = null; $parts = []; @@ -1746,7 +1830,7 @@ private function mediaQuery(&$out): bool * * @return bool */ - private function supportsQuery(&$out): bool + protected function supportsQuery(&$out) { $expressions = null; $parts = []; @@ -1879,7 +1963,7 @@ private function supportsQuery(&$out): bool * * @return bool */ - private function mediaExpression(&$out): bool + protected function mediaExpression(&$out) { $s = $this->count; $value = null; @@ -1912,7 +1996,7 @@ private function mediaExpression(&$out): bool * * @return bool */ - private function argValues(&$out): bool + protected function argValues(&$out) { $discardComments = $this->discardComments; $this->discardComments = true; @@ -1937,7 +2021,7 @@ private function argValues(&$out): bool * * @return bool */ - private function argValue(&$out): bool + protected function argValue(&$out) { $s = $this->count; @@ -1970,7 +2054,7 @@ private function argValue(&$out): bool * @param mixed $directiveName * @return bool */ - private function isKnownGenericDirective($directiveName): bool + protected function isKnownGenericDirective($directiveName) { if (\is_array($directiveName) && \is_string(reset($directiveName))) { $directiveName = reset($directiveName); @@ -2015,14 +2099,15 @@ private function isKnownGenericDirective($directiveName): bool /** * Parse directive value list that considers $vars as keyword * - * @param array $out + * @param mixed $out * @param string|false $endChar + * @param-out array|Number $out * * @return bool * * @phpstan-impure */ - private function directiveValue(&$out, $endChar = false): bool + protected function directiveValue(&$out, $endChar = false) { $s = $this->count; @@ -2079,11 +2164,12 @@ private function directiveValue(&$out, $endChar = false): bool /** * Parse comma separated value list * - * @param array $out + * @param mixed $out + * @param-out array|Number $out * * @return bool */ - private function valueList(&$out): bool + protected function valueList(&$out) { $discardComments = $this->discardComments; $this->discardComments = true; @@ -2097,14 +2183,15 @@ private function valueList(&$out): bool * Parse a function call, where externals () are part of the call * and not of the value list * - * @param array $out + * @param mixed $out * @param bool $mandatoryEnclos * @param null|string $charAfter * @param null|bool $eatWhiteSp + * @param-out array|Number $out * * @return bool */ - private function functionCallArgumentsList(&$out, bool $mandatoryEnclos = true, ?string $charAfter = null, ?bool $eatWhiteSp = null): bool + protected function functionCallArgumentsList(&$out, $mandatoryEnclos = true, $charAfter = null, $eatWhiteSp = null) { $s = $this->count; @@ -2136,11 +2223,12 @@ private function functionCallArgumentsList(&$out, bool $mandatoryEnclos = true, /** * Parse space separated value list * - * @param array $out + * @param mixed $out + * @param-out array|Number $out * * @return bool */ - private function spaceList(&$out): bool + protected function spaceList(&$out) { return $this->genericList($out, 'expression'); } @@ -2148,14 +2236,15 @@ private function spaceList(&$out): bool /** * Parse generic list * - * @param array $out + * @param mixed $out * @param string $parseItem The name of the method used to parse items * @param string $delim * @param bool $flatten + * @param-out ($flatten is false ? array : array|Number) $out * * @return bool */ - private function genericList(&$out, string $parseItem, string $delim = '', bool $flatten = true): bool + protected function genericList(&$out, $parseItem, $delim = '', $flatten = true) { $s = $this->count; $items = []; @@ -2255,15 +2344,16 @@ private function genericList(&$out, string $parseItem, string $delim = '', bool /** * Parse expression * - * @param array|Number $out - * @param bool $listOnly - * @param bool $lookForExp + * @param mixed $out + * @param bool $listOnly + * @param bool $lookForExp + * @param-out array|Number $out * * @return bool * * @phpstan-impure */ - private function expression(&$out, bool $listOnly = false, bool $lookForExp = true): bool + protected function expression(&$out, $listOnly = false, $lookForExp = true) { $s = $this->count; $discard = $this->discardComments; @@ -2322,16 +2412,17 @@ private function expression(&$out, bool $listOnly = false, bool $lookForExp = tr /** * Parse expression specifically checking for lists in parenthesis or brackets * - * @param array $out + * @param mixed $out * @param int $s * @param string $closingParen * @param string[] $allowedTypes + * @param-out array|Number $out * * @return bool * * @phpstan-param array $allowedTypes */ - private function enclosedExpression(&$out, int $s, string $closingParen = ')', array $allowedTypes = [Type::T_LIST, Type::T_MAP]): bool + protected function enclosedExpression(&$out, $s, $closingParen = ')', $allowedTypes = [Type::T_LIST, Type::T_MAP]) { if ($this->matchChar($closingParen) && \in_array(Type::T_LIST, $allowedTypes)) { $out = [Type::T_LIST, '', []]; @@ -2385,19 +2476,19 @@ private function enclosedExpression(&$out, int $s, string $closingParen = ')', a * Parse left-hand side of subexpression * * @param array|Number $lhs - * @param int $minP + * @param int $minP * * @return array|Number */ - private function expHelper($lhs, int $minP) + protected function expHelper($lhs, $minP) { - $operators = self::$operatorPattern; + $operators = static::$operatorPattern; $ss = $this->count; $whiteBefore = isset($this->buffer[$this->count - 1]) && ctype_space($this->buffer[$this->count - 1]); - while ($this->match($operators, $m, false) && self::$precedence[strtolower($m[1])] >= $minP) { + while ($this->match($operators, $m, false) && static::$precedence[strtolower($m[1])] >= $minP) { $whiteAfter = isset($this->buffer[$this->count]) && ctype_space($this->buffer[$this->count]); $varAfter = isset($this->buffer[$this->count]) && @@ -2421,7 +2512,7 @@ private function expHelper($lhs, int $minP) } // consume higher-precedence operators on the right-hand side - $rhs = $this->expHelper($rhs, self::$precedence[strtolower($op)] + 1); + $rhs = $this->expHelper($rhs, static::$precedence[strtolower($op)] + 1); $lhs = [Type::T_EXPRESSION, $op, $lhs, $rhs, $this->inParens, $whiteBefore, $whiteAfter]; @@ -2438,11 +2529,12 @@ private function expHelper($lhs, int $minP) /** * Parse value * - * @param array|Number $out + * @param mixed $out + * @param-out array|Number $out * * @return bool */ - private function value(&$out): bool + protected function value(&$out) { if (! isset($this->buffer[$this->count])) { return false; @@ -2646,11 +2738,12 @@ private function value(&$out): bool /** * Parse parenthesized value * - * @param array|Number $out + * @param mixed $out + * @param-out array|Number $out * * @return bool */ - private function parenValue(&$out): bool + protected function parenValue(&$out) { $s = $this->count; @@ -2689,7 +2782,7 @@ private function parenValue(&$out): bool * * @return bool */ - private function progid(&$out): bool + protected function progid(&$out) { $s = $this->count; @@ -2718,11 +2811,12 @@ private function progid(&$out): bool * Parse function call * * @param string $name - * @param array $func + * @param mixed $func + * @param-out array $func * * @return bool */ - private function func(string $name, &$func): bool + protected function func($name, &$func) { $s = $this->count; @@ -2780,7 +2874,7 @@ private function func(string $name, &$func): bool * * @return bool */ - private function argumentList(&$out): bool + protected function argumentList(&$out) { $s = $this->count; $this->matchChar('('); @@ -2821,11 +2915,12 @@ private function argumentList(&$out): bool /** * Parse mixin/function definition argument list * - * @param array $out + * @param mixed $out + * @param-out list $out * * @return bool */ - private function argumentDef(&$out): bool + protected function argumentDef(&$out) { $s = $this->count; $this->matchChar('('); @@ -2883,11 +2978,12 @@ private function argumentDef(&$out): bool /** * Parse map * - * @param array $out + * @param mixed $out + * @param-out array $out * * @return bool */ - private function map(&$out): bool + protected function map(&$out) { $s = $this->count; @@ -2925,11 +3021,12 @@ private function map(&$out): bool /** * Parse color * - * @param array $out + * @param mixed $out + * @param-out array $out * * @return bool */ - private function color(&$out): bool + protected function color(&$out) { $s = $this->count; @@ -2951,11 +3048,12 @@ private function color(&$out): bool /** * Parse number with unit * - * @param array|Number $unit + * @param mixed $unit + * @param-out Number $unit * * @return bool */ - private function unit(&$unit): bool + protected function unit(&$unit) { $s = $this->count; @@ -2982,7 +3080,7 @@ private function unit(&$unit): bool * * @return bool */ - private function string(&$out, bool $keepDelimWithInterpolation = false): bool + protected function string(&$out, $keepDelimWithInterpolation = false) { $s = $this->count; @@ -3065,7 +3163,7 @@ private function string(&$out, bool $keepDelimWithInterpolation = false): bool * * @return bool */ - private function matchEscapeCharacter(&$out, bool $inKeywords = false): bool + protected function matchEscapeCharacter(&$out, $inKeywords = false) { $s = $this->count; if ($this->match('[a-f0-9]', $m, false)) { @@ -3117,7 +3215,7 @@ private function matchEscapeCharacter(&$out, bool $inKeywords = false): bool * * @return bool */ - private function mixedKeyword(&$out, bool $restricted = false): bool + protected function mixedKeyword(&$out, $restricted = false) { $parts = []; @@ -3157,15 +3255,16 @@ private function mixedKeyword(&$out, bool $restricted = false): bool * Parse an unbounded string stopped by $end * * @param string $end - * @param array $out + * @param mixed $out * @param string $nestOpen * @param string $nestClose * @param bool $rtrim * @param string $disallow + * @param-out array $out * * @return bool */ - private function openString(string $end, &$out, ?string $nestOpen = null, ?string $nestClose = null, bool $rtrim = true, ?string $disallow = null): bool + protected function openString($end, &$out, $nestOpen = null, $nestClose = null, $rtrim = true, $disallow = null) { $oldWhite = $this->eatWhiteDefault; $this->eatWhiteDefault = false; @@ -3178,7 +3277,7 @@ private function openString(string $end, &$out, ?string $nestOpen = null, ?strin $patt = '(' . $patt . '*?)([\'"]|#\{|' . $this->pregQuote($end) . '|' . (($nestClose && $nestClose !== $end) ? $this->pregQuote($nestClose) . '|' : '') - . self::$commentPattern . ')'; + . static::$commentPattern . ')'; $nestingLevel = 0; @@ -3238,12 +3337,13 @@ private function openString(string $end, &$out, ?string $nestOpen = null, ?strin /** * Parser interpolation * - * @param string|array $out - * @param bool $lookWhite save information about whitespace before and after + * @param mixed $out + * @param bool $lookWhite save information about whitespace before and after + * @param-out array $out * * @return bool */ - private function interpolation(&$out, bool $lookWhite = true): bool + protected function interpolation(&$out, $lookWhite = true) { $oldWhite = $this->eatWhiteDefault; $allowVars = $this->allowVars; @@ -3298,7 +3398,7 @@ private function interpolation(&$out, bool $lookWhite = true): bool * * @return bool */ - private function propertyName(&$out): bool + protected function propertyName(&$out) { $parts = []; @@ -3332,7 +3432,7 @@ private function propertyName(&$out): bool } // match comment hack - if (preg_match(self::$whitePattern, $this->buffer, $m, 0, $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]); @@ -3353,7 +3453,7 @@ private function propertyName(&$out): bool * * @return bool */ - private function customProperty(&$out): bool + protected function customProperty(&$out) { $s = $this->count; @@ -3413,7 +3513,7 @@ private function customProperty(&$out): bool * * @return bool */ - private function selectors(&$out, $subSelector = false): bool + protected function selectors(&$out, $subSelector = false) { $s = $this->count; $selectors = []; @@ -3449,7 +3549,7 @@ private function selectors(&$out, $subSelector = false): bool * * @return bool */ - private function selector(&$out, $subSelector = false): bool + protected function selector(&$out, $subSelector = false) { $selector = []; @@ -3494,7 +3594,7 @@ private function selector(&$out, $subSelector = false): bool /** * parsing escaped chars in selectors: * - escaped single chars are kept escaped in the selector but in a normalized form - * (if not in 0-9a-f range as this would be ambiguous) + * (if not in 0-9a-f range as this would be ambigous) * - other escaped sequences (multibyte chars or 0-9a-f) are kept in their initial escaped form, * normalized to lowercase * @@ -3507,7 +3607,7 @@ private function selector(&$out, $subSelector = false): bool * * @return bool */ - private function matchEscapeCharacterInSelector(&$out, bool $keepEscapedNumber = false): bool + protected function matchEscapeCharacterInSelector(&$out, $keepEscapedNumber = false) { $s_escape = $this->count; if ($this->match('\\\\', $m)) { @@ -3553,7 +3653,7 @@ private function matchEscapeCharacterInSelector(&$out, bool $keepEscapedNumber = * * @return bool */ - private function selectorSingle(&$out, $subSelector = false): bool + protected function selectorSingle(&$out, $subSelector = false) { $oldWhite = $this->eatWhiteDefault; $this->eatWhiteDefault = false; @@ -3773,11 +3873,12 @@ private function selectorSingle(&$out, $subSelector = false): bool /** * Parse a variable * - * @param array $out + * @param mixed $out + * @param-out array{Type::*, string} $out * * @return bool */ - private function variable(&$out): bool + protected function variable(&$out) { $s = $this->count; @@ -3802,17 +3903,20 @@ private function variable(&$out): bool /** * Parse a keyword * - * @param string $word - * @param bool $eatWhitespace - * @param bool $inSelector + * @param mixed $word + * @param bool $eatWhitespace + * @param bool $inSelector + * @param-out string $word * * @return bool */ - private function keyword(&$word, ?bool $eatWhitespace = null, bool $inSelector = false): bool + protected function keyword(&$word, $eatWhitespace = null, $inSelector = false) { $s = $this->count; $match = $this->match( - '(([\pL\w\x{00A0}-\x{10FFFF}_\-\*!"\']|\\\\[a-f0-9]{6} ?|\\\\[a-f0-9]{1,5}(?![a-f0-9]) ?|[\\\\].)([\pL\w\x{00A0}-\x{10FFFF}\-_"\']|\\\\[a-f0-9]{6} ?|\\\\[a-f0-9]{1,5}(?![a-f0-9]) ?|[\\\\].)*)', + $this->utf8 + ? '(([\pL\w\x{00A0}-\x{10FFFF}_\-\*!"\']|\\\\[a-f0-9]{6} ?|\\\\[a-f0-9]{1,5}(?![a-f0-9]) ?|[\\\\].)([\pL\w\x{00A0}-\x{10FFFF}\-_"\']|\\\\[a-f0-9]{6} ?|\\\\[a-f0-9]{1,5}(?![a-f0-9]) ?|[\\\\].)*)' + : '(([\w_\-\*!"\']|\\\\[a-f0-9]{6} ?|\\\\[a-f0-9]{1,5}(?![a-f0-9]) ?|[\\\\].)([\w\-_"\']|\\\\[a-f0-9]{6} ?|\\\\[a-f0-9]{1,5}(?![a-f0-9]) ?|[\\\\].)*)', $m, false ); @@ -3873,7 +3977,7 @@ private function keyword(&$word, ?bool $eatWhitespace = null, bool $inSelector = * * @return bool */ - private function restrictedKeyword(&$word, $eatWhitespace = null, $inSelector = false): bool + protected function restrictedKeyword(&$word, $eatWhitespace = null, $inSelector = false) { $s = $this->count; @@ -3893,10 +3997,12 @@ private function restrictedKeyword(&$word, $eatWhitespace = null, $inSelector = * * @return bool */ - private function placeholder(&$placeholder): bool + protected function placeholder(&$placeholder) { $match = $this->match( - '([\pL\w\-_]+)', + $this->utf8 + ? '([\pL\w\-_]+)' + : '([\w\-_]+)', $m ); @@ -3916,20 +4022,21 @@ private function placeholder(&$placeholder): bool /** * Parse a url * - * @param array $out + * @param mixed $out + * @param-out array $out * * @return bool */ - private function url(&$out): bool + protected function url(&$out) { if ($this->literal('url(', 4)) { $s = $this->count; if ( - ($this->string($out) || $this->spaceList($out)) && + ($this->string($inner) || $this->spaceList($inner)) && $this->matchChar(')') ) { - $out = [Type::T_STRING, '', ['url(', $out, ')']]; + $out = [Type::T_STRING, '', ['url(', $inner, ')']]; return true; } @@ -3955,7 +4062,7 @@ private function url(&$out): bool * * @return bool */ - private function end(?bool $eatWhitespace = null): bool + protected function end($eatWhitespace = null) { if ($this->matchChar(';', $eatWhitespace)) { return true; @@ -3972,11 +4079,11 @@ private function end(?bool $eatWhitespace = null): bool /** * Strip assignment flag from the list * - * @param array $value + * @param array|Number $value * * @return string[] */ - private function stripAssignmentFlags(&$value): array + protected function stripAssignmentFlags(&$value) { $flags = []; @@ -4003,7 +4110,7 @@ private function stripAssignmentFlags(&$value): array * * @return bool */ - private function stripOptionalFlag(&$selectors): bool + protected function stripOptionalFlag(&$selectors) { $optional = false; $selector = end($selectors); @@ -4025,7 +4132,7 @@ private function stripOptionalFlag(&$selectors): bool * * @return array */ - private function flattenList($value) + protected function flattenList($value) { if ($value[0] === Type::T_LIST && \count($value[2]) === 1) { return $this->flattenList($value[2][0]); @@ -4041,7 +4148,7 @@ private function flattenList($value) * * @return string */ - private function pregQuote(string $what): string + private function pregQuote($what) { return preg_quote($what, '/'); } @@ -4053,7 +4160,7 @@ private function pregQuote(string $what): string * * @return void */ - private function extractLineNumbers(string $buffer): void + private function extractLineNumbers($buffer) { $this->sourcePositions = [0 => 0]; $prev = 0; @@ -4078,7 +4185,7 @@ private function extractLineNumbers(string $buffer): void * @return array * @phpstan-return array{int, int} */ - private function getSourcePosition(int $pos): array + private function getSourcePosition($pos) { $low = 0; $high = \count($this->sourcePositions); @@ -4114,7 +4221,7 @@ private function getSourcePosition(int $pos): array * * @return void */ - private function saveEncoding(): void + private function saveEncoding() { if (\PHP_VERSION_ID < 80000 && \extension_loaded('mbstring') && (2 & (int) ini_get('mbstring.func_overload')) > 0) { $this->encoding = mb_internal_encoding(); @@ -4128,7 +4235,7 @@ private function saveEncoding(): void * * @return void */ - private function restoreEncoding(): void + private function restoreEncoding() { if (\extension_loaded('mbstring') && $this->encoding) { mb_internal_encoding($this->encoding); diff --git a/scssphp/src/SourceMap/Base64.php b/scssphp/src/SourceMap/Base64.php index 26c7866..00b6b45 100644 --- a/scssphp/src/SourceMap/Base64.php +++ b/scssphp/src/SourceMap/Base64.php @@ -19,7 +19,7 @@ * * @internal */ -final class Base64 +class Base64 { /** * @var array diff --git a/scssphp/src/SourceMap/Base64VLQ.php b/scssphp/src/SourceMap/Base64VLQ.php index 56aaf6b..2a5210c 100644 --- a/scssphp/src/SourceMap/Base64VLQ.php +++ b/scssphp/src/SourceMap/Base64VLQ.php @@ -37,7 +37,7 @@ * * @internal */ -final class Base64VLQ +class Base64VLQ { // A Base64 VLQ digit can represent 5 bits, so it is base-32. const VLQ_BASE_SHIFT = 5; diff --git a/scssphp/src/SourceMap/SourceMapGenerator.php b/scssphp/src/SourceMap/SourceMapGenerator.php index 5062e75..ccd4f02 100644 --- a/scssphp/src/SourceMap/SourceMapGenerator.php +++ b/scssphp/src/SourceMap/SourceMapGenerator.php @@ -12,6 +12,8 @@ namespace ScssPhp\ScssPhp\SourceMap; +use ScssPhp\ScssPhp\Exception\CompilerException; + /** * Source Map Generator * @@ -22,7 +24,7 @@ * * @internal */ -final class SourceMapGenerator +class SourceMapGenerator { /** * What version of source map does the generator generate? @@ -35,7 +37,7 @@ final class SourceMapGenerator * @var array * @phpstan-var array{sourceRoot: string, sourceMapFilename: string|null, sourceMapURL: string|null, sourceMapWriteTo: string|null, outputSourceFiles: bool, sourceMapRootpath: string, sourceMapBasepath: string} */ - private $defaultOptions = [ + protected $defaultOptions = [ // an optional source root, useful for relocating source files // on a server or removing repeated values in the 'sources' entry. // This value is prepended to the individual entries in the 'source' field. @@ -65,7 +67,7 @@ final class SourceMapGenerator * * @var \ScssPhp\ScssPhp\SourceMap\Base64VLQ */ - private $encoder; + protected $encoder; /** * Array of mappings @@ -73,19 +75,26 @@ final class SourceMapGenerator * @var array * @phpstan-var list */ - private $mappings = []; + protected $mappings = []; + + /** + * Array of contents map + * + * @var array + */ + protected $contentsMap = []; /** * File to content map * * @var array */ - private $sources = []; + protected $sources = []; /** * @var array */ - private $sourceKeys = []; + protected $sourceKeys = []; /** * @var array @@ -126,6 +135,38 @@ public function addMapping($generatedLine, $generatedColumn, $originalLine, $ori $this->sources[$sourceFile] = $sourceFile; } + /** + * Saves the source map to a file + * + * @param string $content The content to write + * + * @return string|null + * + * @throws \ScssPhp\ScssPhp\Exception\CompilerException If the file could not be saved + * @deprecated + */ + public function saveMap($content) + { + $file = $this->options['sourceMapWriteTo']; + assert($file !== null); + $dir = \dirname($file); + + // directory does not exist + if (! is_dir($dir)) { + // FIXME: create the dir automatically? + throw new CompilerException( + sprintf('The directory "%s" does not exist. Cannot save the source map.', $dir) + ); + } + + // FIXME: proper saving, with dir write check! + if (file_put_contents($file, $content) === false) { + throw new CompilerException(sprintf('Cannot save the source map to "%s"', $file)); + } + + return $this->options['sourceMapURL']; + } + /** * Generates the JSON source map * @@ -135,7 +176,7 @@ public function addMapping($generatedLine, $generatedColumn, $originalLine, $ori * * @see https://docs.google.com/document/d/1U1RGAehQwRypUTovF1KRlpiOFze0b-_2gc6fAH0KY0k/edit# */ - public function generateJson(string $prefix = ''): string + public function generateJson($prefix = '') { $sourceMap = []; $mappings = $this->generateMappings($prefix); @@ -199,7 +240,7 @@ public function generateJson(string $prefix = ''): string * * @return string[]|null */ - private function getSourcesContent(): ?array + protected function getSourcesContent() { if (empty($this->sources)) { return null; @@ -221,7 +262,7 @@ private function getSourcesContent(): ?array * * @return string */ - public function generateMappings(string $prefix = ''): string + public function generateMappings($prefix = '') { if (! \count($this->mappings)) { return ''; @@ -296,9 +337,9 @@ public function generateMappings(string $prefix = ''): string * * @return int|false */ - private function findFileIndex(string $filename) + protected function findFileIndex($filename) { - return $this->sourceKeys[$filename] ?? false; + return $this->sourceKeys[$filename]; } /** @@ -308,7 +349,7 @@ private function findFileIndex(string $filename) * * @return string */ - private function normalizeFilename(string $filename): string + protected function normalizeFilename($filename) { $filename = $this->fixWindowsPath($filename); $rootpath = $this->options['sourceMapRootpath']; @@ -335,7 +376,7 @@ private function normalizeFilename(string $filename): string * * @return string */ - public function fixWindowsPath(string $path, bool $addEndSlash = false): string + public function fixWindowsPath($path, $addEndSlash = false) { $slash = ($addEndSlash) ? '/' : ''; diff --git a/scssphp/src/Type.php b/scssphp/src/Type.php index 5fcbbff..2f8ab65 100644 --- a/scssphp/src/Type.php +++ b/scssphp/src/Type.php @@ -17,7 +17,7 @@ * * @author Anthon Pang */ -final class Type +class Type { /** * @internal @@ -31,6 +31,11 @@ final class Type * @internal */ const T_BLOCK = 'block'; + /** + * @deprecated + * @internal + */ + const T_BREAK = 'break'; /** * @internal */ @@ -40,6 +45,16 @@ final class Type * @internal */ const T_COMMENT = 'comment'; + /** + * @deprecated + * @internal + */ + const T_CONTINUE = 'continue'; + /** + * @deprecated + * @internal + */ + const T_CONTROL = 'control'; /** * @internal */ @@ -80,6 +95,9 @@ final class Type * @internal */ const T_FOR = 'for'; + /** + * @internal + */ const T_FUNCTION = 'function'; /** * @internal diff --git a/scssphp/src/Util.php b/scssphp/src/Util.php index d507afd..ad608ce 100644 --- a/scssphp/src/Util.php +++ b/scssphp/src/Util.php @@ -15,8 +15,6 @@ use ScssPhp\ScssPhp\Base\Range; use ScssPhp\ScssPhp\Exception\RangeException; use ScssPhp\ScssPhp\Node\Number; -use ScssPhp\ScssPhp\SourceSpan\FileSpan; -use ScssPhp\ScssPhp\Util\StringUtil; /** * Utility functions @@ -25,18 +23,8 @@ * * @internal */ -final class Util +class Util { - /** - * Returns $string with every line indented $indentation spaces. - */ - public static function indent(string $string, int $indentation): string - { - return implode("\n", array_map(function ($line) use ($indentation) { - return str_repeat(' ', $indentation) . $line; - }, explode("\n", $string))); - } - /** * Asserts that `value` falls within `range` (inclusive), leaving * room for slight floating-point errors. @@ -48,9 +36,9 @@ public static function indent(string $string, int $indentation): string * * @return mixed `value` adjusted to fall within range, if it was outside by a floating-point margin. * - * @throws RangeException + * @throws \ScssPhp\ScssPhp\Exception\RangeException */ - public static function checkRange(string $name, Range $range, $value, string $unit = '') + public static function checkRange($name, Range $range, $value, $unit = '') { $val = $value[1]; $grace = new Range(-0.00001, 0.00001); @@ -81,63 +69,13 @@ public static function checkRange(string $name, Range $range, $value, string $un * * @return string */ - public static function encodeURIComponent(string $string): string + public static function encodeURIComponent($string) { $revert = ['%21' => '!', '%2A' => '*', '%27' => "'", '%28' => '(', '%29' => ')']; return strtr(rawurlencode($string), $revert); } - /** - * Returns the variable name (including the leading `$`) from a $span that - * covers a variable declaration, which includes the variable name as well as - * the colon and expression following it. - * - * This isn't particularly efficient, and should only be used for error - * messages. - */ - public static function declarationName(FileSpan $span): string - { - $text = $span->getText(); - $pos = strpos($text, ':'); - - return StringUtil::trimAsciiRight(substr($text, 0, $pos === false ? null : $pos)); - } - - /** - * Returns $name without a vendor prefix. - * - * If $name has no vendor prefix, it's returned as-is. - * - * @param string $name - * - * @return string - */ - public static function unvendor(string $name): string - { - $length = \strlen($name); - - if ($length < 2) { - return $name; - } - - if ($name[0] !== '-') { - return $name; - } - - if ($name[1] === '-') { - return $name; - } - - for ($i = 2; $i < $length; $i++) { - if ($name[$i] === '-') { - return substr($name, $i + 1); - } - } - - return $name; - } - /** * mb_chr() wrapper * @@ -145,7 +83,7 @@ public static function unvendor(string $name): string * * @return string */ - public static function mbChr(int $code): string + public static function mbChr($code) { // Use the native implementation if available, but not on PHP 7.2 as mb_chr(0) is buggy there if (\PHP_VERSION_ID > 70300 && \function_exists('mb_chr')) { @@ -166,50 +104,13 @@ public static function mbChr(int $code): string return $s; } - /** - * mb_ord() wrapper - * - * @param string $string - * - * @return int - */ - public static function mbOrd(string $string): int - { - if (\function_exists('mb_ord')) { - return mb_ord($string, 'UTF-8'); - } - - if (1 === \strlen($string)) { - return \ord($string); - } - - $s = unpack('C*', substr($string, 0, 4)); - - if (!$s) { - return 0; - } - - $code = $s[1]; - if (0xF0 <= $code) { - return (($code - 0xF0) << 18) + (($s[2] - 0x80) << 12) + (($s[3] - 0x80) << 6) + $s[4] - 0x80; - } - if (0xE0 <= $code) { - return (($code - 0xE0) << 12) + (($s[2] - 0x80) << 6) + $s[3] - 0x80; - } - if (0xC0 <= $code) { - return (($code - 0xC0) << 6) + $s[2] - 0x80; - } - - return $code; - } - /** * mb_strlen() wrapper * * @param string $string * @return int */ - public static function mbStrlen(string $string): int + public static function mbStrlen($string) { // Use the native implementation if available. if (\function_exists('mb_strlen')) { @@ -230,7 +131,7 @@ public static function mbStrlen(string $string): int * @param null|int $length * @return string */ - public static function mbSubstr(string $string, int $start, ?int $length = null): string + public static function mbSubstr($string, $start, $length = null) { // Use the native implementation if available. if (\function_exists('mb_substr')) { @@ -268,7 +169,7 @@ public static function mbSubstr(string $string, int $start, ?int $length = null) * * @return int|false */ - public static function mbStrpos(string $haystack, string $needle, int $offset = 0) + public static function mbStrpos($haystack, $needle, $offset = 0) { if (\function_exists('mb_strpos')) { return mb_strpos($haystack, $needle, $offset, 'UTF-8'); diff --git a/scssphp/src/Util/Path.php b/scssphp/src/Util/Path.php index 0d7d95a..f399e41 100644 --- a/scssphp/src/Util/Path.php +++ b/scssphp/src/Util/Path.php @@ -5,14 +5,14 @@ /** * @internal */ -final class Path +class Path { /** * @param string $path * * @return bool */ - public static function isAbsolute(string $path): bool + public static function isAbsolute($path) { if ($path === '') { return false; @@ -22,28 +22,10 @@ public static function isAbsolute(string $path): bool return true; } - if (\DIRECTORY_SEPARATOR === '\\') { - return self::isWindowsAbsolute($path); - } - - return false; - } - - /** - * @param string $path - * - * @return bool - */ - public static function isWindowsAbsolute(string $path): bool - { - if ($path === '') { + if (\DIRECTORY_SEPARATOR !== '\\') { return false; } - if ($path[0] === '/') { - return true; - } - if ($path[0] === '\\') { return true; } @@ -73,7 +55,7 @@ public static function isWindowsAbsolute(string $path): bool * * @return string */ - public static function join(string $part1, string $part2): string + public static function join($part1, $part2) { if ($part1 === '' || self::isAbsolute($part2)) { return $part2; @@ -92,30 +74,4 @@ public static function join(string $part1, string $part2): string return $part1 . $separator . $part2; } - - /** - * Returns a pretty URI for a path - * - * @param string $path - * - * @return string - */ - public static function prettyUri(string $path): string - { - $normalizedPath = $path; - $normalizedRootDirectory = getcwd().'/'; - - if (\DIRECTORY_SEPARATOR === '\\') { - $normalizedRootDirectory = str_replace('\\', '/', $normalizedRootDirectory); - $normalizedPath = str_replace('\\', '/', $path); - } - - // TODO add support for returning a relative path using ../ in some cases, like Dart's path.prettyUri method - - if (0 === strpos($normalizedPath, $normalizedRootDirectory)) { - return substr($path, \strlen($normalizedRootDirectory)); - } - - return $path; - } } diff --git a/scssphp/src/ValueConverter.php b/scssphp/src/ValueConverter.php index 6b6875e..e12a0eb 100644 --- a/scssphp/src/ValueConverter.php +++ b/scssphp/src/ValueConverter.php @@ -33,7 +33,7 @@ private function __construct() * * @return mixed */ - public static function parseValue(string $source) + public static function parseValue($source) { $parser = new Parser(__CLASS__); diff --git a/scssphp/src/Version.php b/scssphp/src/Version.php index 5e57295..45fc983 100644 --- a/scssphp/src/Version.php +++ b/scssphp/src/Version.php @@ -17,7 +17,7 @@ * * @author Leaf Corcoran */ -final class Version +class Version { - const VERSION = '1.11.0'; + const VERSION = '1.13.0'; } diff --git a/scssphp/src/Warn.php b/scssphp/src/Warn.php index 8cdc192..592b44c 100644 --- a/scssphp/src/Warn.php +++ b/scssphp/src/Warn.php @@ -29,7 +29,7 @@ final class Warn * * @return void */ - public static function warning(string $message): void + public static function warning($message) { self::reportWarning($message, false); } @@ -43,7 +43,7 @@ public static function warning(string $message): void * * @return void */ - public static function deprecation(string $message): void + public static function deprecation($message) { self::reportWarning($message, true); } @@ -59,7 +59,7 @@ public static function deprecation(string $message): void * * @internal */ - public static function setCallback(callable $callback = null): ?callable + public static function setCallback(callable $callback = null) { $previousCallback = self::$callback; self::$callback = $callback; @@ -73,7 +73,7 @@ public static function setCallback(callable $callback = null): ?callable * * @return void */ - private static function reportWarning(string $message, bool $deprecation): 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.'); From 71484266903c074b936dc8ef063fe748985a70b6 Mon Sep 17 00:00:00 2001 From: Skylar Bolton Date: Fri, 6 Mar 2026 07:55:18 -0500 Subject: [PATCH 2/2] Remove extraneous 1.11.0 files left by merge MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The merge commit d83318a silently re-added 188 files from scssphp 1.11.0's experimental new architecture (Ast/, Serializer/, Parser/, Value/, Visitor/, etc.) that were removed in 1.13.0. These files don't exist in the upstream 1.13.0 release and are dead code — the autoloader never requests them — but they bloat the bundle and misrepresent the library version. Restores scssphp/src/ to exactly the 1.13.0 file set. --- scssphp/src/Ast/AstNode.php | 27 - scssphp/src/Ast/Css/CssAtRule.php | 35 - scssphp/src/Ast/Css/CssComment.php | 34 - scssphp/src/Ast/Css/CssDeclaration.php | 62 - scssphp/src/Ast/Css/CssImport.php | 37 - scssphp/src/Ast/Css/CssKeyframeBlock.php | 30 - scssphp/src/Ast/Css/CssMediaQuery.php | 258 - scssphp/src/Ast/Css/CssMediaRule.php | 30 - scssphp/src/Ast/Css/CssNode.php | 63 - scssphp/src/Ast/Css/CssParentNode.php | 37 - scssphp/src/Ast/Css/CssStyleRule.php | 39 - scssphp/src/Ast/Css/CssStylesheet.php | 24 - scssphp/src/Ast/Css/CssSupportsRule.php | 28 - scssphp/src/Ast/Css/CssValue.php | 71 - scssphp/src/Ast/Css/IsInvisibleVisitor.php | 63 - scssphp/src/Ast/Css/ModifiableCssAtRule.php | 103 - scssphp/src/Ast/Css/ModifiableCssComment.php | 61 - .../src/Ast/Css/ModifiableCssDeclaration.php | 115 - scssphp/src/Ast/Css/ModifiableCssImport.php | 77 - .../Ast/Css/ModifiableCssKeyframeBlock.php | 69 - .../src/Ast/Css/ModifiableCssMediaRule.php | 65 - scssphp/src/Ast/Css/ModifiableCssNode.php | 151 - .../src/Ast/Css/ModifiableCssParentNode.php | 69 - .../src/Ast/Css/ModifiableCssStyleRule.php | 86 - .../src/Ast/Css/ModifiableCssStylesheet.php | 57 - .../src/Ast/Css/ModifiableCssSupportsRule.php | 69 - scssphp/src/Ast/Css/ModifiableCssValue.php | 32 - scssphp/src/Ast/FakeAstNode.php | 47 - scssphp/src/Ast/Sass/Argument.php | 99 - scssphp/src/Ast/Sass/ArgumentDeclaration.php | 228 - scssphp/src/Ast/Sass/ArgumentInvocation.php | 127 - scssphp/src/Ast/Sass/AtRootQuery.php | 167 - scssphp/src/Ast/Sass/CallableInvocation.php | 18 - scssphp/src/Ast/Sass/ConfiguredVariable.php | 86 - scssphp/src/Ast/Sass/Expression.php | 30 - .../Expression/BinaryOperationExpression.php | 148 - .../Ast/Sass/Expression/BinaryOperator.php | 72 - .../Ast/Sass/Expression/BooleanExpression.php | 63 - .../Sass/Expression/CalculationExpression.php | 235 - .../Ast/Sass/Expression/ColorExpression.php | 64 - .../Sass/Expression/FunctionExpression.php | 141 - .../src/Ast/Sass/Expression/IfExpression.php | 89 - .../InterpolatedFunctionExpression.php | 84 - .../Ast/Sass/Expression/ListExpression.php | 142 - .../src/Ast/Sass/Expression/MapExpression.php | 71 - .../Ast/Sass/Expression/NullExpression.php | 51 - .../Ast/Sass/Expression/NumberExpression.php | 76 - .../Expression/ParenthesizedExpression.php | 63 - .../Sass/Expression/SelectorExpression.php | 51 - .../Ast/Sass/Expression/StringExpression.php | 182 - .../Sass/Expression/SupportsExpression.php | 60 - .../Expression/UnaryOperationExpression.php | 87 - .../src/Ast/Sass/Expression/UnaryOperator.php | 24 - .../Ast/Sass/Expression/ValueExpression.php | 67 - .../Sass/Expression/VariableExpression.php | 104 - scssphp/src/Ast/Sass/Import.php | 22 - scssphp/src/Ast/Sass/Import/DynamicImport.php | 62 - scssphp/src/Ast/Sass/Import/StaticImport.php | 83 - scssphp/src/Ast/Sass/Interpolation.php | 138 - scssphp/src/Ast/Sass/SassDeclaration.php | 37 - scssphp/src/Ast/Sass/SassNode.php | 24 - scssphp/src/Ast/Sass/SassReference.php | 50 - scssphp/src/Ast/Sass/Statement.php | 30 - scssphp/src/Ast/Sass/Statement/AtRootRule.php | 80 - scssphp/src/Ast/Sass/Statement/AtRule.php | 93 - .../Sass/Statement/CallableDeclaration.php | 90 - .../src/Ast/Sass/Statement/ContentBlock.php | 46 - .../src/Ast/Sass/Statement/ContentRule.php | 71 - scssphp/src/Ast/Sass/Statement/DebugRule.php | 66 - .../src/Ast/Sass/Statement/Declaration.php | 143 - scssphp/src/Ast/Sass/Statement/EachRule.php | 88 - scssphp/src/Ast/Sass/Statement/ElseClause.php | 26 - scssphp/src/Ast/Sass/Statement/ErrorRule.php | 66 - scssphp/src/Ast/Sass/Statement/ExtendRule.php | 84 - scssphp/src/Ast/Sass/Statement/ForRule.php | 111 - .../src/Ast/Sass/Statement/FunctionRule.php | 43 - .../Ast/Sass/Statement/HasContentVisitor.php | 31 - scssphp/src/Ast/Sass/Statement/IfClause.php | 44 - scssphp/src/Ast/Sass/Statement/IfRule.php | 106 - .../src/Ast/Sass/Statement/IfRuleClause.php | 73 - scssphp/src/Ast/Sass/Statement/ImportRule.php | 70 - .../src/Ast/Sass/Statement/IncludeRule.php | 149 - .../src/Ast/Sass/Statement/LoudComment.php | 57 - scssphp/src/Ast/Sass/Statement/MediaRule.php | 75 - scssphp/src/Ast/Sass/Statement/MixinRule.php | 81 - .../Ast/Sass/Statement/ParentStatement.php | 86 - scssphp/src/Ast/Sass/Statement/ReturnRule.php | 66 - .../src/Ast/Sass/Statement/SilentComment.php | 63 - scssphp/src/Ast/Sass/Statement/StyleRule.php | 77 - scssphp/src/Ast/Sass/Statement/Stylesheet.php | 126 - .../src/Ast/Sass/Statement/SupportsRule.php | 70 - .../Sass/Statement/VariableDeclaration.php | 174 - scssphp/src/Ast/Sass/Statement/WarnRule.php | 66 - scssphp/src/Ast/Sass/Statement/WhileRule.php | 73 - scssphp/src/Ast/Sass/SupportsCondition.php | 22 - .../SupportsCondition/SupportsAnything.php | 61 - .../SupportsCondition/SupportsDeclaration.php | 91 - .../SupportsCondition/SupportsFunction.php | 74 - .../SupportsInterpolation.php | 60 - .../SupportsCondition/SupportsNegation.php | 63 - .../SupportsCondition/SupportsOperation.php | 94 - .../src/Ast/Selector/AttributeOperator.php | 51 - .../src/Ast/Selector/AttributeSelector.php | 140 - scssphp/src/Ast/Selector/ClassSelector.php | 57 - scssphp/src/Ast/Selector/Combinator.php | 38 - scssphp/src/Ast/Selector/ComplexSelector.php | 289 -- .../Ast/Selector/ComplexSelectorComponent.php | 96 - scssphp/src/Ast/Selector/CompoundSelector.php | 136 - scssphp/src/Ast/Selector/IDSelector.php | 73 - scssphp/src/Ast/Selector/IsBogusVisitor.php | 67 - .../src/Ast/Selector/IsInvisibleVisitor.php | 72 - scssphp/src/Ast/Selector/IsUselessVisitor.php | 43 - scssphp/src/Ast/Selector/ParentSelector.php | 61 - .../src/Ast/Selector/PlaceholderSelector.php | 68 - scssphp/src/Ast/Selector/PseudoSelector.php | 358 -- scssphp/src/Ast/Selector/QualifiedName.php | 69 - scssphp/src/Ast/Selector/Selector.php | 109 - scssphp/src/Ast/Selector/SelectorList.php | 355 -- scssphp/src/Ast/Selector/SimpleSelector.php | 161 - scssphp/src/Ast/Selector/TypeSelector.php | 86 - .../src/Ast/Selector/UniversalSelector.php | 109 - scssphp/src/Collection/Map.php | 163 - scssphp/src/Exception/SassFormatException.php | 54 - .../src/Exception/SassRuntimeException.php | 54 - scssphp/src/Extend/ExtendUtil.php | 1256 ----- scssphp/src/Logger/AdaptingLogger.php | 76 - .../Logger/LocationAwareLoggerInterface.php | 49 - scssphp/src/Parser/AtRootQueryParser.php | 56 - scssphp/src/Parser/CssParser.php | 173 - scssphp/src/Parser/FormatException.php | 38 - scssphp/src/Parser/InterpolationBuffer.php | 109 - scssphp/src/Parser/KeyframeSelectorParser.php | 105 - scssphp/src/Parser/LineScanner.php | 163 - scssphp/src/Parser/MediaQueryParser.php | 154 - scssphp/src/Parser/Parser.php | 958 ---- scssphp/src/Parser/ScssParser.php | 275 - scssphp/src/Parser/SelectorParser.php | 574 --- scssphp/src/Parser/StringScanner.php | 348 -- scssphp/src/Parser/StylesheetParser.php | 4544 ----------------- scssphp/src/Serializer/SerializeResult.php | 37 - scssphp/src/Serializer/SerializeVisitor.php | 1591 ------ scssphp/src/Serializer/Serializer.php | 84 - scssphp/src/Serializer/SimpleStringBuffer.php | 44 - scssphp/src/Serializer/StringBuffer.php | 33 - scssphp/src/SourceSpan/FileSpan.php | 131 - scssphp/src/SourceSpan/SourceFile.php | 209 - scssphp/src/SourceSpan/SourceLocation.php | 68 - scssphp/src/StackTrace/Frame.php | 123 - scssphp/src/StackTrace/Trace.php | 55 - scssphp/src/Syntax.php | 47 - scssphp/src/Util/Character.php | 159 - scssphp/src/Util/Equatable.php | 21 - scssphp/src/Util/EquatableUtil.php | 93 - scssphp/src/Util/ErrorUtil.php | 62 - scssphp/src/Util/ListUtil.php | 154 - scssphp/src/Util/NumberUtil.php | 187 - scssphp/src/Util/ParserUtil.php | 69 - scssphp/src/Util/SpanUtil.php | 142 - scssphp/src/Util/StringUtil.php | 130 - .../src/Value/CalculationInterpolation.php | 45 - scssphp/src/Value/CalculationOperation.php | 89 - scssphp/src/Value/CalculationOperator.php | 48 - scssphp/src/Value/ColorFormat.php | 22 - scssphp/src/Value/ComplexSassNumber.php | 90 - scssphp/src/Value/ListSeparator.php | 24 - scssphp/src/Value/SassArgumentList.php | 69 - scssphp/src/Value/SassBoolean.php | 93 - scssphp/src/Value/SassCalculation.php | 527 -- scssphp/src/Value/SassColor.php | 515 -- scssphp/src/Value/SassFunction.php | 58 - scssphp/src/Value/SassList.php | 159 - scssphp/src/Value/SassMap.php | 130 - scssphp/src/Value/SassNull.php | 64 - scssphp/src/Value/SassNumber.php | 1107 ---- scssphp/src/Value/SassString.php | 243 - scssphp/src/Value/SingleUnitSassNumber.php | 303 -- scssphp/src/Value/SpanColorFormat.php | 36 - scssphp/src/Value/UnitlessSassNumber.php | 219 - scssphp/src/Value/Value.php | 746 --- scssphp/src/Visitor/AnySelectorVisitor.php | 112 - scssphp/src/Visitor/CssVisitor.php | 98 - scssphp/src/Visitor/EveryCssVisitor.php | 106 - scssphp/src/Visitor/ExpressionVisitor.php | 132 - scssphp/src/Visitor/ModifiableCssVisitor.php | 96 - scssphp/src/Visitor/SelectorVisitor.php | 90 - .../src/Visitor/StatementSearchVisitor.php | 266 - scssphp/src/Visitor/StatementVisitor.php | 174 - scssphp/src/Visitor/ValueVisitor.php | 77 - 188 files changed, 28637 deletions(-) delete mode 100644 scssphp/src/Ast/AstNode.php delete mode 100644 scssphp/src/Ast/Css/CssAtRule.php delete mode 100644 scssphp/src/Ast/Css/CssComment.php delete mode 100644 scssphp/src/Ast/Css/CssDeclaration.php delete mode 100644 scssphp/src/Ast/Css/CssImport.php delete mode 100644 scssphp/src/Ast/Css/CssKeyframeBlock.php delete mode 100644 scssphp/src/Ast/Css/CssMediaQuery.php delete mode 100644 scssphp/src/Ast/Css/CssMediaRule.php delete mode 100644 scssphp/src/Ast/Css/CssNode.php delete mode 100644 scssphp/src/Ast/Css/CssParentNode.php delete mode 100644 scssphp/src/Ast/Css/CssStyleRule.php delete mode 100644 scssphp/src/Ast/Css/CssStylesheet.php delete mode 100644 scssphp/src/Ast/Css/CssSupportsRule.php delete mode 100644 scssphp/src/Ast/Css/CssValue.php delete mode 100644 scssphp/src/Ast/Css/IsInvisibleVisitor.php delete mode 100644 scssphp/src/Ast/Css/ModifiableCssAtRule.php delete mode 100644 scssphp/src/Ast/Css/ModifiableCssComment.php delete mode 100644 scssphp/src/Ast/Css/ModifiableCssDeclaration.php delete mode 100644 scssphp/src/Ast/Css/ModifiableCssImport.php delete mode 100644 scssphp/src/Ast/Css/ModifiableCssKeyframeBlock.php delete mode 100644 scssphp/src/Ast/Css/ModifiableCssMediaRule.php delete mode 100644 scssphp/src/Ast/Css/ModifiableCssNode.php delete mode 100644 scssphp/src/Ast/Css/ModifiableCssParentNode.php delete mode 100644 scssphp/src/Ast/Css/ModifiableCssStyleRule.php delete mode 100644 scssphp/src/Ast/Css/ModifiableCssStylesheet.php delete mode 100644 scssphp/src/Ast/Css/ModifiableCssSupportsRule.php delete mode 100644 scssphp/src/Ast/Css/ModifiableCssValue.php delete mode 100644 scssphp/src/Ast/FakeAstNode.php delete mode 100644 scssphp/src/Ast/Sass/Argument.php delete mode 100644 scssphp/src/Ast/Sass/ArgumentDeclaration.php delete mode 100644 scssphp/src/Ast/Sass/ArgumentInvocation.php delete mode 100644 scssphp/src/Ast/Sass/AtRootQuery.php delete mode 100644 scssphp/src/Ast/Sass/CallableInvocation.php delete mode 100644 scssphp/src/Ast/Sass/ConfiguredVariable.php delete mode 100644 scssphp/src/Ast/Sass/Expression.php delete mode 100644 scssphp/src/Ast/Sass/Expression/BinaryOperationExpression.php delete mode 100644 scssphp/src/Ast/Sass/Expression/BinaryOperator.php delete mode 100644 scssphp/src/Ast/Sass/Expression/BooleanExpression.php delete mode 100644 scssphp/src/Ast/Sass/Expression/CalculationExpression.php delete mode 100644 scssphp/src/Ast/Sass/Expression/ColorExpression.php delete mode 100644 scssphp/src/Ast/Sass/Expression/FunctionExpression.php delete mode 100644 scssphp/src/Ast/Sass/Expression/IfExpression.php delete mode 100644 scssphp/src/Ast/Sass/Expression/InterpolatedFunctionExpression.php delete mode 100644 scssphp/src/Ast/Sass/Expression/ListExpression.php delete mode 100644 scssphp/src/Ast/Sass/Expression/MapExpression.php delete mode 100644 scssphp/src/Ast/Sass/Expression/NullExpression.php delete mode 100644 scssphp/src/Ast/Sass/Expression/NumberExpression.php delete mode 100644 scssphp/src/Ast/Sass/Expression/ParenthesizedExpression.php delete mode 100644 scssphp/src/Ast/Sass/Expression/SelectorExpression.php delete mode 100644 scssphp/src/Ast/Sass/Expression/StringExpression.php delete mode 100644 scssphp/src/Ast/Sass/Expression/SupportsExpression.php delete mode 100644 scssphp/src/Ast/Sass/Expression/UnaryOperationExpression.php delete mode 100644 scssphp/src/Ast/Sass/Expression/UnaryOperator.php delete mode 100644 scssphp/src/Ast/Sass/Expression/ValueExpression.php delete mode 100644 scssphp/src/Ast/Sass/Expression/VariableExpression.php delete mode 100644 scssphp/src/Ast/Sass/Import.php delete mode 100644 scssphp/src/Ast/Sass/Import/DynamicImport.php delete mode 100644 scssphp/src/Ast/Sass/Import/StaticImport.php delete mode 100644 scssphp/src/Ast/Sass/Interpolation.php delete mode 100644 scssphp/src/Ast/Sass/SassDeclaration.php delete mode 100644 scssphp/src/Ast/Sass/SassNode.php delete mode 100644 scssphp/src/Ast/Sass/SassReference.php delete mode 100644 scssphp/src/Ast/Sass/Statement.php delete mode 100644 scssphp/src/Ast/Sass/Statement/AtRootRule.php delete mode 100644 scssphp/src/Ast/Sass/Statement/AtRule.php delete mode 100644 scssphp/src/Ast/Sass/Statement/CallableDeclaration.php delete mode 100644 scssphp/src/Ast/Sass/Statement/ContentBlock.php delete mode 100644 scssphp/src/Ast/Sass/Statement/ContentRule.php delete mode 100644 scssphp/src/Ast/Sass/Statement/DebugRule.php delete mode 100644 scssphp/src/Ast/Sass/Statement/Declaration.php delete mode 100644 scssphp/src/Ast/Sass/Statement/EachRule.php delete mode 100644 scssphp/src/Ast/Sass/Statement/ElseClause.php delete mode 100644 scssphp/src/Ast/Sass/Statement/ErrorRule.php delete mode 100644 scssphp/src/Ast/Sass/Statement/ExtendRule.php delete mode 100644 scssphp/src/Ast/Sass/Statement/ForRule.php delete mode 100644 scssphp/src/Ast/Sass/Statement/FunctionRule.php delete mode 100644 scssphp/src/Ast/Sass/Statement/HasContentVisitor.php delete mode 100644 scssphp/src/Ast/Sass/Statement/IfClause.php delete mode 100644 scssphp/src/Ast/Sass/Statement/IfRule.php delete mode 100644 scssphp/src/Ast/Sass/Statement/IfRuleClause.php delete mode 100644 scssphp/src/Ast/Sass/Statement/ImportRule.php delete mode 100644 scssphp/src/Ast/Sass/Statement/IncludeRule.php delete mode 100644 scssphp/src/Ast/Sass/Statement/LoudComment.php delete mode 100644 scssphp/src/Ast/Sass/Statement/MediaRule.php delete mode 100644 scssphp/src/Ast/Sass/Statement/MixinRule.php delete mode 100644 scssphp/src/Ast/Sass/Statement/ParentStatement.php delete mode 100644 scssphp/src/Ast/Sass/Statement/ReturnRule.php delete mode 100644 scssphp/src/Ast/Sass/Statement/SilentComment.php delete mode 100644 scssphp/src/Ast/Sass/Statement/StyleRule.php delete mode 100644 scssphp/src/Ast/Sass/Statement/Stylesheet.php delete mode 100644 scssphp/src/Ast/Sass/Statement/SupportsRule.php delete mode 100644 scssphp/src/Ast/Sass/Statement/VariableDeclaration.php delete mode 100644 scssphp/src/Ast/Sass/Statement/WarnRule.php delete mode 100644 scssphp/src/Ast/Sass/Statement/WhileRule.php delete mode 100644 scssphp/src/Ast/Sass/SupportsCondition.php delete mode 100644 scssphp/src/Ast/Sass/SupportsCondition/SupportsAnything.php delete mode 100644 scssphp/src/Ast/Sass/SupportsCondition/SupportsDeclaration.php delete mode 100644 scssphp/src/Ast/Sass/SupportsCondition/SupportsFunction.php delete mode 100644 scssphp/src/Ast/Sass/SupportsCondition/SupportsInterpolation.php delete mode 100644 scssphp/src/Ast/Sass/SupportsCondition/SupportsNegation.php delete mode 100644 scssphp/src/Ast/Sass/SupportsCondition/SupportsOperation.php delete mode 100644 scssphp/src/Ast/Selector/AttributeOperator.php delete mode 100644 scssphp/src/Ast/Selector/AttributeSelector.php delete mode 100644 scssphp/src/Ast/Selector/ClassSelector.php delete mode 100644 scssphp/src/Ast/Selector/Combinator.php delete mode 100644 scssphp/src/Ast/Selector/ComplexSelector.php delete mode 100644 scssphp/src/Ast/Selector/ComplexSelectorComponent.php delete mode 100644 scssphp/src/Ast/Selector/CompoundSelector.php delete mode 100644 scssphp/src/Ast/Selector/IDSelector.php delete mode 100644 scssphp/src/Ast/Selector/IsBogusVisitor.php delete mode 100644 scssphp/src/Ast/Selector/IsInvisibleVisitor.php delete mode 100644 scssphp/src/Ast/Selector/IsUselessVisitor.php delete mode 100644 scssphp/src/Ast/Selector/ParentSelector.php delete mode 100644 scssphp/src/Ast/Selector/PlaceholderSelector.php delete mode 100644 scssphp/src/Ast/Selector/PseudoSelector.php delete mode 100644 scssphp/src/Ast/Selector/QualifiedName.php delete mode 100644 scssphp/src/Ast/Selector/Selector.php delete mode 100644 scssphp/src/Ast/Selector/SelectorList.php delete mode 100644 scssphp/src/Ast/Selector/SimpleSelector.php delete mode 100644 scssphp/src/Ast/Selector/TypeSelector.php delete mode 100644 scssphp/src/Ast/Selector/UniversalSelector.php delete mode 100644 scssphp/src/Collection/Map.php delete mode 100644 scssphp/src/Exception/SassFormatException.php delete mode 100644 scssphp/src/Exception/SassRuntimeException.php delete mode 100644 scssphp/src/Extend/ExtendUtil.php delete mode 100644 scssphp/src/Logger/AdaptingLogger.php delete mode 100644 scssphp/src/Logger/LocationAwareLoggerInterface.php delete mode 100644 scssphp/src/Parser/AtRootQueryParser.php delete mode 100644 scssphp/src/Parser/CssParser.php delete mode 100644 scssphp/src/Parser/FormatException.php delete mode 100644 scssphp/src/Parser/InterpolationBuffer.php delete mode 100644 scssphp/src/Parser/KeyframeSelectorParser.php delete mode 100644 scssphp/src/Parser/LineScanner.php delete mode 100644 scssphp/src/Parser/MediaQueryParser.php delete mode 100644 scssphp/src/Parser/Parser.php delete mode 100644 scssphp/src/Parser/ScssParser.php delete mode 100644 scssphp/src/Parser/SelectorParser.php delete mode 100644 scssphp/src/Parser/StringScanner.php delete mode 100644 scssphp/src/Parser/StylesheetParser.php delete mode 100644 scssphp/src/Serializer/SerializeResult.php delete mode 100644 scssphp/src/Serializer/SerializeVisitor.php delete mode 100644 scssphp/src/Serializer/Serializer.php delete mode 100644 scssphp/src/Serializer/SimpleStringBuffer.php delete mode 100644 scssphp/src/Serializer/StringBuffer.php delete mode 100644 scssphp/src/SourceSpan/FileSpan.php delete mode 100644 scssphp/src/SourceSpan/SourceFile.php delete mode 100644 scssphp/src/SourceSpan/SourceLocation.php delete mode 100644 scssphp/src/StackTrace/Frame.php delete mode 100644 scssphp/src/StackTrace/Trace.php delete mode 100644 scssphp/src/Syntax.php delete mode 100644 scssphp/src/Util/Character.php delete mode 100644 scssphp/src/Util/Equatable.php delete mode 100644 scssphp/src/Util/EquatableUtil.php delete mode 100644 scssphp/src/Util/ErrorUtil.php delete mode 100644 scssphp/src/Util/ListUtil.php delete mode 100644 scssphp/src/Util/NumberUtil.php delete mode 100644 scssphp/src/Util/ParserUtil.php delete mode 100644 scssphp/src/Util/SpanUtil.php delete mode 100644 scssphp/src/Util/StringUtil.php delete mode 100644 scssphp/src/Value/CalculationInterpolation.php delete mode 100644 scssphp/src/Value/CalculationOperation.php delete mode 100644 scssphp/src/Value/CalculationOperator.php delete mode 100644 scssphp/src/Value/ColorFormat.php delete mode 100644 scssphp/src/Value/ComplexSassNumber.php delete mode 100644 scssphp/src/Value/ListSeparator.php delete mode 100644 scssphp/src/Value/SassArgumentList.php delete mode 100644 scssphp/src/Value/SassBoolean.php delete mode 100644 scssphp/src/Value/SassCalculation.php delete mode 100644 scssphp/src/Value/SassColor.php delete mode 100644 scssphp/src/Value/SassFunction.php delete mode 100644 scssphp/src/Value/SassList.php delete mode 100644 scssphp/src/Value/SassMap.php delete mode 100644 scssphp/src/Value/SassNull.php delete mode 100644 scssphp/src/Value/SassNumber.php delete mode 100644 scssphp/src/Value/SassString.php delete mode 100644 scssphp/src/Value/SingleUnitSassNumber.php delete mode 100644 scssphp/src/Value/SpanColorFormat.php delete mode 100644 scssphp/src/Value/UnitlessSassNumber.php delete mode 100644 scssphp/src/Value/Value.php delete mode 100644 scssphp/src/Visitor/AnySelectorVisitor.php delete mode 100644 scssphp/src/Visitor/CssVisitor.php delete mode 100644 scssphp/src/Visitor/EveryCssVisitor.php delete mode 100644 scssphp/src/Visitor/ExpressionVisitor.php delete mode 100644 scssphp/src/Visitor/ModifiableCssVisitor.php delete mode 100644 scssphp/src/Visitor/SelectorVisitor.php delete mode 100644 scssphp/src/Visitor/StatementSearchVisitor.php delete mode 100644 scssphp/src/Visitor/StatementVisitor.php delete mode 100644 scssphp/src/Visitor/ValueVisitor.php diff --git a/scssphp/src/Ast/AstNode.php b/scssphp/src/Ast/AstNode.php deleted file mode 100644 index ec15547..0000000 --- a/scssphp/src/Ast/AstNode.php +++ /dev/null @@ -1,27 +0,0 @@ - - */ - public function getName(): CssValue; - - /** - * The value of this rule. - * - * @return CssValue|null - */ - public function getValue(): ?CssValue; -} diff --git a/scssphp/src/Ast/Css/CssComment.php b/scssphp/src/Ast/Css/CssComment.php deleted file mode 100644 index 27a8285..0000000 --- a/scssphp/src/Ast/Css/CssComment.php +++ /dev/null @@ -1,34 +0,0 @@ - - */ - public function getName(): CssValue; - - /** - * The value of this declaration. - * - * @return CssValue - */ - public function getValue(): CssValue; - - /** - * The span for {@see getValue} that should be emitted to the source map. - * - * When the declaration's expression is just a variable, this is the span - * where that variable was declared whereas `$this->getValue()->getSpan()` is the span where - * the variable was used. Otherwise, this is identical to `$this->getValue()->getSpan()`. - */ - public function getValueSpanForMap(): FileSpan; - - /** - * Returns whether this is a CSS Custom Property declaration. - */ - public function isCustomProperty(): bool; - - /** - * Whether this was originally parsed as a custom property declaration, as - * opposed to using something like `#{--foo}: ...` to cause it to be parsed - * as a normal Sass declaration. - * - * If this is `true`, {@see isCustomProperty} will also be `true` and {@see getValue} will - * contain a {@see SassString}. - */ - public function isParsedAsCustomProperty(): bool; -} diff --git a/scssphp/src/Ast/Css/CssImport.php b/scssphp/src/Ast/Css/CssImport.php deleted file mode 100644 index 59562ec..0000000 --- a/scssphp/src/Ast/Css/CssImport.php +++ /dev/null @@ -1,37 +0,0 @@ - - */ - public function getUrl(): CssValue; - - /** - * The modifiers (such as media or supports queries) attached to this import. - * - * @return CssValue|null - */ - public function getModifiers(): ?CssValue; -} diff --git a/scssphp/src/Ast/Css/CssKeyframeBlock.php b/scssphp/src/Ast/Css/CssKeyframeBlock.php deleted file mode 100644 index 8a4f3f9..0000000 --- a/scssphp/src/Ast/Css/CssKeyframeBlock.php +++ /dev/null @@ -1,30 +0,0 @@ -> - */ - public function getSelector(): CssValue; -} diff --git a/scssphp/src/Ast/Css/CssMediaQuery.php b/scssphp/src/Ast/Css/CssMediaQuery.php deleted file mode 100644 index 0b3a2ff..0000000 --- a/scssphp/src/Ast/Css/CssMediaQuery.php +++ /dev/null @@ -1,258 +0,0 @@ -`] production. - * - * [``]: https://drafts.csswg.org/mediaqueries-4/#typedef-media-in-parens - * - * @var list - * @readonly - */ - private $conditions; - - /** - * Parses a media query from $contents. - * - * If passed, $url is the name of the file from which $contents comes. - * - * @return list - * - * @throws SassFormatException if parsing fails - */ - public static function parseList(string $contents, ?LoggerInterface $logger = null, ?string $url = null): array - { - return (new MediaQueryParser($contents, $logger, $url))->parse(); - } - - /** - * @param list $conditions - */ - private function __construct(array $conditions = [], bool $conjunction = true, ?string $type = null, ?string $modifier = null) - { - $this->modifier = $modifier; - $this->type = $type; - $this->conditions = $conditions; - $this->conjunction = $conjunction; - } - - /** - * Creates a media query specifies a type and, optionally, conditions. - * - * This always sets {@see $conjunction} to `true`. - * - * @param list $conditions - */ - public static function type(?string $type, ?string $modifier = null, array $conditions = []): CssMediaQuery - { - return new CssMediaQuery($conditions, true, $type, $modifier); - } - - /** - * Creates a media query that matches $conditions according to - * $conjunction. - * - * The $conjunction argument may not be null if $conditions is longer than - * a single element. - * - * @param list $conditions - */ - public static function condition(array $conditions, ?bool $conjunction = null): CssMediaQuery - { - if (\count($conditions) > 1 && $conjunction === null) { - throw new \InvalidArgumentException('If conditions is longer than one element, conjunction may not be null.'); - } - - return new CssMediaQuery($conditions, $conjunction ?? true); - } - - public function getModifier(): ?string - { - return $this->modifier; - } - - public function getType(): ?string - { - return $this->type; - } - - public function isConjunction(): bool - { - return $this->conjunction; - } - - /** - * @return list - */ - public function getConditions(): array - { - return $this->conditions; - } - - /** - * Whether this media query matches all media types. - */ - public function matchesAllTypes(): bool - { - return $this->type === null || strtolower($this->type) === 'all'; - } - - /** - * Merges this with $other to return a query that matches the intersection - * of both inputs. - * - * @return CssMediaQuery|string - * @phpstan-return CssMediaQuery|CssMediaQuery::* - */ - public function merge(CssMediaQuery $other) - { - if (!$this->conjunction || !$other->conjunction) { - return self::MERGE_RESULT_UNREPRESENTABLE; - } - - $ourModifier = $this->modifier !== null ? strtolower($this->modifier) : null; - $ourType = $this->type !== null ? strtolower($this->type) : null; - $theirModifier = $other->modifier !== null ? strtolower($other->modifier) : null; - $theirType = $other->type !== null ? strtolower($other->type) : null; - - if ($ourType === null && $theirType === null) { - return self::condition(array_merge($this->conditions, $other->conditions), true); - } - - if (($ourModifier === 'not') !== ($theirModifier === 'not')) { - if ($ourType === $theirType) { - $negativeConditions = $ourModifier === 'not' ? $this->conditions : $other->conditions; - $positiveConditions = $ourModifier === 'not' ? $other->conditions : $this->conditions; - - // If the negative conditions are a subset of the positive conditions, the - // query is empty. For example, `not screen and (color)` has no - // intersection with `screen and (color) and (grid)`. - // - // However, `not screen and (color)` *does* intersect with `screen and - // (grid)`, because it means `not (screen and (color))` and so it allows - // a screen with no color but with a grid. - if (empty(array_diff($negativeConditions, $positiveConditions))) { - return self::MERGE_RESULT_EMPTY; - } - - return self::MERGE_RESULT_UNREPRESENTABLE; - } - - if ($this->matchesAllTypes() || $other->matchesAllTypes()) { - return self::MERGE_RESULT_UNREPRESENTABLE; - } - - if ($ourModifier === 'not') { - $modifier = $theirModifier; - $type = $theirType; - $conditions = $other->conditions; - } else { - $modifier = $ourModifier; - $type = $ourType; - $conditions = $this->conditions; - } - } elseif ($ourModifier === 'not') { - // CSS has no way of representing "neither screen nor print". - if ($ourType !== $theirType) { - return self::MERGE_RESULT_UNREPRESENTABLE; - } - - $moreConditions = \count($this->conditions) > \count($other->conditions) ? $this->conditions : $other->conditions; - $fewerConditions = \count($this->conditions) > \count($other->conditions) ? $other->conditions : $this->conditions; - - // If one set of features is a superset of the other, use those features - // because they're strictly narrower. - if (empty(array_diff($fewerConditions, $moreConditions))) { - $modifier = $ourModifier; // "not" - $type = $ourType; - $conditions = $moreConditions; - } else { - // Otherwise, there's no way to represent the intersection. - return self::MERGE_RESULT_UNREPRESENTABLE; - } - } elseif ($this->matchesAllTypes()) { - $modifier = $theirModifier; - // Omit the type if either input query did, since that indicates that they - // aren't targeting a browser that requires "all and". - $type = $other->matchesAllTypes() && $ourType === null ? null : $theirType; - $conditions = array_merge($this->conditions, $other->conditions); - } elseif ($other->matchesAllTypes()) { - $modifier = $ourModifier; - $type = $ourType; - $conditions = array_merge($this->conditions, $other->conditions); - } elseif ($ourType !== $theirType) { - return self::MERGE_RESULT_EMPTY; - } else { - $modifier = $ourModifier ?? $theirModifier; - $type = $ourType; - $conditions = array_merge($this->conditions, $other->conditions); - } - - return CssMediaQuery::type( - $type === $ourType ? $this->type : $other->type, - $modifier === $ourModifier ? $this->modifier : $other->modifier, - $conditions - ); - } -} diff --git a/scssphp/src/Ast/Css/CssMediaRule.php b/scssphp/src/Ast/Css/CssMediaRule.php deleted file mode 100644 index f1287c3..0000000 --- a/scssphp/src/Ast/Css/CssMediaRule.php +++ /dev/null @@ -1,30 +0,0 @@ - - */ - public function getQueries(): array; -} diff --git a/scssphp/src/Ast/Css/CssNode.php b/scssphp/src/Ast/Css/CssNode.php deleted file mode 100644 index 9a94689..0000000 --- a/scssphp/src/Ast/Css/CssNode.php +++ /dev/null @@ -1,63 +0,0 @@ - $visitor - * - * @return T - */ - public function accept($visitor); - - /** - * Whether this is invisible and won't be emitted to the compiled stylesheet. - * - * Note that this doesn't consider nodes that contain loud comments to be - * invisible even though they're omitted in compressed mode. - */ - public function isInvisible(): bool; - - /** - * Whether this node would be invisible even if style rule selectors within it - * didn't have bogus combinators. - * - * Note that this doesn't consider nodes that contain loud comments to be - * invisible even though they're omitted in compressed mode. - */ - public function isInvisibleOtherThanBogusCombinators(): bool; - - /** - * Whether this node will be invisible when loud comments are stripped. - */ - public function isInvisibleHidingComments(): bool; -} diff --git a/scssphp/src/Ast/Css/CssParentNode.php b/scssphp/src/Ast/Css/CssParentNode.php deleted file mode 100644 index 3e4637a..0000000 --- a/scssphp/src/Ast/Css/CssParentNode.php +++ /dev/null @@ -1,37 +0,0 @@ - - */ - public function getChildren(): array; - - /** - * Whether the rule has no children and should be emitted without curly - * braces. - * - * This implies `children.isEmpty`, but the reverse is not true—for a rule - * like `@foo {}`, {@see getChildren} is empty but {@see isChildless} is `false`. - */ - public function isChildless(): bool; -} diff --git a/scssphp/src/Ast/Css/CssStyleRule.php b/scssphp/src/Ast/Css/CssStyleRule.php deleted file mode 100644 index be51fa4..0000000 --- a/scssphp/src/Ast/Css/CssStyleRule.php +++ /dev/null @@ -1,39 +0,0 @@ - - */ - public function getSelector(): CssValue; - - /** - * The selector for this rule, before any extensions were applied. - */ - public function getOriginalSelector(): SelectorList; -} diff --git a/scssphp/src/Ast/Css/CssStylesheet.php b/scssphp/src/Ast/Css/CssStylesheet.php deleted file mode 100644 index 6e69e39..0000000 --- a/scssphp/src/Ast/Css/CssStylesheet.php +++ /dev/null @@ -1,24 +0,0 @@ - - */ - public function getCondition(): CssValue; -} diff --git a/scssphp/src/Ast/Css/CssValue.php b/scssphp/src/Ast/Css/CssValue.php deleted file mode 100644 index 76e93bf..0000000 --- a/scssphp/src/Ast/Css/CssValue.php +++ /dev/null @@ -1,71 +0,0 @@ -value = $value; - $this->span = $span; - } - - /** - * @return T - */ - public function getValue() - { - return $this->value; - } - - public function getSpan(): FileSpan - { - return $this->span; - } - - public function __toString(): string - { - if (\is_array($this->value)) { - return implode($this->value); - } - - return (string) $this->value; - } -} diff --git a/scssphp/src/Ast/Css/IsInvisibleVisitor.php b/scssphp/src/Ast/Css/IsInvisibleVisitor.php deleted file mode 100644 index ca7a343..0000000 --- a/scssphp/src/Ast/Css/IsInvisibleVisitor.php +++ /dev/null @@ -1,63 +0,0 @@ -includeBogus = $includeBogus; - $this->includeComments = $includeComments; - } - - public function visitCssAtRule($node): bool - { - // An unknown at-rule is never invisible. Because we don't know the semantics - // of unknown rules, we can't guarantee that (for example) `@foo {}` isn't - // meaningful. - return false; - } - - public function visitCssComment($node): bool - { - return $this->includeComments && !$node->isPreserved(); - } - - public function visitCssStyleRule($node): bool - { - return ($this->includeBogus ? $node->getSelector()->getValue()->isInvisible() : $node->getSelector()->getValue()->isInvisibleOtherThanBogusCombinators()) || parent::visitCssStyleRule($node); - } -} diff --git a/scssphp/src/Ast/Css/ModifiableCssAtRule.php b/scssphp/src/Ast/Css/ModifiableCssAtRule.php deleted file mode 100644 index 0119a39..0000000 --- a/scssphp/src/Ast/Css/ModifiableCssAtRule.php +++ /dev/null @@ -1,103 +0,0 @@ - - */ - private $name; - - /** - * @var CssValue|null - */ - private $value; - - /** - * @var bool - * @readonly - */ - private $childless; - - /** - * @var FileSpan - * @readonly - */ - private $span; - - /** - * @param CssValue $name - * @param CssValue|null $value - * @param bool $childless - * @param FileSpan $span - */ - public function __construct(CssValue $name, FileSpan $span, bool $childless = false, ?CssValue $value = null) - { - parent::__construct(); - - $this->name = $name; - $this->value = $value; - $this->childless = $childless; - $this->span = $span; - } - - public function getName(): CssValue - { - return $this->name; - } - - public function getValue(): ?CssValue - { - return $this->value; - } - - public function isChildless(): bool - { - return $this->childless; - } - - public function getSpan(): FileSpan - { - return $this->span; - } - - public function accept($visitor) - { - return $visitor->visitCssAtRule($this); - } - - /** - * @phpstan-return ModifiableCssAtRule - */ - public function copyWithoutChildren(): ModifiableCssParentNode - { - return new ModifiableCssAtRule($this->name, $this->span, $this->childless, $this->value); - } - - public function addChild(ModifiableCssNode $child): void - { - if ($this->childless) { - throw new \LogicException('Cannot add a child in a childless at-rule.'); - } - - parent::addChild($child); - } -} diff --git a/scssphp/src/Ast/Css/ModifiableCssComment.php b/scssphp/src/Ast/Css/ModifiableCssComment.php deleted file mode 100644 index 68331ec..0000000 --- a/scssphp/src/Ast/Css/ModifiableCssComment.php +++ /dev/null @@ -1,61 +0,0 @@ -text = $text; - $this->span = $span; - } - - public function getText(): string - { - return $this->text; - } - - public function getSpan(): FileSpan - { - return $this->span; - } - - public function isPreserved(): bool - { - return $this->text[2] === '!'; - } - - public function accept($visitor) - { - return $visitor->visitCssComment($this); - } -} diff --git a/scssphp/src/Ast/Css/ModifiableCssDeclaration.php b/scssphp/src/Ast/Css/ModifiableCssDeclaration.php deleted file mode 100644 index ba73cd1..0000000 --- a/scssphp/src/Ast/Css/ModifiableCssDeclaration.php +++ /dev/null @@ -1,115 +0,0 @@ - - * @readonly - */ - private $name; - - /** - * @var CssValue - * @readonly - */ - private $value; - - /** - * @var bool - * @readonly - */ - private $parsedAsCustomProperty; - - /** - * @var FileSpan - * @readonly - */ - private $valueSpanForMap; - - /** - * @var FileSpan - * @readonly - */ - private $span; - - /** - * @param CssValue $name - * @param CssValue $value - * @param bool $parsedAsCustomProperty - * @param FileSpan $valueSpanForMap - * @param FileSpan $span - */ - public function __construct(CssValue $name, CssValue $value, FileSpan $span, bool $parsedAsCustomProperty, ?FileSpan $valueSpanForMap = null) { - $this->name = $name; - $this->value = $value; - $this->parsedAsCustomProperty = $parsedAsCustomProperty; - $this->valueSpanForMap = $valueSpanForMap ?? $value->getSpan(); - $this->span = $span; - - if ($parsedAsCustomProperty) { - if (!$this->isCustomProperty()) { - throw new \InvalidArgumentException('parsedAsCustomProperty must be false if name doesn\'t begin with "--".'); - } - - if (!$value->getValue() instanceof SassString) { - throw new \InvalidArgumentException(sprintf('If parsedAsCustomProperty is true, value must contain a SassString (was %s).', get_class($value->getValue()))); - } - } - } - - public function getName(): CssValue - { - return $this->name; - } - - public function getValue(): CssValue - { - return $this->value; - } - - public function isParsedAsCustomProperty(): bool - { - return $this->parsedAsCustomProperty; - } - - public function getValueSpanForMap(): FileSpan - { - return $this->valueSpanForMap; - } - - public function getSpan(): FileSpan - { - return $this->span; - } - - public function isCustomProperty(): bool - { - return 0 === strpos($this->name->getValue(), '--'); - } - - public function accept($visitor) - { - return $visitor->visitCssDeclaration($this); - } -} diff --git a/scssphp/src/Ast/Css/ModifiableCssImport.php b/scssphp/src/Ast/Css/ModifiableCssImport.php deleted file mode 100644 index 52899c0..0000000 --- a/scssphp/src/Ast/Css/ModifiableCssImport.php +++ /dev/null @@ -1,77 +0,0 @@ - - * @readonly - */ - private $url; - - /** - * @var CssValue|null - * @readonly - */ - private $modifiers; - - /** - * @var FileSpan - * @readonly - */ - private $span; - - /** - * @param CssValue $url - * @param FileSpan $span - * @param CssValue|null $modifiers - */ - public function __construct(CssValue $url, FileSpan $span, ?CssValue $modifiers = null) - { - $this->url = $url; - $this->modifiers = $modifiers; - $this->span = $span; - } - - public function getUrl(): CssValue - { - return $this->url; - } - - public function getModifiers(): ?CssValue - { - return $this->modifiers; - } - - public function getSpan(): FileSpan - { - return $this->span; - } - - public function accept($visitor) - { - return $visitor->visitCssImport($this); - } -} diff --git a/scssphp/src/Ast/Css/ModifiableCssKeyframeBlock.php b/scssphp/src/Ast/Css/ModifiableCssKeyframeBlock.php deleted file mode 100644 index ba4c8cc..0000000 --- a/scssphp/src/Ast/Css/ModifiableCssKeyframeBlock.php +++ /dev/null @@ -1,69 +0,0 @@ -> - * @readonly - */ - private $selector; - - /** - * @var FileSpan - * @readonly - */ - private $span; - - /** - * @param CssValue> $selector - * @param FileSpan $span - */ - public function __construct(CssValue $selector, FileSpan $span) - { - parent::__construct(); - $this->selector = $selector; - $this->span = $span; - } - - public function getSelector(): CssValue - { - return $this->selector; - } - - public function getSpan(): FileSpan - { - return $this->span; - } - - public function accept($visitor) - { - return $visitor->visitCssKeyframeBlock($this); - } - - /** - * @phpstan-return ModifiableCssKeyframeBlock - */ - public function copyWithoutChildren(): ModifiableCssParentNode - { - return new ModifiableCssKeyframeBlock($this->selector, $this->span); - } -} diff --git a/scssphp/src/Ast/Css/ModifiableCssMediaRule.php b/scssphp/src/Ast/Css/ModifiableCssMediaRule.php deleted file mode 100644 index 99007ec..0000000 --- a/scssphp/src/Ast/Css/ModifiableCssMediaRule.php +++ /dev/null @@ -1,65 +0,0 @@ - - */ - private $queries; - - /** - * @var FileSpan - * @readonly - */ - private $span; - - /** - * @param CssMediaQuery[] $queries - * @param FileSpan $span - */ - public function __construct(array $queries, FileSpan $span) - { - parent::__construct(); - $this->queries = $queries; - $this->span = $span; - } - - public function getQueries(): array - { - return $this->queries; - } - - public function getSpan(): FileSpan - { - return $this->span; - } - - public function accept($visitor) - { - return $visitor->visitCssMediaRule($this); - } - - public function copyWithoutChildren(): ModifiableCssParentNode - { - return new ModifiableCssMediaRule($this->queries, $this->span); - } -} diff --git a/scssphp/src/Ast/Css/ModifiableCssNode.php b/scssphp/src/Ast/Css/ModifiableCssNode.php deleted file mode 100644 index c7dbd1c..0000000 --- a/scssphp/src/Ast/Css/ModifiableCssNode.php +++ /dev/null @@ -1,151 +0,0 @@ -parent; - } - - protected function setParent(ModifiableCssParentNode $parent, int $indexInParent): void - { - $this->parent = $parent; - $this->indexInParent = $indexInParent; - } - - public function isGroupEnd(): bool - { - return $this->groupEnd; - } - - public function setGroupEnd(bool $groupEnd): void - { - $this->groupEnd = $groupEnd; - } - - /** - * Whether this node has a visible sibling after it. - */ - public function hasFollowingSibling(): bool - { - $parent = $this->parent; - - if ($parent === null) { - return false; - } - - assert($this->indexInParent !== null); - $siblings = $parent->getChildren(); - - for ($i = $this->indexInParent + 1; $i < \count($siblings); $i++) { - $sibling = $siblings[$i]; - - if (!$sibling->isInvisible()) { - return true; - } - } - - return false; - } - - public function isInvisible(): bool - { - return $this->accept(new IsInvisibleVisitor(true, false)); - } - - public function isInvisibleOtherThanBogusCombinators(): bool - { - return $this->accept(new IsInvisibleVisitor(false, false)); - } - - public function isInvisibleHidingComments(): bool - { - return $this->accept(new IsInvisibleVisitor(true, true)); - } - - /** - * Calls the appropriate visit method on $visitor. - * - * @template T - * - * @param ModifiableCssVisitor $visitor - * - * @return T - */ - abstract public function accept($visitor); - - /** - * Removes $this from {@see parent}'s child list. - * - * @throws \LogicException if {@see parent} is `null`. - */ - public function remove(): void - { - $parent = $this->parent; - - if ($parent === null) { - throw new \LogicException("Can't remove a node without a parent."); - } - - assert($this->indexInParent !== null); - - $parent->removeChildAt($this->indexInParent); - $children = $parent->getChildren(); - - for ($i = $this->indexInParent; $i < \count($children); $i++) { - $child = $children[$i]; - assert($child->indexInParent !== null); - $child->indexInParent = $child->indexInParent - 1; - } - $this->parent = null; - $this->indexInParent = null; - } - - public function __toString(): string - { - return Serializer::serialize($this, true)->getCss(); - } -} diff --git a/scssphp/src/Ast/Css/ModifiableCssParentNode.php b/scssphp/src/Ast/Css/ModifiableCssParentNode.php deleted file mode 100644 index 306eecb..0000000 --- a/scssphp/src/Ast/Css/ModifiableCssParentNode.php +++ /dev/null @@ -1,69 +0,0 @@ - - */ - private $children; - - /** - * @param list $children - */ - public function __construct(array $children = []) - { - $this->children = $children; - } - - /** - * @return list - */ - public function getChildren(): array - { - return $this->children; - } - - public function isChildless(): bool - { - return false; - } - - /** - * Returns a copy of $this with an empty {@see children} list. - * - * This is *not* a deep copy. If other parts of this node are modifiable, - * they are shared between the new and old nodes. - */ - abstract public function copyWithoutChildren(): ModifiableCssParentNode; - - public function addChild(ModifiableCssNode $child): void - { - $child->setParent($this, \count($this->children)); - $this->children[] = $child; - } - - /** - * @internal - */ - public function removeChildAt(int $index): void - { - array_splice($this->children, $index, 1); - } -} diff --git a/scssphp/src/Ast/Css/ModifiableCssStyleRule.php b/scssphp/src/Ast/Css/ModifiableCssStyleRule.php deleted file mode 100644 index 739bdd2..0000000 --- a/scssphp/src/Ast/Css/ModifiableCssStyleRule.php +++ /dev/null @@ -1,86 +0,0 @@ - - * @readonly - */ - private $selector; - - /** - * @var SelectorList - * @readonly - */ - private $originalSelector; - - /** - * @var FileSpan - * @readonly - */ - private $span; - - /** - * @param ModifiableCssValue $selector - * @param FileSpan $span - * @param SelectorList|null $originalSelector - */ - public function __construct(ModifiableCssValue $selector, FileSpan $span, ?SelectorList $originalSelector = null) - { - parent::__construct(); - $this->selector = $selector; - $this->originalSelector = $originalSelector ?? $selector->getValue(); - $this->span = $span; - } - - /** - * @phpstan-return ModifiableCssValue - */ - public function getSelector(): CssValue - { - return $this->selector; - } - - public function getOriginalSelector(): SelectorList - { - return $this->originalSelector; - } - - public function getSpan(): FileSpan - { - return $this->span; - } - - public function accept($visitor) - { - return $visitor->visitCssStyleRule($this); - } - - /** - * @phpstan-return ModifiableCssStyleRule - */ - public function copyWithoutChildren(): ModifiableCssParentNode - { - return new ModifiableCssStyleRule($this->selector, $this->span, $this->originalSelector); - } -} diff --git a/scssphp/src/Ast/Css/ModifiableCssStylesheet.php b/scssphp/src/Ast/Css/ModifiableCssStylesheet.php deleted file mode 100644 index 02c4355..0000000 --- a/scssphp/src/Ast/Css/ModifiableCssStylesheet.php +++ /dev/null @@ -1,57 +0,0 @@ - $children - */ - public function __construct(FileSpan $span, array $children = []) - { - parent::__construct($children); - $this->span = $span; - } - - public function getSpan(): FileSpan - { - return $this->span; - } - - public function accept($visitor) - { - return $visitor->visitCssStylesheet($this); - } - - /** - * @phpstan-return ModifiableCssStylesheet - */ - public function copyWithoutChildren(): ModifiableCssParentNode - { - return new ModifiableCssStylesheet($this->span); - } -} diff --git a/scssphp/src/Ast/Css/ModifiableCssSupportsRule.php b/scssphp/src/Ast/Css/ModifiableCssSupportsRule.php deleted file mode 100644 index 2d8b078..0000000 --- a/scssphp/src/Ast/Css/ModifiableCssSupportsRule.php +++ /dev/null @@ -1,69 +0,0 @@ - - * @readonly - */ - private $condition; - - /** - * @var FileSpan - * @readonly - */ - private $span; - - /** - * @param CssValue $condition - * @param FileSpan $span - */ - public function __construct(CssValue $condition, FileSpan $span) - { - parent::__construct(); - $this->condition = $condition; - $this->span = $span; - } - - public function getCondition(): CssValue - { - return $this->condition; - } - - public function getSpan(): FileSpan - { - return $this->span; - } - - public function accept($visitor) - { - return $visitor->visitCssSupportsRule($this); - } - - /** - * @phpstan-return ModifiableCssSupportsRule - */ - public function copyWithoutChildren(): ModifiableCssParentNode - { - return new ModifiableCssSupportsRule($this->condition, $this->span); - } -} diff --git a/scssphp/src/Ast/Css/ModifiableCssValue.php b/scssphp/src/Ast/Css/ModifiableCssValue.php deleted file mode 100644 index e90491a..0000000 --- a/scssphp/src/Ast/Css/ModifiableCssValue.php +++ /dev/null @@ -1,32 +0,0 @@ - - * - * @internal - */ -final class ModifiableCssValue extends CssValue -{ - /** - * @param T $value - */ - public function setValue($value): void - { - $this->value = $value; - } -} diff --git a/scssphp/src/Ast/FakeAstNode.php b/scssphp/src/Ast/FakeAstNode.php deleted file mode 100644 index cdb7534..0000000 --- a/scssphp/src/Ast/FakeAstNode.php +++ /dev/null @@ -1,47 +0,0 @@ -callback = $callback; - } - - public function getSpan(): FileSpan - { - return ($this->callback)(); - } - - public function __toString(): string - { - return ''; - } -} diff --git a/scssphp/src/Ast/Sass/Argument.php b/scssphp/src/Ast/Sass/Argument.php deleted file mode 100644 index da28693..0000000 --- a/scssphp/src/Ast/Sass/Argument.php +++ /dev/null @@ -1,99 +0,0 @@ -name = $name; - $this->defaultValue = $defaultValue; - $this->span = $span; - } - - public function getName(): string - { - return $this->name; - } - - /** - * The variable name as written in the document, without underscores - * converted to hyphens and including the leading `$`. - * - * This isn't particularly efficient, and should only be used for error - * messages. - */ - public function getOriginalName(): string - { - if ($this->defaultValue === null) { - return $this->span->getText(); - } - - return Util::declarationName($this->span); - } - - public function getNameSpan(): FileSpan - { - if ($this->defaultValue === null) { - return $this->span; - } - - return SpanUtil::initialIdentifier($this->span, 1); - } - - public function getDefaultValue(): ?Expression - { - return $this->defaultValue; - } - - public function getSpan(): FileSpan - { - return $this->span; - } - - public function __toString(): string - { - if ($this->defaultValue === null) { - return $this->name; - } - - return $this->name . ': ' . $this->defaultValue; - } -} diff --git a/scssphp/src/Ast/Sass/ArgumentDeclaration.php b/scssphp/src/Ast/Sass/ArgumentDeclaration.php deleted file mode 100644 index 8137585..0000000 --- a/scssphp/src/Ast/Sass/ArgumentDeclaration.php +++ /dev/null @@ -1,228 +0,0 @@ - - * @readonly - */ - private $arguments; - - /** - * @var string|null - * @readonly - */ - private $restArgument; - - /** - * @var FileSpan - * @readonly - */ - private $span; - - /** - * @param list $arguments - * @param FileSpan $span - * @param string|null $restArgument - */ - public function __construct(array $arguments, FileSpan $span, ?string $restArgument = null) - { - $this->arguments = $arguments; - $this->restArgument = $restArgument; - $this->span = $span; - } - - public static function createEmpty(FileSpan $span): ArgumentDeclaration - { - return new self([], $span); - } - - /** - * Parses an argument declaration from $contents, which should be of the - * form `@rule name(args) {`. - * - * If passed, $url is the name of the file from which $contents comes. - * - * @throws SassFormatException if parsing fails. - */ - public static function parse(string $contents, ?LoggerInterface $logger = null, ?string $url = null): ArgumentDeclaration - { - return (new ScssParser($contents, $logger, $url))->parseArgumentDeclaration(); - } - - public function isEmpty(): bool - { - return \count($this->arguments) === 0 && $this->restArgument === null; - } - - /** - * @return list - */ - public function getArguments(): array - { - return $this->arguments; - } - - public function getRestArgument(): ?string - { - return $this->restArgument; - } - - public function getSpan(): FileSpan - { - return $this->span; - } - - /** - * @param int $positional - * @param array $names Only keys are relevant - * - * @throws SassScriptException if $positional and $names aren't valid for this argument declaration. - */ - public function verify(int $positional, array $names): void - { - $nameUsed = 0; - - foreach ($this->arguments as $i => $argument) { - if ($i < $positional) { - if (isset($names[$argument->getName()])) { - $originalName = $this->originalArgumentName($argument->getName()); - throw new SassScriptException(sprintf('Argument $%s was passed both by position and by name.', $originalName)); - } - } elseif (isset($names[$argument->getName()])) { - $nameUsed++; - } elseif ($argument->getDefaultValue() === null) { - $originalName = $this->originalArgumentName($argument->getName()); - throw new SassScriptException(sprintf('Missing argument $%s', $originalName)); - } - } - - if ($this->restArgument !== null) { - return; - } - - if ($positional > \count($this->arguments)) { - $message = sprintf( - 'Only %d %sargument%s allowed, but %d %s passed.', - \count($this->arguments), - empty($names) ? '' : 'positional ', - \count($this->arguments) === 1 ? '' : 's', - $positional, - $positional === 1 ? 'was' : 'were' - ); - throw new SassScriptException($message); - } - - if ($nameUsed < \count($names)) { - $unknownNames = array_values(array_diff(array_keys($names), array_map(function ($argument) { - return $argument->getName(); - }, $this->arguments))); - $lastName = array_pop($unknownNames); - $message = sprintf( - 'No argument%s named $%s%s.', - $unknownNames ? 's' : '', - $unknownNames ? implode(', $', $unknownNames) . ' or $' : '', - $lastName - ); - throw new SassScriptException($message); - } - } - - private function originalArgumentName(string $name): string - { - if ($name === $this->restArgument) { - $text = $this->span->getText(); - $lastDollar = strrpos($text, '$'); - assert($lastDollar !== false); - $fromDollar = substr($text, $lastDollar); - $dot = strrpos($fromDollar, '.'); - assert($dot !== false); - - return substr($fromDollar, 0, $dot); - } - - foreach ($this->arguments as $argument) { - if ($argument->getName() === $name) { - return $argument->getOriginalName(); - } - } - - throw new \InvalidArgumentException("This declaration has no argument named \"\$$name\"."); - } - - /** - * Returns whether $positional and $names are valid for this argument - * declaration. - * - * @param int $positional - * @param array $names Only keys are relevant - * - * @return bool - */ - public function matches(int $positional, array $names): bool - { - $nameUsed = 0; - - foreach ($this->arguments as $i => $argument) { - if ($i < $positional) { - if (isset($names[$argument->getName()])) { - return false; - } - } elseif (isset($names[$argument->getName()])) { - $nameUsed++; - } elseif ($argument->getDefaultValue() === null) { - return false; - } - } - - if ($this->restArgument !== null) { - return true; - } - - if ($positional > \count($this->arguments)) { - return false; - } - - if ($nameUsed < \count($names)) { - return false; - } - - return true; - } - - public function __toString(): string - { - $parts = []; - foreach ($this->arguments as $arg) { - $parts[] = "\$$arg"; - } - if ($this->restArgument !== null) { - $parts[] = "\$$this->restArgument..."; - } - - return implode(', ', $parts); - } -} diff --git a/scssphp/src/Ast/Sass/ArgumentInvocation.php b/scssphp/src/Ast/Sass/ArgumentInvocation.php deleted file mode 100644 index 3be3517..0000000 --- a/scssphp/src/Ast/Sass/ArgumentInvocation.php +++ /dev/null @@ -1,127 +0,0 @@ - - * @readonly - */ - private $positional; - - /** - * @var array - * @readonly - */ - private $named; - - /** - * @var Expression|null - * @readonly - */ - private $rest; - - /** - * @var Expression|null - */ - private $keywordRest; - - /** - * @var FileSpan - * @readonly - */ - private $span; - - /** - * @param list $positional - * @param array $named - * @param FileSpan $span - * @param Expression|null $rest - * @param Expression|null $keywordRest - */ - public function __construct(array $positional, array $named, FileSpan $span, ?Expression $rest = null, ?Expression $keywordRest = null) - { - assert($keywordRest === null || $rest !== null); - - $this->positional = $positional; - $this->named = $named; - $this->rest = $rest; - $this->keywordRest = $keywordRest; - $this->span = $span; - } - - public static function createEmpty(FileSpan $span): ArgumentInvocation - { - return new self([], [], $span); - } - - public function isEmpty(): bool - { - return \count($this->positional) === 0 && \count($this->named) === 0 && $this->rest === null; - } - - /** - * @return list - */ - public function getPositional(): array - { - return $this->positional; - } - - /** - * @return array - */ - public function getNamed(): array - { - return $this->named; - } - - public function getRest(): ?Expression - { - return $this->rest; - } - - public function getKeywordRest(): ?Expression - { - return $this->keywordRest; - } - - public function getSpan(): FileSpan - { - return $this->span; - } - - public function __toString(): string - { - $parts = $this->positional; - foreach ($this->named as $name => $arg) { - $parts[] = "\$$name: $arg"; - } - if ($this->rest !== null) { - $parts[] = "$this->rest..."; - } - if ($this->keywordRest !== null) { - $parts[] = "$this->keywordRest..."; - } - - return '(' . implode(', ', $parts) . ')'; - } -} diff --git a/scssphp/src/Ast/Sass/AtRootQuery.php b/scssphp/src/Ast/Sass/AtRootQuery.php deleted file mode 100644 index 30a05f9..0000000 --- a/scssphp/src/Ast/Sass/AtRootQuery.php +++ /dev/null @@ -1,167 +0,0 @@ -parse(); - } - - /** - * @param string[] $names - * @param bool $include - */ - public static function create(array $names, bool $include): AtRootQuery - { - return new AtRootQuery($names, $include, \in_array('all', $names, true), \in_array('rule', $names, true)); - } - - /** - * The default at-root query - */ - public static function getDefault(): AtRootQuery - { - return new AtRootQuery([], false, false, true); - } - - /** - * @param string[] $names - * @param bool $include - * @param bool $all - * @param bool $rule - */ - private function __construct(array $names, bool $include, bool $all, bool $rule) - { - $this->include = $include; - $this->names = $names; - $this->all = $all; - $this->rule = $rule; - } - - public function getInclude(): bool - { - return $this->include; - } - - /** - * @return string[] - */ - public function getNames(): array - { - return $this->names; - } - - /** - * Whether this excludes style rules. - * - * Note that this takes {@see include} into account. - */ - public function excludesStyleRules(): bool - { - return ($this->all || $this->rule) !== $this->include; - } - - /** - * Returns whether $this excludes $node - */ - public function excludes(CssParentNode $node): bool - { - if ($this->all) { - return !$this->include; - } - - if ($node instanceof CssStyleRule) { - return $this->excludesStyleRules(); - } - - if ($node instanceof CssMediaRule) { - return $this->excludesName('media'); - } - - if ($node instanceof CssSupportsRule) { - return $this->excludesName('supports'); - } - - if ($node instanceof CssAtRule) { - return $this->excludesName(strtolower($node->getName()->getValue())); - } - - return false; - } - - /** - * Returns whether $this excludes an at-rule with the given $name. - */ - public function excludesName(string $name): bool - { - return ($this->all || \in_array($name, $this->names, true)) !== $this->include; - } -} diff --git a/scssphp/src/Ast/Sass/CallableInvocation.php b/scssphp/src/Ast/Sass/CallableInvocation.php deleted file mode 100644 index 94be58d..0000000 --- a/scssphp/src/Ast/Sass/CallableInvocation.php +++ /dev/null @@ -1,18 +0,0 @@ -name = $name; - $this->expression = $expression; - $this->span = $span; - $this->guarded = $guarded; - } - - public function getName(): string - { - return $this->name; - } - - public function getExpression(): Expression - { - return $this->expression; - } - - public function getSpan(): FileSpan - { - return $this->span; - } - - public function isGuarded(): bool - { - return $this->guarded; - } - - public function getNameSpan(): FileSpan - { - return SpanUtil::initialIdentifier($this->span, 1); - } - - public function __toString(): string - { - return '$' . $this->name . ': ' . $this->expression . ($this->guarded ? ' !default' : ''); - } -} diff --git a/scssphp/src/Ast/Sass/Expression.php b/scssphp/src/Ast/Sass/Expression.php deleted file mode 100644 index 99fb067..0000000 --- a/scssphp/src/Ast/Sass/Expression.php +++ /dev/null @@ -1,30 +0,0 @@ - $visitor - * @return T - */ - public function accept(ExpressionVisitor $visitor); -} diff --git a/scssphp/src/Ast/Sass/Expression/BinaryOperationExpression.php b/scssphp/src/Ast/Sass/Expression/BinaryOperationExpression.php deleted file mode 100644 index e00b7cf..0000000 --- a/scssphp/src/Ast/Sass/Expression/BinaryOperationExpression.php +++ /dev/null @@ -1,148 +0,0 @@ -operator = $operator; - $this->left = $left; - $this->right = $right; - } - - /** - * Creates a dividedBy operation that may be interpreted as slash-separated numbers. - */ - public static function slash(Expression $left, Expression $right): self - { - $operation = new self(BinaryOperator::DIVIDED_BY, $left, $right); - $operation->allowsSlash = true; - - return $operation; - } - - /** - * @return BinaryOperator::* - */ - public function getOperator(): string - { - return $this->operator; - } - - public function getLeft(): Expression - { - return $this->left; - } - - public function getRight(): Expression - { - return $this->right; - } - - public function allowsSlash(): bool - { - return $this->allowsSlash; - } - - public function getSpan(): FileSpan - { - $left = $this->left; - - while ($left instanceof BinaryOperationExpression) { - $left = $left->left; - } - - $right = $this->right; - - while ($right instanceof BinaryOperationExpression) { - $right = $right->right; - } - - $leftSpan = $left->getSpan(); - $rightSpan = $right->getSpan(); - - return $leftSpan->expand($rightSpan); - } - - public function accept(ExpressionVisitor $visitor) - { - return $visitor->visitBinaryOperationExpression($this); - } - - public function __toString(): string - { - $buffer = ''; - - $leftNeedsParens = $this->left instanceof BinaryOperationExpression && BinaryOperator::getPrecedence($this->left->getOperator()) < BinaryOperator::getPrecedence($this->operator); - if ($leftNeedsParens) { - $buffer .= '('; - } - $buffer .= $this->left; - if ($leftNeedsParens) { - $buffer .= ')'; - } - - $buffer .= ' '; - $buffer .= $this->operator; - $buffer .= ' '; - - $rightNeedsParens = $this->right instanceof BinaryOperationExpression && BinaryOperator::getPrecedence($this->right->getOperator()) <= BinaryOperator::getPrecedence($this->operator); - if ($rightNeedsParens) { - $buffer .= '('; - } - $buffer .= $this->right; - if ($rightNeedsParens) { - $buffer .= ')'; - } - - return $buffer; - } -} diff --git a/scssphp/src/Ast/Sass/Expression/BinaryOperator.php b/scssphp/src/Ast/Sass/Expression/BinaryOperator.php deleted file mode 100644 index ab75136..0000000 --- a/scssphp/src/Ast/Sass/Expression/BinaryOperator.php +++ /dev/null @@ -1,72 +0,0 @@ -'; - const GREATER_THAN_OR_EQUALS = '>='; - const LESS_THAN = '<'; - const LESS_THAN_OR_EQUALS = '<='; - const PLUS = '+'; - const MINUS = '-'; - const TIMES = '*'; - const DIVIDED_BY = '/'; - const MODULO = '%'; - - /** - * @param BinaryOperator::* $operator - */ - public static function getPrecedence(string $operator): int - { - switch ($operator) { - case self::SINGLE_EQUALS: - return 0; - - case self::OR: - return 1; - - case self::AND: - return 2; - - case self::EQUALS: - case self::NOT_EQUALS: - return 3; - - case self::GREATER_THAN: - case self::GREATER_THAN_OR_EQUALS: - case self::LESS_THAN: - case self::LESS_THAN_OR_EQUALS: - return 4; - - case self::PLUS: - case self::MINUS: - return 5; - - case self::TIMES: - case self::DIVIDED_BY: - case self::MODULO: - return 6; - } - - throw new \InvalidArgumentException(sprintf('Unknown operator "%s".', $operator)); - } -} diff --git a/scssphp/src/Ast/Sass/Expression/BooleanExpression.php b/scssphp/src/Ast/Sass/Expression/BooleanExpression.php deleted file mode 100644 index 0981d3e..0000000 --- a/scssphp/src/Ast/Sass/Expression/BooleanExpression.php +++ /dev/null @@ -1,63 +0,0 @@ -value = $value; - $this->span = $span; - } - - public function getValue(): bool - { - return $this->value; - } - - public function getSpan(): FileSpan - { - return $this->span; - } - - public function accept(ExpressionVisitor $visitor) - { - return $visitor->visitBooleanExpression($this); - } - - public function __toString(): string - { - return $this->value ? 'true' : 'false'; - } -} diff --git a/scssphp/src/Ast/Sass/Expression/CalculationExpression.php b/scssphp/src/Ast/Sass/Expression/CalculationExpression.php deleted file mode 100644 index 7f5e611..0000000 --- a/scssphp/src/Ast/Sass/Expression/CalculationExpression.php +++ /dev/null @@ -1,235 +0,0 @@ - - * @readonly - */ - private $arguments; - - /** - * @var FileSpan - * @readonly - */ - private $span; - - /** - * Returns a `calc()` calculation expression. - * - * @param Expression $argument - * @param FileSpan $span - * - * @return CalculationExpression - */ - public static function calc(Expression $argument, FileSpan $span): CalculationExpression - { - return new CalculationExpression('calc', [$argument], $span); - } - - /** - * Returns a `min()` calculation expression. - * - * @param list $arguments - * @param FileSpan $span - * - * @return CalculationExpression - */ - public static function min(array $arguments, FileSpan $span): CalculationExpression - { - if (!$arguments) { - throw new \InvalidArgumentException('min() requires at least one argument.'); - } - - return new CalculationExpression('min', $arguments, $span); - } - - /** - * Returns a `max()` calculation expression. - * - * @param list $arguments - * @param FileSpan $span - * - * @return CalculationExpression - */ - public static function max(array $arguments, FileSpan $span): CalculationExpression - { - if (!$arguments) { - throw new \InvalidArgumentException('max() requires at least one argument.'); - } - - return new CalculationExpression('max', $arguments, $span); - } - - /** - * Returns a `clamp()` calculation expression. - * - * @param Expression $min - * @param Expression $value - * @param Expression $max - * @param FileSpan $span - * - * @return CalculationExpression - */ - public static function clamp(Expression $min, Expression $value, Expression $max, FileSpan $span): CalculationExpression - { - return new CalculationExpression('clamp', [$min, $value, $max], $span); - } - - /** - * Returns a calculation expression with the given name and arguments. - * - * Unlike the other constructors, this doesn't verify that the arguments are - * valid for the name. - * - * @param string $name - * @param list $arguments - * @param FileSpan $span - */ - public function __construct(string $name, array $arguments, FileSpan $span) - { - self::verifyArguments($arguments); - $this->name = $name; - $this->arguments = $arguments; - $this->span = $span; - } - - /** - * @return string - */ - public function getName(): string - { - return $this->name; - } - - /** - * @return list - */ - public function getArguments(): array - { - return $this->arguments; - } - - public function getSpan(): FileSpan - { - return $this->span; - } - - public function accept(ExpressionVisitor $visitor) - { - return $visitor->visitCalculationExpression($this); - } - - /** - * @param list $arguments - * - * @throws \InvalidArgumentException if $arguments aren't valid calculation arguments. - */ - private static function verifyArguments(array $arguments): void - { - foreach ($arguments as $argument) { - self::verify($argument); - } - } - - /** - * @throws \InvalidArgumentException if $expression isn't a valid calculation argument. - */ - private static function verify(Expression $expression): void - { - if ($expression instanceof NumberExpression) { - return; - } - - if ($expression instanceof CalculationExpression) { - return; - } - - if ($expression instanceof VariableExpression) { - return; - } - - if ($expression instanceof FunctionExpression) { - return; - } - - if ($expression instanceof IfExpression) { - return; - } - - if ($expression instanceof StringExpression) { - if ($expression->hasQuotes()) { - throw new \InvalidArgumentException('Invalid calculation argument.'); - } - - return; - } - - if ($expression instanceof ParenthesizedExpression) { - self::verify($expression->getExpression()); - - return; - } - - if ($expression instanceof BinaryOperationExpression) { - self::verify($expression->getLeft()); - self::verify($expression->getRight()); - - if ($expression->getOperator() === BinaryOperator::PLUS) { - return; - } - - if ($expression->getOperator() === BinaryOperator::MINUS) { - return; - } - - if ($expression->getOperator() === BinaryOperator::TIMES) { - return; - } - - if ($expression->getOperator() === BinaryOperator::DIVIDED_BY) { - return; - } - - throw new \InvalidArgumentException('Invalid calculation argument.'); - } - - throw new \InvalidArgumentException('Invalid calculation argument.'); - } - - public function __toString(): string - { - return $this->name . '(' . implode(', ', $this->arguments) . ')'; - } -} diff --git a/scssphp/src/Ast/Sass/Expression/ColorExpression.php b/scssphp/src/Ast/Sass/Expression/ColorExpression.php deleted file mode 100644 index dc8583e..0000000 --- a/scssphp/src/Ast/Sass/Expression/ColorExpression.php +++ /dev/null @@ -1,64 +0,0 @@ -value = $value; - $this->span = $span; - } - - public function getValue(): SassColor - { - return $this->value; - } - - public function getSpan(): FileSpan - { - return $this->span; - } - - public function accept(ExpressionVisitor $visitor) - { - return $visitor->visitColorExpression($this); - } - - public function __toString(): string - { - return (string) $this->value; - } -} diff --git a/scssphp/src/Ast/Sass/Expression/FunctionExpression.php b/scssphp/src/Ast/Sass/Expression/FunctionExpression.php deleted file mode 100644 index c0a642a..0000000 --- a/scssphp/src/Ast/Sass/Expression/FunctionExpression.php +++ /dev/null @@ -1,141 +0,0 @@ -span = $span; - $this->originalName = $originalName; - $this->arguments = $arguments; - $this->namespace = $namespace; - } - - /** - * @return string - */ - public function getOriginalName(): string - { - return $this->originalName; - } - - /** - * The name of the function being invoked, with underscores converted to - * hyphens. - * - * If this function is a plain CSS function, use {@see getOriginalName} instead. - */ - public function getName(): string - { - return str_replace('_', '-', $this->originalName); - } - - public function getArguments(): ArgumentInvocation - { - return $this->arguments; - } - - public function getNamespace(): ?string - { - return $this->namespace; - } - - public function getSpan(): FileSpan - { - return $this->span; - } - - public function getNameSpan(): FileSpan - { - if ($this->namespace === null) { - return SpanUtil::initialIdentifier($this->span); - } - - return SpanUtil::initialIdentifier(SpanUtil::withoutNamespace($this->span)); - } - - public function getNamespaceSpan(): ?FileSpan - { - if ($this->namespace === null) { - return null; - } - - return SpanUtil::initialIdentifier($this->span); - } - - public function accept(ExpressionVisitor $visitor) - { - return $visitor->visitFunctionExpression($this); - } - - public function __toString(): string - { - $buffer = ''; - - if ($this->namespace !== null) { - $buffer .= $this->namespace . '.'; - } - - $buffer .= $this->originalName . $this->arguments; - - return $buffer; - } -} diff --git a/scssphp/src/Ast/Sass/Expression/IfExpression.php b/scssphp/src/Ast/Sass/Expression/IfExpression.php deleted file mode 100644 index 06c95b9..0000000 --- a/scssphp/src/Ast/Sass/Expression/IfExpression.php +++ /dev/null @@ -1,89 +0,0 @@ -span = $span; - $this->arguments = $arguments; - } - - /** - * The declaration of `if()`, as though it were a normal function. - */ - public static function getDeclaration(): ArgumentDeclaration - { - if (self::$declaration === null) { - self::$declaration = ArgumentDeclaration::parse('@function if($condition, $if-true, $if-false) {'); - } - - return self::$declaration; - } - - public function getArguments(): ArgumentInvocation - { - return $this->arguments; - } - - public function getSpan(): FileSpan - { - return $this->span; - } - - public function accept(ExpressionVisitor $visitor) - { - return $visitor->visitIfExpression($this); - } - - public function __toString(): string - { - return 'if' . $this->arguments; - } -} diff --git a/scssphp/src/Ast/Sass/Expression/InterpolatedFunctionExpression.php b/scssphp/src/Ast/Sass/Expression/InterpolatedFunctionExpression.php deleted file mode 100644 index 2087658..0000000 --- a/scssphp/src/Ast/Sass/Expression/InterpolatedFunctionExpression.php +++ /dev/null @@ -1,84 +0,0 @@ -span = $span; - $this->name = $name; - $this->arguments = $arguments; - } - - public function getName(): Interpolation - { - return $this->name; - } - - public function getArguments(): ArgumentInvocation - { - return $this->arguments; - } - - public function getSpan(): FileSpan - { - return $this->span; - } - - public function accept(ExpressionVisitor $visitor) - { - return $visitor->visitInterpolatedFunctionExpression($this); - } - - public function __toString(): string - { - return $this->name . $this->arguments; - } -} diff --git a/scssphp/src/Ast/Sass/Expression/ListExpression.php b/scssphp/src/Ast/Sass/Expression/ListExpression.php deleted file mode 100644 index 053a212..0000000 --- a/scssphp/src/Ast/Sass/Expression/ListExpression.php +++ /dev/null @@ -1,142 +0,0 @@ -contents = $contents; - $this->separator = $separator; - $this->span = $span; - $this->brackets = $brackets; - } - - /** - * @return Expression[] - */ - public function getContents(): array - { - return $this->contents; - } - - /** - * @return ListSeparator::* - */ - public function getSeparator(): string - { - return $this->separator; - } - - public function hasBrackets(): bool - { - return $this->brackets; - } - - public function getSpan(): FileSpan - { - return $this->span; - } - - public function accept(ExpressionVisitor $visitor) - { - return $visitor->visitListExpression($this); - } - - public function __toString(): string - { - $buffer = ''; - if ($this->hasBrackets()) { - $buffer .= '['; - } - - $buffer .= implode($this->separator === ListSeparator::COMMA ? ', ' : ' ', array_map(function ($element) { - return $this->elementNeedsParens($element) ? "($element)" : (string) $element; - }, $this->contents)); - - if ($this->hasBrackets()) { - $buffer .= ']'; - } - - return $buffer; - } - - /** - * Returns whether $expression, contained in $this, needs parentheses when - * printed as Sass source. - */ - private function elementNeedsParens(Expression $expression): bool - { - if ($expression instanceof ListExpression) { - if (\count($expression->contents) < 2) { - return false; - } - - if ($expression->brackets) { - return false; - } - - return $this->separator === ListSeparator::COMMA ? $expression->separator === ListSeparator::COMMA : $expression->separator !== ListSeparator::UNDECIDED; - } - - if ($this->separator !== ListSeparator::SPACE) { - return false; - } - - if ($expression instanceof UnaryOperationExpression) { - return $expression->getOperator() === UnaryOperator::PLUS || $expression->getOperator() === UnaryOperator::MINUS; - } - - return false; - } -} diff --git a/scssphp/src/Ast/Sass/Expression/MapExpression.php b/scssphp/src/Ast/Sass/Expression/MapExpression.php deleted file mode 100644 index 6615f2a..0000000 --- a/scssphp/src/Ast/Sass/Expression/MapExpression.php +++ /dev/null @@ -1,71 +0,0 @@ - - * @readonly - */ - private $pairs; - - /** - * @var FileSpan - * @readonly - */ - private $span; - - /** - * @param list $pairs - */ - public function __construct(array $pairs, FileSpan $span) - { - $this->pairs = $pairs; - $this->span = $span; - } - - /** - * @return list - */ - public function getPairs(): array - { - return $this->pairs; - } - - public function getSpan(): FileSpan - { - return $this->span; - } - - public function accept(ExpressionVisitor $visitor) - { - return $visitor->visitMapExpression($this); - } - - public function __toString(): string - { - return '(' . implode(', ', array_map(function ($pair) { - return $pair[0] . ': ' . $pair[1]; - }, $this->pairs)) . ')'; - } -} diff --git a/scssphp/src/Ast/Sass/Expression/NullExpression.php b/scssphp/src/Ast/Sass/Expression/NullExpression.php deleted file mode 100644 index 6db94f1..0000000 --- a/scssphp/src/Ast/Sass/Expression/NullExpression.php +++ /dev/null @@ -1,51 +0,0 @@ -span = $span; - } - - public function getSpan(): FileSpan - { - return $this->span; - } - - public function accept(ExpressionVisitor $visitor) - { - return $visitor->visitNullExpression($this); - } - - public function __toString(): string - { - return 'null'; - } -} diff --git a/scssphp/src/Ast/Sass/Expression/NumberExpression.php b/scssphp/src/Ast/Sass/Expression/NumberExpression.php deleted file mode 100644 index e0e939c..0000000 --- a/scssphp/src/Ast/Sass/Expression/NumberExpression.php +++ /dev/null @@ -1,76 +0,0 @@ -value = $value; - $this->span = $span; - $this->unit = $unit; - } - - public function getValue(): float - { - return $this->value; - } - - public function getSpan(): FileSpan - { - return $this->span; - } - - public function getUnit(): ?string - { - return $this->unit; - } - - public function accept(ExpressionVisitor $visitor) - { - return $visitor->visitNumberExpression($this); - } - - public function __toString(): string - { - return (string) SassNumber::create($this->value, $this->unit); - } -} diff --git a/scssphp/src/Ast/Sass/Expression/ParenthesizedExpression.php b/scssphp/src/Ast/Sass/Expression/ParenthesizedExpression.php deleted file mode 100644 index 6c2e7ad..0000000 --- a/scssphp/src/Ast/Sass/Expression/ParenthesizedExpression.php +++ /dev/null @@ -1,63 +0,0 @@ -expression = $expression; - $this->span = $span; - } - - public function getExpression(): Expression - { - return $this->expression; - } - - public function getSpan(): FileSpan - { - return $this->span; - } - - public function accept(ExpressionVisitor $visitor) - { - return $visitor->visitParenthesizedExpression($this); - } - - public function __toString(): string - { - return '(' . $this->expression . ')'; - } -} diff --git a/scssphp/src/Ast/Sass/Expression/SelectorExpression.php b/scssphp/src/Ast/Sass/Expression/SelectorExpression.php deleted file mode 100644 index 18ffe35..0000000 --- a/scssphp/src/Ast/Sass/Expression/SelectorExpression.php +++ /dev/null @@ -1,51 +0,0 @@ -span = $span; - } - - public function getSpan(): FileSpan - { - return $this->span; - } - - public function accept(ExpressionVisitor $visitor) - { - return $visitor->visitSelectorExpression($this); - } - - public function __toString(): string - { - return '&'; - } -} diff --git a/scssphp/src/Ast/Sass/Expression/StringExpression.php b/scssphp/src/Ast/Sass/Expression/StringExpression.php deleted file mode 100644 index 5b071f7..0000000 --- a/scssphp/src/Ast/Sass/Expression/StringExpression.php +++ /dev/null @@ -1,182 +0,0 @@ -text = $text; - $this->quotes = $quotes; - } - - /** - * Returns a string expression with no interpolation. - */ - public static function plain(string $text, FileSpan $span, bool $quotes = false): self - { - return new self(new Interpolation([$text], $span), $quotes); - } - - /** - * Returns Sass source for a quoted string that, when evaluated, will have - * $text as its contents. - */ - public static function quoteText(string $text): string - { - $quote = self::bestQuote([$text]); - $buffer = $quote; - $buffer .= self::quoteInnerText($text, $quote, true); - $buffer .= $quote; - - return $buffer; - } - - public function getText(): Interpolation - { - return $this->text; - } - - public function hasQuotes(): bool - { - return $this->quotes; - } - - public function getSpan(): FileSpan - { - return $this->text->getSpan(); - } - - public function accept(ExpressionVisitor $visitor) - { - return $visitor->visitStringExpression($this); - } - - public function asInterpolation(bool $static = false, string $quote = null): Interpolation - { - if (!$this->quotes) { - return $this->text; - } - - $quote = $quote ?? self::bestQuote($this->text->getContents()); - $buffer = new InterpolationBuffer(); - - $buffer->write($quote); - - foreach ($this->text->getContents() as $value) { - if ($value instanceof Expression) { - $buffer->add($value); - } else { - $buffer->write(self::quoteInnerText($value, $quote, $static)); - } - } - - $buffer->write($quote); - - return $buffer->buildInterpolation($this->text->getSpan()); - } - - private static function quoteInnerText(string $value, string $quote, bool $static = false): string - { - $buffer = ''; - $length = \strlen($value); - - for ($i = 0; $i < $length; $i++) { - $char = $value[$i]; - - if (Character::isNewline($char)) { - $buffer .= '\\a'; - - if ($i !== $length - 1) { - $next = $value[$i + 1]; - - if (Character::isWhitespace($next) || Character::isHex($next)) { - $buffer .= ' '; - } - } - } else { - if ($char === $quote || $char === '\\' || ($static && $char === '#' && $i < $length - 1 && $value[$i + 1] === '{')) { - $buffer .= '\\'; - } - - if (\ord($char) < 0x80) { - $buffer .= $char; - } else { - if (!preg_match('/./usA', $value, $m, 0, $i)) { - throw new \UnexpectedValueException('Invalid UTF-8 char'); - } - - $buffer .= $m[0]; - $i += \strlen($m[0]) - 1; // skip over the extra bytes that have been processed. - } - } - } - - return $buffer; - } - - /** - * @param array $parts - * - * @return string - */ - private static function bestQuote(array $parts): string - { - $containsDoubleQuote = false; - - foreach ($parts as $part) { - if (!\is_string($part)) { - continue; - } - - if (false !== strpos($part, "'")) { - return '"'; - } - - if (false !== strpos($part, '"')) { - $containsDoubleQuote = true; - } - } - - return $containsDoubleQuote ? "'": '"'; - } - - public function __toString(): string - { - return (string) $this->asInterpolation(); - } -} diff --git a/scssphp/src/Ast/Sass/Expression/SupportsExpression.php b/scssphp/src/Ast/Sass/Expression/SupportsExpression.php deleted file mode 100644 index 7ef31bb..0000000 --- a/scssphp/src/Ast/Sass/Expression/SupportsExpression.php +++ /dev/null @@ -1,60 +0,0 @@ -condition = $condition; - } - - public function getCondition(): SupportsCondition - { - return $this->condition; - } - - public function getSpan(): FileSpan - { - return $this->condition->getSpan(); - } - - public function accept(ExpressionVisitor $visitor) - { - return $visitor->visitSupportsExpression($this); - } - - public function __toString(): string - { - return (string) $this->condition; - } -} diff --git a/scssphp/src/Ast/Sass/Expression/UnaryOperationExpression.php b/scssphp/src/Ast/Sass/Expression/UnaryOperationExpression.php deleted file mode 100644 index ecb8904..0000000 --- a/scssphp/src/Ast/Sass/Expression/UnaryOperationExpression.php +++ /dev/null @@ -1,87 +0,0 @@ -operator = $operator; - $this->operand = $operand; - $this->span = $span; - } - - /** - * @return UnaryOperator::* - */ - public function getOperator() - { - return $this->operator; - } - - public function getOperand(): Expression - { - return $this->operand; - } - - public function getSpan(): FileSpan - { - return $this->span; - } - - public function accept(ExpressionVisitor $visitor) - { - return $visitor->visitUnaryOperationExpression($this); - } - - public function __toString(): string - { - $buffer = $this->operator; - if ($this->operator === UnaryOperator::NOT) { - $buffer .= ' '; - } - $buffer .= $this->operand; - - return $buffer; - } -} diff --git a/scssphp/src/Ast/Sass/Expression/UnaryOperator.php b/scssphp/src/Ast/Sass/Expression/UnaryOperator.php deleted file mode 100644 index d82afe6..0000000 --- a/scssphp/src/Ast/Sass/Expression/UnaryOperator.php +++ /dev/null @@ -1,24 +0,0 @@ -value = $value; - $this->span = $span; - } - - public function getValue(): Value - { - return $this->value; - } - - public function getSpan(): FileSpan - { - return $this->span; - } - - public function accept(ExpressionVisitor $visitor) - { - return $visitor->visitValueExpression($this); - } - - public function __toString(): string - { - return (string) $this->value; - } -} diff --git a/scssphp/src/Ast/Sass/Expression/VariableExpression.php b/scssphp/src/Ast/Sass/Expression/VariableExpression.php deleted file mode 100644 index 5ccb449..0000000 --- a/scssphp/src/Ast/Sass/Expression/VariableExpression.php +++ /dev/null @@ -1,104 +0,0 @@ -span = $span; - $this->name = $name; - $this->namespace = $namespace; - } - - public function getName(): string - { - return $this->name; - } - - public function getNamespace(): ?string - { - return $this->namespace; - } - - public function getSpan(): FileSpan - { - return $this->span; - } - - public function getNameSpan(): FileSpan - { - if ($this->namespace === null) { - return $this->span; - } - - return SpanUtil::withoutNamespace($this->span); - } - - public function getNamespaceSpan(): ?FileSpan - { - if ($this->namespace === null) { - return null; - } - - return SpanUtil::initialIdentifier($this->span); - } - - public function accept(ExpressionVisitor $visitor) - { - return $visitor->visitVariableExpression($this); - } - - public function __toString(): string - { - if ($this->namespace === null) { - return '$' . $this->name; - } - - return $this->namespace . '$' . $this->name; - } -} diff --git a/scssphp/src/Ast/Sass/Import.php b/scssphp/src/Ast/Sass/Import.php deleted file mode 100644 index 0a7dbb5..0000000 --- a/scssphp/src/Ast/Sass/Import.php +++ /dev/null @@ -1,22 +0,0 @@ -urlString = $urlString; - $this->span = $span; - } - - public function getUrlString(): string - { - return $this->urlString; - } - - public function getSpan(): FileSpan - { - return $this->span; - } - - public function __toString(): string - { - return StringExpression::quoteText($this->urlString); - } -} diff --git a/scssphp/src/Ast/Sass/Import/StaticImport.php b/scssphp/src/Ast/Sass/Import/StaticImport.php deleted file mode 100644 index 693f3e7..0000000 --- a/scssphp/src/Ast/Sass/Import/StaticImport.php +++ /dev/null @@ -1,83 +0,0 @@ -url = $url; - $this->span = $span; - $this->modifiers = $modifiers; - } - - public function getUrl(): Interpolation - { - return $this->url; - } - - public function getModifiers(): ?Interpolation - { - return $this->modifiers; - } - - public function getSpan(): FileSpan - { - return $this->span; - } - - public function __toString(): string - { - $buffer = (string) $this->url; - - if ($this->modifiers !== null) { - $buffer .= ' ' . $this->modifiers; - } - - return $buffer; - } -} diff --git a/scssphp/src/Ast/Sass/Interpolation.php b/scssphp/src/Ast/Sass/Interpolation.php deleted file mode 100644 index 3b5656c..0000000 --- a/scssphp/src/Ast/Sass/Interpolation.php +++ /dev/null @@ -1,138 +0,0 @@ - - * @readonly - */ - private $contents; - - /** - * @var FileSpan - * @readonly - */ - private $span; - - /** - * Creates a new {@see Interpolation} by concatenating a sequence of strings, - * {@see Expression}s, or nested {@see Interpolation}s. - * - * @param array $contents - */ - public static function concat(array $contents, FileSpan $span): Interpolation - { - $buffer = new InterpolationBuffer(); - - foreach ($contents as $element) { - if (\is_string($element)) { - $buffer->write($element); - } elseif ($element instanceof Expression) { - $buffer->add($element); - } elseif ($element instanceof Interpolation) { - $buffer->addInterpolation($element); - } else { - throw new \InvalidArgumentException(sprintf('The elements in $contents may only contains strings, Expressions, or Interpolations, "%s" given.', \is_object($element) ? get_class($element) : gettype($element))); - } - } - - return $buffer->buildInterpolation($span); - } - - /** - * @param list $contents - */ - public function __construct(array $contents, FileSpan $span) - { - for ($i = 0; $i < \count($contents); $i++) { - if (!\is_string($contents[$i]) && !$contents[$i] instanceof Expression) { - throw new \TypeError('The contents of an Interpolation may only contain strings or Expression instances.'); - } - - if ($i != 0 && \is_string($contents[$i]) && \is_string($contents[$i - 1])) { - throw new \InvalidArgumentException('The contents of an Interpolation may not contain adjacent strings.'); - } - } - - $this->contents = $contents; - $this->span = $span; - } - - /** - * @return list - */ - public function getContents(): array - { - return $this->contents; - } - - public function getSpan(): FileSpan - { - return $this->span; - } - - /** - * If this contains no interpolated expressions, returns its text contents. - * - * Otherwise, returns `null`. - * - * @psalm-mutation-free - */ - public function getAsPlain(): ?string - { - if (\count($this->contents) === 0) { - return ''; - } - - if (\count($this->contents) > 1) { - return null; - } - - if (\is_string($this->contents[0])) { - return $this->contents[0]; - } - - return null; - } - - /** - * Returns the plain text before the interpolation, or the empty string. - */ - public function getInitialPlain(): string - { - $first = $this->contents[0] ?? null; - - if (\is_string($first)) { - return $first; - } - - return ''; - } - - public function __toString(): string - { - return implode('', array_map(function ($value) { - return \is_string($value) ? $value : '#{' . $value .'}'; - }, $this->contents)); - } -} diff --git a/scssphp/src/Ast/Sass/SassDeclaration.php b/scssphp/src/Ast/Sass/SassDeclaration.php deleted file mode 100644 index fcf2dcc..0000000 --- a/scssphp/src/Ast/Sass/SassDeclaration.php +++ /dev/null @@ -1,37 +0,0 @@ - $visitor - * @return T - */ - public function accept(StatementVisitor $visitor); -} diff --git a/scssphp/src/Ast/Sass/Statement/AtRootRule.php b/scssphp/src/Ast/Sass/Statement/AtRootRule.php deleted file mode 100644 index 3da58c3..0000000 --- a/scssphp/src/Ast/Sass/Statement/AtRootRule.php +++ /dev/null @@ -1,80 +0,0 @@ - - * - * @internal - */ -final class AtRootRule extends ParentStatement -{ - /** - * @var Interpolation|null - * @readonly - */ - private $query; - - /** - * @var FileSpan - * @readonly - */ - private $span; - - /** - * @param Statement[] $children - */ - public function __construct(array $children, FileSpan $span, ?Interpolation $query = null) - { - $this->query = $query; - $this->span = $span; - parent::__construct($children); - } - - /** - * The query specifying which statements this should move its contents through. - */ - public function getQuery(): ?Interpolation - { - return $this->query; - } - - public function getSpan(): FileSpan - { - return $this->span; - } - - public function accept(StatementVisitor $visitor) - { - return $visitor->visitAtRootRule($this); - } - - public function __toString(): string - { - $buffer = '@at-root '; - if ($this->query !== null) { - $buffer .= $this->query . ' '; - } - - return $buffer . '{' . implode(' ', $this->getChildren()) . '}'; - } -} diff --git a/scssphp/src/Ast/Sass/Statement/AtRule.php b/scssphp/src/Ast/Sass/Statement/AtRule.php deleted file mode 100644 index d401284..0000000 --- a/scssphp/src/Ast/Sass/Statement/AtRule.php +++ /dev/null @@ -1,93 +0,0 @@ - - * - * @internal - */ -final class AtRule extends ParentStatement -{ - /** - * @var Interpolation - * @readonly - */ - private $name; - - /** - * @var Interpolation|null - * @readonly - */ - private $value; - - /** - * @var FileSpan - * @readonly - */ - private $span; - - /** - * @param Statement[]|null $children - */ - public function __construct(Interpolation $name, FileSpan $span, ?Interpolation $value = null, ?array $children = null) - { - $this->name = $name; - $this->value = $value; - $this->span = $span; - parent::__construct($children); - } - - public function getName(): Interpolation - { - return $this->name; - } - - public function getValue(): ?Interpolation - { - return $this->value; - } - - public function getSpan(): FileSpan - { - return $this->span; - } - - public function accept(StatementVisitor $visitor) - { - return $visitor->visitAtRule($this); - } - - public function __toString(): string - { - $buffer = '@' . $this->name; - if ($this->value !== null) { - $buffer .= ' ' . $this->value; - } - - $children = $this->getChildren(); - - if ($children === null) { - return $buffer . ';'; - } - - return $buffer . '{' . implode(' ', $children) . '}'; - } -} diff --git a/scssphp/src/Ast/Sass/Statement/CallableDeclaration.php b/scssphp/src/Ast/Sass/Statement/CallableDeclaration.php deleted file mode 100644 index 0018e92..0000000 --- a/scssphp/src/Ast/Sass/Statement/CallableDeclaration.php +++ /dev/null @@ -1,90 +0,0 @@ - - * - * @internal - */ -abstract class CallableDeclaration extends ParentStatement -{ - /** - * @var string - * @readonly - */ - private $name; - - /** - * @var ArgumentDeclaration - * @readonly - */ - private $arguments; - - /** - * @var SilentComment|null - * @readonly - */ - private $comment; - - /** - * @var FileSpan - * @readonly - */ - private $span; - - /** - * @param Statement[] $children - */ - public function __construct(string $name, ArgumentDeclaration $arguments, FileSpan $span, array $children, ?SilentComment $comment = null) - { - $this->name = $name; - $this->arguments = $arguments; - $this->comment = $comment; - $this->span = $span; - parent::__construct($children); - } - - /** - * The name of this callable, with underscores converted to hyphens. - */ - final public function getName(): string - { - return $this->name; - } - - final public function getArguments(): ArgumentDeclaration - { - return $this->arguments; - } - - /** - * @return SilentComment|null - */ - final public function getComment(): ?SilentComment - { - return $this->comment; - } - - final public function getSpan(): FileSpan - { - return $this->span; - } -} diff --git a/scssphp/src/Ast/Sass/Statement/ContentBlock.php b/scssphp/src/Ast/Sass/Statement/ContentBlock.php deleted file mode 100644 index 02c2e69..0000000 --- a/scssphp/src/Ast/Sass/Statement/ContentBlock.php +++ /dev/null @@ -1,46 +0,0 @@ -visitContentBlock($this); - } - - public function __toString(): string - { - $buffer = $this->getArguments()->isEmpty() ? '' : ' using (' . $this->getArguments() . ')'; - - return $buffer . '{' . implode(' ', $this->getChildren()) . '}'; - } -} diff --git a/scssphp/src/Ast/Sass/Statement/ContentRule.php b/scssphp/src/Ast/Sass/Statement/ContentRule.php deleted file mode 100644 index ee95226..0000000 --- a/scssphp/src/Ast/Sass/Statement/ContentRule.php +++ /dev/null @@ -1,71 +0,0 @@ -arguments = $arguments; - $this->span = $span; - } - - public function getArguments(): ArgumentInvocation - { - return $this->arguments; - } - - public function getSpan(): FileSpan - { - return $this->span; - } - - public function accept(StatementVisitor $visitor) - { - return $visitor->visitContentRule($this); - } - - public function __toString(): string - { - return $this->arguments->isEmpty() ? '@content;' : "@content($this->arguments);"; - } -} diff --git a/scssphp/src/Ast/Sass/Statement/DebugRule.php b/scssphp/src/Ast/Sass/Statement/DebugRule.php deleted file mode 100644 index 57490e8..0000000 --- a/scssphp/src/Ast/Sass/Statement/DebugRule.php +++ /dev/null @@ -1,66 +0,0 @@ -expression = $expression; - $this->span = $span; - } - - public function getExpression(): Expression - { - return $this->expression; - } - - public function getSpan(): FileSpan - { - return $this->span; - } - - public function accept(StatementVisitor $visitor) - { - return $visitor->visitDebugRule($this); - } - - public function __toString(): string - { - return '@debug ' . $this->expression . ';'; - } -} diff --git a/scssphp/src/Ast/Sass/Statement/Declaration.php b/scssphp/src/Ast/Sass/Statement/Declaration.php deleted file mode 100644 index d12d44c..0000000 --- a/scssphp/src/Ast/Sass/Statement/Declaration.php +++ /dev/null @@ -1,143 +0,0 @@ - - * - * @internal - */ -final class Declaration extends ParentStatement -{ - /** - * @var Interpolation - * @readonly - */ - private $name; - - /** - * The value of this declaration. - * - * If {@see getChildren} is `null`, this is never `null`. Otherwise, it may or may - * not be `null`. - * - * @var Expression|null - * @readonly - */ - private $value; - - /** - * @var FileSpan - * @readonly - */ - private $span; - - /** - * @param Statement[]|null $children - */ - private function __construct(Interpolation $name, ?Expression $value, FileSpan $span, ?array $children = null) - { - $this->name = $name; - $this->value = $value; - $this->span = $span; - parent::__construct($children); - } - - public static function create(Interpolation $name, Expression $value, FileSpan $span): self - { - $declaration = new self($name, $value, $span); - - if ($declaration->isCustomProperty() && !$value instanceof StringExpression) { - throw new \InvalidArgumentException(sprintf('Declarations whose names begin with "--" must have StringExpression values (got %s)', get_class($value))); - } - - return $declaration; - } - - /** - * @param Statement[] $children - */ - public static function nested(Interpolation $name, array $children, FileSpan $span, ?Expression $value = null): self - { - $declaration = new self($name, $value, $span, $children); - - if ($declaration->isCustomProperty() && !$value instanceof StringExpression) { - throw new \InvalidArgumentException('Declarations whose names begin with "--" may not be nested.'); - } - - return $declaration; - } - - public function getName(): Interpolation - { - return $this->name; - } - - public function getValue(): ?Expression - { - return $this->value; - } - - /** - * Returns whether this is a CSS Custom Property declaration. - * - * Note that this can return `false` for declarations that will ultimately be - * serialized as custom properties if they aren't *parsed as* custom - * properties, such as `#{--foo}: ...`. - * - * If this is `true`, then `value` will be a {@see StringExpression}. - */ - public function isCustomProperty(): bool - { - return 0 === strpos($this->name->getInitialPlain(), '--'); - } - - public function getSpan(): FileSpan - { - return $this->span; - } - - public function accept(StatementVisitor $visitor) - { - return $visitor->visitDeclaration($this); - } - - public function __toString(): string - { - $buffer = $this->name . ':'; - - if ($this->value !== null) { - if (!$this->isCustomProperty()) { - $buffer .= ' '; - } - $buffer .= $this->value; - } - - $children = $this->getChildren(); - - if ($children === null) { - return $buffer . ';'; - } - - return $buffer . '{' . implode(' ', $children) . '}'; - } -} diff --git a/scssphp/src/Ast/Sass/Statement/EachRule.php b/scssphp/src/Ast/Sass/Statement/EachRule.php deleted file mode 100644 index 3771812..0000000 --- a/scssphp/src/Ast/Sass/Statement/EachRule.php +++ /dev/null @@ -1,88 +0,0 @@ - - * - * @internal - */ -final class EachRule extends ParentStatement -{ - /** - * @var string[] - * @readonly - */ - private $variables; - - /** - * @var Expression - * @readonly - */ - private $list; - - /** - * @var FileSpan - * @readonly - */ - private $span; - - /** - * @param string[] $variables - * @param Statement[] $children - */ - public function __construct(array $variables, Expression $list, array $children, FileSpan $span) - { - $this->variables = $variables; - $this->list = $list; - $this->span = $span; - parent::__construct($children); - } - - /** - * @return string[] - */ - public function getVariables(): array - { - return $this->variables; - } - - public function getList(): Expression - { - return $this->list; - } - - public function getSpan(): FileSpan - { - return $this->span; - } - - public function accept(StatementVisitor $visitor) - { - return $visitor->visitEachRule($this); - } - - public function __toString(): string - { - return '@each ' . implode(', ', array_map(function ($variable) { return '$' . $variable; }, $this->variables)) . ' in ' . $this->list . ' {' . implode(' ', $this->getChildren()) . '}'; - } -} diff --git a/scssphp/src/Ast/Sass/Statement/ElseClause.php b/scssphp/src/Ast/Sass/Statement/ElseClause.php deleted file mode 100644 index 8ced96e..0000000 --- a/scssphp/src/Ast/Sass/Statement/ElseClause.php +++ /dev/null @@ -1,26 +0,0 @@ -getChildren()) . '}'; - } -} diff --git a/scssphp/src/Ast/Sass/Statement/ErrorRule.php b/scssphp/src/Ast/Sass/Statement/ErrorRule.php deleted file mode 100644 index 8dcf4d1..0000000 --- a/scssphp/src/Ast/Sass/Statement/ErrorRule.php +++ /dev/null @@ -1,66 +0,0 @@ -expression = $expression; - $this->span = $span; - } - - public function getExpression(): Expression - { - return $this->expression; - } - - public function getSpan(): FileSpan - { - return $this->span; - } - - public function accept(StatementVisitor $visitor) - { - return $visitor->visitErrorRule($this); - } - - public function __toString(): string - { - return '@error ' . $this->expression . ';'; - } -} diff --git a/scssphp/src/Ast/Sass/Statement/ExtendRule.php b/scssphp/src/Ast/Sass/Statement/ExtendRule.php deleted file mode 100644 index 323dab2..0000000 --- a/scssphp/src/Ast/Sass/Statement/ExtendRule.php +++ /dev/null @@ -1,84 +0,0 @@ -selector = $selector; - $this->span = $span; - $this->optional = $optional; - } - - public function getSelector(): Interpolation - { - return $this->selector; - } - - /** - * Whether this is an optional extension. - * - * If an extension isn't optional, it will emit an error if it doesn't match - * any selectors. - */ - public function isOptional(): bool - { - return $this->optional; - } - - public function getSpan(): FileSpan - { - return $this->span; - } - - public function accept(StatementVisitor $visitor) - { - return $visitor->visitExtendRule($this); - } - - public function __toString(): string - { - return '@extend ' . $this->selector . ($this->optional ? ' !optional' : '') . ';'; - } -} diff --git a/scssphp/src/Ast/Sass/Statement/ForRule.php b/scssphp/src/Ast/Sass/Statement/ForRule.php deleted file mode 100644 index 2480e1f..0000000 --- a/scssphp/src/Ast/Sass/Statement/ForRule.php +++ /dev/null @@ -1,111 +0,0 @@ - - * - * @internal - */ -final class ForRule extends ParentStatement -{ - /** - * @var string - * @readonly - */ - private $variable; - - /** - * @var Expression - * @readonly - */ - private $from; - - /** - * @var Expression - * @readonly - */ - private $to; - - /** - * @var bool - * @readonly - */ - private $exclusive; - - /** - * @var FileSpan - * @readonly - */ - private $span; - - /** - * @param Statement[] $children - */ - public function __construct(string $variable, Expression $from, Expression $to, array $children, FileSpan $span, bool $exclusive = false) - { - $this->variable = $variable; - $this->from = $from; - $this->to = $to; - $this->exclusive = $exclusive; - $this->span = $span; - parent::__construct($children); - } - - public function getVariable(): string - { - return $this->variable; - } - - public function getFrom(): Expression - { - return $this->from; - } - - public function getTo(): Expression - { - return $this->to; - } - - /** - * Whether {@see getTo} is exclusive. - */ - public function isExclusive(): bool - { - return $this->exclusive; - } - - public function getSpan(): FileSpan - { - return $this->span; - } - - public function accept(StatementVisitor $visitor) - { - return $visitor->visitForRule($this); - } - - public function __toString(): string - { - return '@for $' . $this->variable . ' from ' . $this->from . ($this->exclusive ? ' to ' : ' through ') . $this->to . '{' . implode(' ', $this->getChildren()) . '}'; - } -} diff --git a/scssphp/src/Ast/Sass/Statement/FunctionRule.php b/scssphp/src/Ast/Sass/Statement/FunctionRule.php deleted file mode 100644 index c872828..0000000 --- a/scssphp/src/Ast/Sass/Statement/FunctionRule.php +++ /dev/null @@ -1,43 +0,0 @@ -getSpan())); - } - - public function accept(StatementVisitor $visitor) - { - return $visitor->visitFunctionRule($this); - } - - public function __toString(): string - { - return '@function ' . $this->getName() . '(' . $this->getArguments() . ') {' . implode(' ', $this->getChildren()) . '}'; - } -} diff --git a/scssphp/src/Ast/Sass/Statement/HasContentVisitor.php b/scssphp/src/Ast/Sass/Statement/HasContentVisitor.php deleted file mode 100644 index 0537265..0000000 --- a/scssphp/src/Ast/Sass/Statement/HasContentVisitor.php +++ /dev/null @@ -1,31 +0,0 @@ - - */ -final class HasContentVisitor extends StatementSearchVisitor -{ - public function visitContentRule(ContentRule $node): bool - { - return true; - } -} diff --git a/scssphp/src/Ast/Sass/Statement/IfClause.php b/scssphp/src/Ast/Sass/Statement/IfClause.php deleted file mode 100644 index 3708aaf..0000000 --- a/scssphp/src/Ast/Sass/Statement/IfClause.php +++ /dev/null @@ -1,44 +0,0 @@ -expression = $expression; - parent::__construct($children); - } - - public function getExpression(): Expression - { - return $this->expression; - } -} diff --git a/scssphp/src/Ast/Sass/Statement/IfRule.php b/scssphp/src/Ast/Sass/Statement/IfRule.php deleted file mode 100644 index 1525694..0000000 --- a/scssphp/src/Ast/Sass/Statement/IfRule.php +++ /dev/null @@ -1,106 +0,0 @@ -clauses = $clauses; - $this->span = $span; - $this->lastClause = $lastClause; - } - - /** - * The `@if` and `@else if` clauses. - * - * The first clause whose expression evaluates to `true` will have its - * statements executed. If no expression evaluates to `true`, `lastClause` - * will be executed if it's not `null`. - * - * @return IfClause[] - */ - public function getClauses(): array - { - return $this->clauses; - } - - /** - * The final, unconditional `@else` clause. - * - * This is `null` if there is no unconditional `@else`. - * - * @return ElseClause|null - */ - public function getLastClause(): ?ElseClause - { - return $this->lastClause; - } - - public function getSpan(): FileSpan - { - return $this->span; - } - - public function accept(StatementVisitor $visitor) - { - return $visitor->visitIfRule($this); - } - - public function __toString(): string - { - $parts = []; - - foreach ($this->clauses as $index => $clause) { - $parts[] = ($index === 0 ? '@if ' : '@else if ') . $clause->getExpression() . '{' . implode(' ', $clause->getChildren()) . '}'; - } - - if ($this->lastClause !== null) { - $parts[] = $this->lastClause; - } - - return implode(' ', $parts); - } -} diff --git a/scssphp/src/Ast/Sass/Statement/IfRuleClause.php b/scssphp/src/Ast/Sass/Statement/IfRuleClause.php deleted file mode 100644 index da3047e..0000000 --- a/scssphp/src/Ast/Sass/Statement/IfRuleClause.php +++ /dev/null @@ -1,73 +0,0 @@ -children = $children; - - foreach ($children as $child) { - if ($child instanceof VariableDeclaration || $child instanceof FunctionRule || $child instanceof MixinRule) { - $this->declarations = true; - break; - } - - if ($child instanceof ImportRule) { - foreach ($child->getImports() as $import) { - if ($import instanceof DynamicImport) { - $this->declarations = true; - break 2; - } - } - } - } - } - - /** - * @return Statement[] - */ - final public function getChildren(): array - { - return $this->children; - } - - final public function hasDeclarations(): bool - { - return $this->declarations; - } -} diff --git a/scssphp/src/Ast/Sass/Statement/ImportRule.php b/scssphp/src/Ast/Sass/Statement/ImportRule.php deleted file mode 100644 index 99e5277..0000000 --- a/scssphp/src/Ast/Sass/Statement/ImportRule.php +++ /dev/null @@ -1,70 +0,0 @@ -imports = $imports; - $this->span = $span; - } - - /** - * @return Import[] - */ - public function getImports(): array - { - return $this->imports; - } - - public function getSpan(): FileSpan - { - return $this->span; - } - - public function accept(StatementVisitor $visitor) - { - return $visitor->visitImportRule($this); - } - - public function __toString(): string - { - return '@import ' . implode(', ', $this->imports) . ';'; - } -} diff --git a/scssphp/src/Ast/Sass/Statement/IncludeRule.php b/scssphp/src/Ast/Sass/Statement/IncludeRule.php deleted file mode 100644 index 3a346ec..0000000 --- a/scssphp/src/Ast/Sass/Statement/IncludeRule.php +++ /dev/null @@ -1,149 +0,0 @@ -name = $name; - $this->arguments = $arguments; - $this->span = $span; - $this->namespace = $namespace; - $this->content = $content; - } - - public function getNamespace(): ?string - { - return $this->namespace; - } - - public function getName(): string - { - return $this->name; - } - - public function getArguments(): ArgumentInvocation - { - return $this->arguments; - } - - public function getContent(): ?ContentBlock - { - return $this->content; - } - - public function getSpan(): FileSpan - { - return $this->span; - } - - public function getSpanWithoutContent(): FileSpan - { - if ($this->content === null) { - return $this->span; - } - - return SpanUtil::trim($this->span->getFile()->span($this->span->getStart()->getOffset(), $this->arguments->getSpan()->getEnd()->getOffset())); - } - - public function getNameSpan(): FileSpan - { - $startSpan = $this->span->getText()[0] === '+' ? SpanUtil::trimLeft($this->span->subspan(1)) : SpanUtil::withoutInitialAtRule($this->span); - - if ($this->namespace !== null) { - $startSpan = SpanUtil::withoutNamespace($startSpan); - } - - return SpanUtil::initialIdentifier($startSpan); - } - - public function getNamespaceSpan(): ?FileSpan - { - if ($this->namespace === null) { - return null; - } - - $startSpan = $this->span->getText()[0] === '+' - ? SpanUtil::trimLeft($this->span->subspan(1)) - : SpanUtil::withoutInitialAtRule($this->span); - - return SpanUtil::initialIdentifier($startSpan); - } - - public function accept(StatementVisitor $visitor) - { - return $visitor->visitIncludeRule($this); - } - - public function __toString(): string - { - $buffer = '@include '; - - if ($this->namespace !== null) { - $buffer .= $this->namespace . '.'; - } - $buffer .= $this->name; - - if (!$this->arguments->isEmpty()) { - $buffer .= "($this->arguments)"; - } - - $buffer .= $this->content === null ? ';' : ' ' . $this->content; - - return $buffer; - } -} diff --git a/scssphp/src/Ast/Sass/Statement/LoudComment.php b/scssphp/src/Ast/Sass/Statement/LoudComment.php deleted file mode 100644 index 743d9b1..0000000 --- a/scssphp/src/Ast/Sass/Statement/LoudComment.php +++ /dev/null @@ -1,57 +0,0 @@ -text = $text; - } - - public function getText(): Interpolation - { - return $this->text; - } - - public function getSpan(): FileSpan - { - return $this->text->getSpan(); - } - - public function accept(StatementVisitor $visitor) - { - return $visitor->visitLoudComment($this); - } - - public function __toString(): string - { - return (string) $this->text; - } -} diff --git a/scssphp/src/Ast/Sass/Statement/MediaRule.php b/scssphp/src/Ast/Sass/Statement/MediaRule.php deleted file mode 100644 index f846767..0000000 --- a/scssphp/src/Ast/Sass/Statement/MediaRule.php +++ /dev/null @@ -1,75 +0,0 @@ - - * - * @internal - */ -final class MediaRule extends ParentStatement -{ - /** - * @var Interpolation - * @readonly - */ - private $query; - - /** - * @var FileSpan - * @readonly - */ - private $span; - - /** - * @param Statement[] $children - */ - public function __construct(Interpolation $query, array $children, FileSpan $span) - { - $this->query = $query; - $this->span = $span; - parent::__construct($children); - } - - /** - * The query that determines on which platforms the styles will be in effect. - * - * This is only parsed after the interpolation has been resolved. - */ - public function getQuery(): Interpolation - { - return $this->query; - } - - public function getSpan(): FileSpan - { - return $this->span; - } - - public function accept(StatementVisitor $visitor) - { - return $visitor->visitMediaRule($this); - } - - public function __toString(): string - { - return '@media ' . $this->query . ' {' . implode(' ', $this->getChildren()) . '}'; - } -} diff --git a/scssphp/src/Ast/Sass/Statement/MixinRule.php b/scssphp/src/Ast/Sass/Statement/MixinRule.php deleted file mode 100644 index ec6ddda..0000000 --- a/scssphp/src/Ast/Sass/Statement/MixinRule.php +++ /dev/null @@ -1,81 +0,0 @@ -content)) { - $this->content = (new HasContentVisitor())->visitMixinRule($this) === true; - } - - return $this->content; - } - - public function getNameSpan(): FileSpan - { - $startSpan = $this->getSpan()->getText()[0] === '=' - ? SpanUtil::trimLeft($this->getSpan()->subspan(1)) - : SpanUtil::withoutInitialAtRule($this->getSpan()); - - return SpanUtil::initialIdentifier($startSpan); - } - - public function accept(StatementVisitor $visitor) - { - return $visitor->visitMixinRule($this); - } - - public function __toString(): string - { - $buffer = '@mixin ' . $this->getName(); - - if (!$this->getArguments()->isEmpty()) { - $buffer .= "({$this->getArguments()})"; - } - - $buffer .= ' {' . implode(' ', $this->getChildren()) . '}'; - - return $buffer; - } -} diff --git a/scssphp/src/Ast/Sass/Statement/ParentStatement.php b/scssphp/src/Ast/Sass/Statement/ParentStatement.php deleted file mode 100644 index 19838ac..0000000 --- a/scssphp/src/Ast/Sass/Statement/ParentStatement.php +++ /dev/null @@ -1,86 +0,0 @@ -children = $children; - - if ($children === null) { - $this->declarations = false; - return; - } - - foreach ($children as $child) { - if ($child instanceof VariableDeclaration || $child instanceof FunctionRule || $child instanceof MixinRule) { - $this->declarations = true; - return; - } - - if ($child instanceof ImportRule) { - foreach ($child->getImports() as $import) { - if ($import instanceof DynamicImport) { - $this->declarations = true; - return; - } - } - } - } - - $this->declarations = false; - } - - /** - * @return T - */ - final public function getChildren() - { - return $this->children; - } - - final public function hasDeclarations(): bool - { - return $this->declarations; - } -} diff --git a/scssphp/src/Ast/Sass/Statement/ReturnRule.php b/scssphp/src/Ast/Sass/Statement/ReturnRule.php deleted file mode 100644 index 3a2ee76..0000000 --- a/scssphp/src/Ast/Sass/Statement/ReturnRule.php +++ /dev/null @@ -1,66 +0,0 @@ -expression = $expression; - $this->span = $span; - } - - public function getExpression(): Expression - { - return $this->expression; - } - - public function getSpan(): FileSpan - { - return $this->span; - } - - public function accept(StatementVisitor $visitor) - { - return $visitor->visitReturnRule($this); - } - - public function __toString(): string - { - return '@return ' . $this->expression . ';'; - } -} diff --git a/scssphp/src/Ast/Sass/Statement/SilentComment.php b/scssphp/src/Ast/Sass/Statement/SilentComment.php deleted file mode 100644 index ec6f1af..0000000 --- a/scssphp/src/Ast/Sass/Statement/SilentComment.php +++ /dev/null @@ -1,63 +0,0 @@ -text = $text; - $this->span = $span; - } - - public function getText(): string - { - return $this->text; - } - - public function getSpan(): FileSpan - { - return $this->span; - } - - public function accept(StatementVisitor $visitor) - { - return $visitor->visitSilentComment($this); - } - - public function __toString(): string - { - return $this->text; - } -} diff --git a/scssphp/src/Ast/Sass/Statement/StyleRule.php b/scssphp/src/Ast/Sass/Statement/StyleRule.php deleted file mode 100644 index aec02de..0000000 --- a/scssphp/src/Ast/Sass/Statement/StyleRule.php +++ /dev/null @@ -1,77 +0,0 @@ - - * - * @internal - */ -final class StyleRule extends ParentStatement -{ - /** - * @var Interpolation - * @readonly - */ - private $selector; - - /** - * @var FileSpan - * @readonly - */ - private $span; - - /** - * @param Statement[] $children - */ - public function __construct(Interpolation $selector, array $children, FileSpan $span) - { - $this->selector = $selector; - $this->span = $span; - parent::__construct($children); - } - - /** - * The selector to which the declaration will be applied. - * - * This is only parsed after the interpolation has been resolved. - */ - public function getSelector(): Interpolation - { - return $this->selector; - } - - public function getSpan(): FileSpan - { - return $this->span; - } - - public function accept(StatementVisitor $visitor) - { - return $visitor->visitStyleRule($this); - } - - public function __toString(): string - { - return $this->selector . ' {' . implode(' ', $this->getChildren()) . '}'; - } -} diff --git a/scssphp/src/Ast/Sass/Statement/Stylesheet.php b/scssphp/src/Ast/Sass/Statement/Stylesheet.php deleted file mode 100644 index 95196d4..0000000 --- a/scssphp/src/Ast/Sass/Statement/Stylesheet.php +++ /dev/null @@ -1,126 +0,0 @@ - - * - * @internal - */ -final class Stylesheet extends ParentStatement -{ - /** - * @var bool - * @readonly - */ - private $plainCss; - - /** - * @var FileSpan - * @readonly - */ - private $span; - - /** - * @param Statement[] $children - */ - public function __construct(array $children, FileSpan $span, bool $plainCss = false) - { - $this->span = $span; - $this->plainCss = $plainCss; - parent::__construct($children); - } - - public function isPlainCss(): bool - { - return $this->plainCss; - } - - public function getSpan(): FileSpan - { - return $this->span; - } - - public function accept(StatementVisitor $visitor) - { - return $visitor->visitStylesheet($this); - } - - /** - * @param Syntax::* $syntax - * - * @throws SassFormatException when parsing fails - */ - public static function parse(string $contents, string $syntax, ?LoggerInterface $logger = null, ?string $sourceUrl = null): self - { - switch ($syntax) { - case Syntax::SASS: - return self::parseSass($contents, $logger, $sourceUrl); - - case Syntax::SCSS: - return self::parseScss($contents, $logger, $sourceUrl); - - case Syntax::CSS: - return self::parseCss($contents, $logger, $sourceUrl); - - default: - throw new \InvalidArgumentException("Unknown syntax $syntax."); - } - } - - /** - * @throws SassFormatException when parsing fails - */ - public static function parseSass(string $contents, ?LoggerInterface $logger = null, ?string $sourceUrl = null): self - { - $file = new SourceFile($contents, $sourceUrl); - $span = $file->span(0, 0); - - throw new SassFormatException('The Sass indented syntax is not implemented.', $span); - } - - /** - * @throws SassFormatException when parsing fails - */ - public static function parseScss(string $contents, ?LoggerInterface $logger = null, ?string $sourceUrl = null): self - { - return (new ScssParser($contents, $logger, $sourceUrl))->parse(); - } - - /** - * @throws SassFormatException when parsing fails - */ - public static function parseCss(string $contents, ?LoggerInterface $logger = null, ?string $sourceUrl = null): self - { - return (new CssParser($contents, $logger, $sourceUrl))->parse(); - } - - public function __toString(): string - { - return implode(' ', $this->getChildren()); - } -} diff --git a/scssphp/src/Ast/Sass/Statement/SupportsRule.php b/scssphp/src/Ast/Sass/Statement/SupportsRule.php deleted file mode 100644 index cfac6bd..0000000 --- a/scssphp/src/Ast/Sass/Statement/SupportsRule.php +++ /dev/null @@ -1,70 +0,0 @@ - - * - * @internal - */ -final class SupportsRule extends ParentStatement -{ - /** - * @var SupportsCondition - * @readonly - */ - private $condition; - - /** - * @var FileSpan - * @readonly - */ - private $span; - - /** - * @param Statement[] $children - */ - public function __construct(SupportsCondition $condition, array $children, FileSpan $span) - { - $this->condition = $condition; - $this->span = $span; - parent::__construct($children); - } - - public function getCondition(): SupportsCondition - { - return $this->condition; - } - - public function getSpan(): FileSpan - { - return $this->span; - } - - public function accept(StatementVisitor $visitor) - { - return $visitor->visitSupportsRule($this); - } - - public function __toString(): string - { - return '@supports ' . $this->condition . ' {' . implode(' ', $this->getChildren()) . '}'; - } -} diff --git a/scssphp/src/Ast/Sass/Statement/VariableDeclaration.php b/scssphp/src/Ast/Sass/Statement/VariableDeclaration.php deleted file mode 100644 index f5fc3fd..0000000 --- a/scssphp/src/Ast/Sass/Statement/VariableDeclaration.php +++ /dev/null @@ -1,174 +0,0 @@ -name = $name; - $this->expression = $expression; - $this->span = $span; - $this->namespace = $namespace; - $this->guarded = $guarded; - $this->global = $global; - $this->comment = $comment; - - if ($namespace !== null && $global) { - throw new \InvalidArgumentException("Other modules' members can't be defined with !global."); - } - } - - public function getNamespace(): ?string - { - return $this->namespace; - } - - /** - * The name of the variable, with underscores converted to hyphens. - */ - public function getName(): string - { - return $this->name; - } - - /** - * The variable name as written in the document, without underscores - * converted to hyphens and including the leading `$`. - * - * This isn't particularly efficient, and should only be used for error - * messages. - */ - public function getOriginalName(): string - { - return Util::declarationName($this->span); - } - - public function getComment(): ?SilentComment - { - return $this->comment; - } - - public function getExpression(): Expression - { - return $this->expression; - } - - public function isGuarded(): bool - { - return $this->guarded; - } - - public function isGlobal(): bool - { - return $this->global; - } - - public function getSpan(): FileSpan - { - return $this->span; - } - - public function getNameSpan(): FileSpan - { - $span = $this->span; - - if ($this->namespace !== null) { - $span = SpanUtil::withoutNamespace($span); - } - - return SpanUtil::initialIdentifier($span, 1); - } - - public function getNamespaceSpan(): ?FileSpan - { - if ($this->namespace === null) { - return null; - } - - return SpanUtil::initialIdentifier($this->span); - } - - public function accept(StatementVisitor $visitor) - { - return $visitor->visitVariableDeclaration($this); - } - - public function __toString(): string - { - $buffer = ''; - if ($this->namespace !== null) { - $buffer .= $this->namespace . '.'; - } - $buffer .= "\$$this->name: $this->expression;"; - - return $buffer; - } -} diff --git a/scssphp/src/Ast/Sass/Statement/WarnRule.php b/scssphp/src/Ast/Sass/Statement/WarnRule.php deleted file mode 100644 index ac0501c..0000000 --- a/scssphp/src/Ast/Sass/Statement/WarnRule.php +++ /dev/null @@ -1,66 +0,0 @@ -expression = $expression; - $this->span = $span; - } - - public function getExpression(): Expression - { - return $this->expression; - } - - public function getSpan(): FileSpan - { - return $this->span; - } - - public function accept(StatementVisitor $visitor) - { - return $visitor->visitWarnRule($this); - } - - public function __toString(): string - { - return '@warn ' . $this->expression . ';'; - } -} diff --git a/scssphp/src/Ast/Sass/Statement/WhileRule.php b/scssphp/src/Ast/Sass/Statement/WhileRule.php deleted file mode 100644 index e4cb376..0000000 --- a/scssphp/src/Ast/Sass/Statement/WhileRule.php +++ /dev/null @@ -1,73 +0,0 @@ - - * - * @internal - */ -final class WhileRule extends ParentStatement -{ - /** - * @var Expression - * @readonly - */ - private $condition; - - /** - * @var FileSpan - * @readonly - */ - private $span; - - /** - * @param Statement[] $children - */ - public function __construct(Expression $condition, array $children, FileSpan $span) - { - $this->condition = $condition; - $this->span = $span; - parent::__construct($children); - } - - public function getCondition(): Expression - { - return $this->condition; - } - - public function getSpan(): FileSpan - { - return $this->span; - } - - public function accept(StatementVisitor $visitor) - { - return $visitor->visitWhileRule($this); - } - - public function __toString(): string - { - return '@while ' . $this->condition . ' {' . implode(' ', $this->getChildren()) . '}'; - } -} diff --git a/scssphp/src/Ast/Sass/SupportsCondition.php b/scssphp/src/Ast/Sass/SupportsCondition.php deleted file mode 100644 index b30c210..0000000 --- a/scssphp/src/Ast/Sass/SupportsCondition.php +++ /dev/null @@ -1,22 +0,0 @@ -` production. - * - * @internal - */ -final class SupportsAnything implements SupportsCondition -{ - /** - * The contents of the condition. - * - * @var Interpolation - * @readonly - */ - private $contents; - - /** - * @var FileSpan - * @readonly - */ - private $span; - - public function __construct(Interpolation $contents, FileSpan $span) - { - $this->contents = $contents; - $this->span = $span; - } - - public function getContents(): Interpolation - { - return $this->contents; - } - - public function getSpan(): FileSpan - { - return $this->span; - } - - public function __toString(): string - { - return "($this->contents)"; - } -} diff --git a/scssphp/src/Ast/Sass/SupportsCondition/SupportsDeclaration.php b/scssphp/src/Ast/Sass/SupportsCondition/SupportsDeclaration.php deleted file mode 100644 index 46ca4a3..0000000 --- a/scssphp/src/Ast/Sass/SupportsCondition/SupportsDeclaration.php +++ /dev/null @@ -1,91 +0,0 @@ -name = $name; - $this->value = $value; - $this->span = $span; - } - - public function getName(): Expression - { - return $this->name; - } - - public function getValue(): Expression - { - return $this->value; - } - - public function getSpan(): FileSpan - { - return $this->span; - } - - /** - * Returns whether this is a CSS Custom Property declaration. - * - * Note that this can return `false` for declarations that will ultimately be - * serialized as custom properties if they aren't *parsed as* custom - * properties, such as `#{--foo}: ...`. - * - * If this is `true`, then `value` will be a {@see StringExpression}. - */ - public function isCustomProperty(): bool - { - return $this->name instanceof StringExpression && !$this->name->hasQuotes() && StringUtil::startsWith($this->name->getText()->getInitialPlain(), '--'); - } - - public function __toString(): string - { - return "($this->name: $this->value)"; - } -} diff --git a/scssphp/src/Ast/Sass/SupportsCondition/SupportsFunction.php b/scssphp/src/Ast/Sass/SupportsCondition/SupportsFunction.php deleted file mode 100644 index b22c238..0000000 --- a/scssphp/src/Ast/Sass/SupportsCondition/SupportsFunction.php +++ /dev/null @@ -1,74 +0,0 @@ -name = $name; - $this->arguments = $arguments; - $this->span = $span; - } - - public function getName(): Interpolation - { - return $this->name; - } - - public function getArguments(): Interpolation - { - return $this->arguments; - } - - public function getSpan(): FileSpan - { - return $this->span; - } - - public function __toString(): string - { - return "$this->name($this->arguments)"; - } -} diff --git a/scssphp/src/Ast/Sass/SupportsCondition/SupportsInterpolation.php b/scssphp/src/Ast/Sass/SupportsCondition/SupportsInterpolation.php deleted file mode 100644 index 7cc832a..0000000 --- a/scssphp/src/Ast/Sass/SupportsCondition/SupportsInterpolation.php +++ /dev/null @@ -1,60 +0,0 @@ -expression = $expression; - $this->span = $span; - } - - public function getExpression(): Expression - { - return $this->expression; - } - - public function getSpan(): FileSpan - { - return $this->span; - } - - public function __toString(): string - { - return '#{' . $this->expression . '}'; - } -} diff --git a/scssphp/src/Ast/Sass/SupportsCondition/SupportsNegation.php b/scssphp/src/Ast/Sass/SupportsCondition/SupportsNegation.php deleted file mode 100644 index eec20e6..0000000 --- a/scssphp/src/Ast/Sass/SupportsCondition/SupportsNegation.php +++ /dev/null @@ -1,63 +0,0 @@ -condition = $condition; - $this->span = $span; - } - - public function getCondition(): SupportsCondition - { - return $this->condition; - } - - public function getSpan(): FileSpan - { - return $this->span; - } - - public function __toString(): string - { - if ($this->condition instanceof SupportsNegation || $this->condition instanceof SupportsOperation) { - return "not ($this->condition)"; - } - - return 'not ' . $this->condition; - } -} diff --git a/scssphp/src/Ast/Sass/SupportsCondition/SupportsOperation.php b/scssphp/src/Ast/Sass/SupportsCondition/SupportsOperation.php deleted file mode 100644 index 11425b2..0000000 --- a/scssphp/src/Ast/Sass/SupportsCondition/SupportsOperation.php +++ /dev/null @@ -1,94 +0,0 @@ -left = $left; - $this->right = $right; - $this->operator = $operator; - $this->span = $span; - } - - public function getLeft(): SupportsCondition - { - return $this->left; - } - - public function getRight(): SupportsCondition - { - return $this->right; - } - - public function getOperator(): string - { - return $this->operator; - } - - public function getSpan(): FileSpan - { - return $this->span; - } - - public function __toString(): string - { - return $this->parenthesize($this->left) . ' ' . $this->operator . ' ' . $this->parenthesize($this->right); - } - - private function parenthesize(SupportsCondition $condition): string - { - if ($condition instanceof SupportsNegation || $condition instanceof SupportsOperation && $condition->operator === $this->operator) { - return "($condition)"; - } - - return (string) $condition; - } -} diff --git a/scssphp/src/Ast/Selector/AttributeOperator.php b/scssphp/src/Ast/Selector/AttributeOperator.php deleted file mode 100644 index 3c44e1f..0000000 --- a/scssphp/src/Ast/Selector/AttributeOperator.php +++ /dev/null @@ -1,51 +0,0 @@ -name = $name; - $this->op = $op; - $this->value = $value; - $this->modifier = $modifier; - } - - public function getName(): QualifiedName - { - return $this->name; - } - - /** - * @phpstan-return AttributeOperator::*|null - */ - public function getOp(): ?string - { - return $this->op; - } - - public function getValue(): ?string - { - return $this->value; - } - - public function getModifier(): ?string - { - return $this->modifier; - } - - public function accept(SelectorVisitor $visitor) - { - return $visitor->visitAttributeSelector($this); - } - - public function equals(object $other): bool - { - return $other instanceof AttributeSelector && - $other->name->equals($this->name) && - $other->op === $this->op && - $other->value === $this->value && - $other->modifier === $this->modifier; - } -} diff --git a/scssphp/src/Ast/Selector/ClassSelector.php b/scssphp/src/Ast/Selector/ClassSelector.php deleted file mode 100644 index f66a852..0000000 --- a/scssphp/src/Ast/Selector/ClassSelector.php +++ /dev/null @@ -1,57 +0,0 @@ -name = $name; - } - - public function getName(): string - { - return $this->name; - } - - public function accept(SelectorVisitor $visitor) - { - return $visitor->visitClassSelector($this); - } - - public function equals(object $other): bool - { - return $other instanceof ClassSelector && $other->name === $this->name; - } - - public function addSuffix(string $suffix): SimpleSelector - { - return new ClassSelector($this->name . $suffix); - } -} diff --git a/scssphp/src/Ast/Selector/Combinator.php b/scssphp/src/Ast/Selector/Combinator.php deleted file mode 100644 index d720355..0000000 --- a/scssphp/src/Ast/Selector/Combinator.php +++ /dev/null @@ -1,38 +0,0 @@ -'; - - /** - * Matches the right-hand selector if it comes after the left-hand selector - * in the DOM tree. - */ - public const FOLLOWING_SIBLING = '~'; -} diff --git a/scssphp/src/Ast/Selector/ComplexSelector.php b/scssphp/src/Ast/Selector/ComplexSelector.php deleted file mode 100644 index 5c13866..0000000 --- a/scssphp/src/Ast/Selector/ComplexSelector.php +++ /dev/null @@ -1,289 +0,0 @@ - - * @phpstan-var list - * @readonly - */ - private $leadingCombinators; - - /** - * The components of this selector. - * - * This is only empty if {@see $leadingCombinators} is not empty. - * - * Descendant combinators aren't explicitly represented here. If two - * {@see CompoundSelector}s are adjacent to one another, there's an implicit - * descendant combinator between them. - * - * It's possible for multiple {@see Combinator}s to be adjacent to one another. - * This isn't valid CSS, but Sass supports it for CSS hack purposes. - * - * @var list - * @readonly - */ - private $components; - - /** - * Whether a line break should be emitted *before* this selector. - * - * @var bool - * @readonly - */ - private $lineBreak; - - /** - * @var int|null - */ - private $specificity; - - /** - * @param list $leadingCombinators - * @param list $components - * @param bool $lineBreak - * - * @phpstan-param list $leadingCombinators - */ - public function __construct(array $leadingCombinators, array $components, bool $lineBreak = false) - { - if ($leadingCombinators === [] && $components === []) { - throw new \InvalidArgumentException('leadingCombinators and components may not both be empty.'); - } - - $this->leadingCombinators = $leadingCombinators; - $this->components = $components; - $this->lineBreak = $lineBreak; - } - - /** - * Parses a complex selector from $contents. - * - * If passed, $url is the name of the file from which $contents comes. - * $allowParent controls whether a {@see ParentSelector} is allowed in this - * selector. - * - * @throws SassFormatException if parsing fails. - */ - public static function parse(string $contents, ?LoggerInterface $logger = null, ?string $url = null, bool $allowParent = true): ComplexSelector - { - return (new SelectorParser($contents, $logger, $url, $allowParent))->parseComplexSelector(); - } - - /** - * @return list - * @phpstan-return list - */ - public function getLeadingCombinators(): array - { - return $this->leadingCombinators; - } - - /** - * @return list - */ - public function getComponents(): array - { - return $this->components; - } - - /** - * If this compound selector is composed of a single compound selector with - * no combinators, returns it. - * - * Otherwise, returns null. - * - * @return CompoundSelector|null - */ - public function getSingleCompound(): ?CompoundSelector - { - if (\count($this->leadingCombinators) === 0 && \count($this->components) === 1 && \count($this->components[0]->getCombinators()) === 0) { - return $this->components[0]->getSelector(); - } - - return null; - } - - public function getLastComponent(): ComplexSelectorComponent - { - if (\count($this->components) === 0) { - throw new \OutOfBoundsException('Cannot get the last component of an empty list.'); - } - - return $this->components[\count($this->components) - 1]; - } - - public function getLineBreak(): bool - { - return $this->lineBreak; - } - - /** - * This selector's specificity. - * - * Specificity is represented in base 1000. The spec says this should be - * "sufficiently high"; it's extremely unlikely that any single selector - * sequence will contain 1000 simple selectors. - */ - public function getSpecificity(): int - { - if ($this->specificity === null) { - $specificity = 0; - - foreach ($this->components as $component) { - $specificity += $component->getSelector()->getSpecificity(); - } - - $this->specificity = $specificity; - } - - return $this->specificity; - } - - public function accept(SelectorVisitor $visitor) - { - return $visitor->visitComplexSelector($this); - } - - /** - * Whether this is a superselector of $other. - * - * That is, whether this matches every element that $other matches, as well - * as possibly additional elements. - */ - public function isSuperselector(ComplexSelector $other): bool - { - return \count($this->leadingCombinators) === 0 && \count($other->leadingCombinators) ===0 && ExtendUtil::complexIsSuperselector($this->components, $other->components); - } - - public function equals(object $other): bool - { - return $other instanceof ComplexSelector && $this->leadingCombinators === $other->leadingCombinators && EquatableUtil::listEquals($this->components, $other->components); - } - - /** - * Returns a copy of `$this` with $combinators added to the end of the final - * component in {@see components}. - * - * If $forceLineBreak is `true`, this will mark the new complex selector as - * having a line break. - * - * @param list $combinators - * @param bool $forceLineBreak - * - * @return ComplexSelector - * - * @phpstan-param list $combinators - */ - public function withAdditionalCombinators(array $combinators, bool $forceLineBreak = false): ComplexSelector - { - if ($combinators === []) { - return $this; - } - - if ($this->components === []) { - return new ComplexSelector(array_merge($this->leadingCombinators, $combinators), [], $this->lineBreak || $forceLineBreak); - } - - return new ComplexSelector( - $this->leadingCombinators, - array_merge( - ListUtil::exceptLast($this->components), - [ListUtil::last($this->components)->withAdditionalCombinators($combinators)] - ), - $this->lineBreak || $forceLineBreak - ); - } - - /** - * Returns a copy of `$this` with an additional $component added to the end. - * - * If $forceLineBreak is `true`, this will mark the new complex selector as - * having a line break. - * - * @param ComplexSelectorComponent $component - * @param bool $forceLineBreak - * - * @return ComplexSelector - */ - public function withAdditionalComponent(ComplexSelectorComponent $component, bool $forceLineBreak = false): ComplexSelector - { - return new ComplexSelector($this->leadingCombinators, array_merge($this->components, [$component]), $this->lineBreak || $forceLineBreak); - } - - /** - * Returns a copy of `this` with $child's combinators added to the end. - * - * If $child has {@see leadingCombinators}, they're appended to `this`'s last - * combinator. This does _not_ resolve parent selectors. - * - * If $forceLineBreak is `true`, this will mark the new complex selector as - * having a line break. - * - * @param ComplexSelector $child - * @param bool $forceLineBreak - * - * @return ComplexSelector - */ - public function concatenate(ComplexSelector $child, bool $forceLineBreak = false): ComplexSelector - { - if (\count($child->leadingCombinators) === 0) { - return new ComplexSelector( - $this->leadingCombinators, - array_merge($this->components, $child->components), - $this->lineBreak || $child->lineBreak || $forceLineBreak - ); - } - - if (\count($this->components) === 0) { - return new ComplexSelector( - array_merge($this->leadingCombinators, $child->leadingCombinators), - $child->components, - $this->lineBreak || $child->lineBreak || $forceLineBreak - ); - } - - return new ComplexSelector( - $this->leadingCombinators, - array_merge( - ListUtil::exceptLast($this->components), - [ListUtil::last($this->components)->withAdditionalCombinators($child->leadingCombinators)], - $child->components - ), - $this->lineBreak || $child->lineBreak || $forceLineBreak - ); - } -} diff --git a/scssphp/src/Ast/Selector/ComplexSelectorComponent.php b/scssphp/src/Ast/Selector/ComplexSelectorComponent.php deleted file mode 100644 index feedc54..0000000 --- a/scssphp/src/Ast/Selector/ComplexSelectorComponent.php +++ /dev/null @@ -1,96 +0,0 @@ - - * @phpstan-var list - * @readonly - */ - private $combinators; - - /** - * @param CompoundSelector $selector - * @param list $combinators - * - * @phpstan-param list $combinators - */ - public function __construct(CompoundSelector $selector, array $combinators) - { - $this->selector = $selector; - $this->combinators = $combinators; - } - - public function getSelector(): CompoundSelector - { - return $this->selector; - } - - /** - * @return list - * @phpstan-return list - */ - public function getCombinators(): array - { - return $this->combinators; - } - - public function equals(object $other): bool - { - return $other instanceof ComplexSelectorComponent && $this->selector->equals($other->selector) && $this->combinators === $other->combinators; - } - - /** - * Returns a copy of $this with $combinators added to the end of - * `$this->combinators`. - * - * @param list $combinators - * - * @return ComplexSelectorComponent - * - * @phpstan-param list $combinators - */ - public function withAdditionalCombinators(array $combinators): ComplexSelectorComponent - { - if ($combinators === []) { - return $this; - } - - return new ComplexSelectorComponent($this->selector, array_merge($this->combinators, $combinators)); - } -} diff --git a/scssphp/src/Ast/Selector/CompoundSelector.php b/scssphp/src/Ast/Selector/CompoundSelector.php deleted file mode 100644 index cf30a4a..0000000 --- a/scssphp/src/Ast/Selector/CompoundSelector.php +++ /dev/null @@ -1,136 +0,0 @@ - - */ - private $components; - - /** - * @var int|null - */ - private $specificity; - - /** - * Parses a compound selector from $contents. - * - * If passed, $url is the name of the file from which $contents comes. - * $allowParent controls whether a {@see ParentSelector} is allowed in this - * selector. - * - * @throws SassFormatException if parsing fails. - */ - public static function parse(string $contents, ?LoggerInterface $logger = null, ?string $url = null, bool $allowParent = true): CompoundSelector - { - return (new SelectorParser($contents, $logger, $url, $allowParent))->parseCompoundSelector(); - } - - /** - * @param list $components - */ - public function __construct(array $components) - { - if ($components === []) { - throw new \InvalidArgumentException('components may not be empty.'); - } - - $this->components = $components; - } - - /** - * @return list - */ - public function getComponents(): array - { - return $this->components; - } - - public function getLastComponent(): SimpleSelector - { - return $this->components[\count($this->components) - 1]; - } - - /** - * This selector's specificity. - * - * Specificity is represented in base 1000. The spec says this should be - * "sufficiently high"; it's extremely unlikely that any single selector - * sequence will contain 1000 simple selectors. - */ - public function getSpecificity(): int - { - if ($this->specificity === null) { - $specificity = 0; - - foreach ($this->components as $component) { - $specificity += $component->getSpecificity(); - } - - $this->specificity = $specificity; - } - - return $this->specificity; - } - - /** - * If this compound selector is composed of a single simple selector, returns - * it. - * - * Otherwise, returns null. - */ - public function getSingleSimple(): ?SimpleSelector - { - return \count($this->components) === 1 ? $this->components[0] : null; - } - - public function accept(SelectorVisitor $visitor) - { - return $visitor->visitCompoundSelector($this); - } - - /** - * Whether this is a superselector of $other. - * - * That is, whether this matches every element that $other matches, as well - * as possibly additional elements. - */ - public function isSuperselector(CompoundSelector $other): bool - { - return ExtendUtil::compoundIsSuperselector($this, $other); - } - - public function equals(object $other): bool - { - return $other instanceof CompoundSelector && EquatableUtil::listEquals($this->components, $other->components); - } -} diff --git a/scssphp/src/Ast/Selector/IDSelector.php b/scssphp/src/Ast/Selector/IDSelector.php deleted file mode 100644 index c850790..0000000 --- a/scssphp/src/Ast/Selector/IDSelector.php +++ /dev/null @@ -1,73 +0,0 @@ -name = $name; - } - - public function getName(): string - { - return $this->name; - } - - public function getSpecificity(): int - { - return parent::getSpecificity() ** 2; - } - - public function accept(SelectorVisitor $visitor) - { - return $visitor->visitIDSelector($this); - } - - public function addSuffix(string $suffix): SimpleSelector - { - return new IDSelector($this->name . $suffix); - } - - public function unify(array $compound): ?array - { - // A given compound selector may only contain one ID. - foreach ($compound as $simple) { - if ($simple instanceof IDSelector && !$simple->equals($this)) { - return null; - } - } - - return parent::unify($compound); - } - - public function equals(object $other): bool - { - return $other instanceof IDSelector && $other->name === $this->name; - } -} diff --git a/scssphp/src/Ast/Selector/IsBogusVisitor.php b/scssphp/src/Ast/Selector/IsBogusVisitor.php deleted file mode 100644 index 460688c..0000000 --- a/scssphp/src/Ast/Selector/IsBogusVisitor.php +++ /dev/null @@ -1,67 +0,0 @@ -includeLeadingCombinator = $includeLeadingCombinator; - } - - public function visitComplexSelector(ComplexSelector $complex): bool - { - if (\count($complex->getComponents()) === 0) { - return \count($complex->getLeadingCombinators()) > 0; - } - - if (\count($complex->getLeadingCombinators()) > ($this->includeLeadingCombinator ? 0 : 1) || count($complex->getLastComponent()->getCombinators()) !== 0) { - return true; - } - - foreach ($complex->getComponents() as $component) { - if (\count($component->getCombinators()) > 1 || $component->getSelector()->accept($this)) { - return true; - } - } - - return false; - } - - public function visitPseudoSelector(PseudoSelector $pseudo): bool - { - $selector = $pseudo->getSelector(); - - if ($selector === null) { - return false; - } - - // The CSS spec specifically allows leading combinators in `:has()`. - return $pseudo->getName() === 'has' ? $selector->isBogusOtherThanLeadingCombinator() : $selector->isBogus(); - } -} diff --git a/scssphp/src/Ast/Selector/IsInvisibleVisitor.php b/scssphp/src/Ast/Selector/IsInvisibleVisitor.php deleted file mode 100644 index 3b7c9a4..0000000 --- a/scssphp/src/Ast/Selector/IsInvisibleVisitor.php +++ /dev/null @@ -1,72 +0,0 @@ -includeBogus = $includeBogus; - } - - public function visitSelectorList(SelectorList $list): bool - { - foreach ($list->getComponents() as $complex) { - if (!$this->visitComplexSelector($complex)) { - return false; - } - } - - return true; - } - - public function visitComplexSelector(ComplexSelector $complex): bool - { - return parent::visitComplexSelector($complex) || ($this->includeBogus && $complex->isBogusOtherThanLeadingCombinator()); - } - - public function visitPlaceholderSelector(PlaceholderSelector $placeholder): bool - { - return true; - } - - public function visitPseudoSelector(PseudoSelector $pseudo): bool - { - $selector = $pseudo->getSelector(); - - if ($selector === null) { - return false; - } - - // We don't consider `:not(%foo)` to be invisible because, semantically, it - // means "doesn't match this selector that matches nothing", so it's - // equivalent to *. If the entire compound selector is composed of `:not`s - // with invisible lists, the serializer emits it as `*`. - return $pseudo->getName() === 'not' ? ($this->includeBogus && $selector->isBogus()) : $selector->accept($this); - } -} diff --git a/scssphp/src/Ast/Selector/IsUselessVisitor.php b/scssphp/src/Ast/Selector/IsUselessVisitor.php deleted file mode 100644 index ff8a6da..0000000 --- a/scssphp/src/Ast/Selector/IsUselessVisitor.php +++ /dev/null @@ -1,43 +0,0 @@ -getLeadingCombinators()) > 1) { - return true; - } - - foreach ($complex->getComponents() as $component) { - if (\count($component->getCombinators()) > 1 || $component->getSelector()->accept($this)) { - return true; - } - } - - return false; - } - - public function visitPseudoSelector(PseudoSelector $pseudo): bool - { - return $pseudo->isBogus(); - } -} diff --git a/scssphp/src/Ast/Selector/ParentSelector.php b/scssphp/src/Ast/Selector/ParentSelector.php deleted file mode 100644 index 82e7d8b..0000000 --- a/scssphp/src/Ast/Selector/ParentSelector.php +++ /dev/null @@ -1,61 +0,0 @@ -suffix = $suffix; - } - - public function getSuffix(): ?string - { - return $this->suffix; - } - - public function equals(object $other): bool - { - return $other === $this; - } - - public function accept(SelectorVisitor $visitor) - { - return $visitor->visitParentSelector($this); - } - - public function unify(array $compound): ?array - { - throw new \LogicException("& doesn't support unification."); - } -} diff --git a/scssphp/src/Ast/Selector/PlaceholderSelector.php b/scssphp/src/Ast/Selector/PlaceholderSelector.php deleted file mode 100644 index e00c565..0000000 --- a/scssphp/src/Ast/Selector/PlaceholderSelector.php +++ /dev/null @@ -1,68 +0,0 @@ -name = $name; - } - - public function getName(): string - { - return $this->name; - } - - /** - * Returns whether this is a private selector (that is, whether it begins - * with `-` or `_`). - */ - public function isPrivate(): bool - { - return Character::isPrivate($this->name); - } - - public function accept(SelectorVisitor $visitor) - { - return $visitor->visitPlaceholderSelector($this); - } - - public function addSuffix(string $suffix): SimpleSelector - { - return new PlaceholderSelector($this->name . $suffix); - } - - public function equals(object $other): bool - { - return $other instanceof PlaceholderSelector && $other->name === $this->name; - } -} diff --git a/scssphp/src/Ast/Selector/PseudoSelector.php b/scssphp/src/Ast/Selector/PseudoSelector.php deleted file mode 100644 index ab1d7f7..0000000 --- a/scssphp/src/Ast/Selector/PseudoSelector.php +++ /dev/null @@ -1,358 +0,0 @@ -name = $name; - $this->isClass = !$element && !self::isFakePseudoElement($name); - $this->isSyntacticClass = !$element; - $this->argument = $argument; - $this->selector = $selector; - $this->normalizedName = Util::unvendor($name); - } - - /** - * Returns whether $name is the name of a pseudo-element that can be written - * with pseudo-class syntax (`:before`, `:after`, `:first-line`, or - * `:first-letter`) - */ - private static function isFakePseudoElement(string $name): bool - { - if ($name === '') { - return false; - } - - switch ($name[0]) { - case 'a': - case 'A': - return strtolower($name) === 'after'; - - case 'b': - case 'B': - return strtolower($name) === 'before'; - - case 'f': - case 'F': - $lowerCasedName = strtolower($name); - - return $lowerCasedName === 'first-line' || $lowerCasedName === 'first-letter'; - - default: - return false; - } - } - - public function getName(): string - { - return $this->name; - } - - public function getNormalizedName(): string - { - return $this->normalizedName; - } - - /** - * Whether this is a pseudo-class selector. - * - * This is `true` if and only if {@see isElement} is `false`. - */ - public function isClass(): bool - { - return $this->isClass; - } - - /** - * Whether this is a pseudo-element selector. - * - * This is `true` if and only if {@see isClass} is `false`. - */ - public function isElement(): bool - { - return !$this->isClass; - } - - /** - * Whether this is syntactically a pseudo-class selector. - * - * This is the same as {@see isClass} unless this selector is a pseudo-element - * that was written syntactically as a pseudo-class (`:before`, `:after`, - * `:first-line`, or `:first-letter`). - * - * This is `true` if and only if {@see isSyntacticElement} is `false`. - */ - public function isSyntacticClass(): bool - { - return $this->isSyntacticClass; - } - - /** - * Whether this is syntactically a pseudo-element selector. - * - * This is `true` if and only if {@see isSyntacticClass} is `false`. - */ - public function isSyntacticElement(): bool - { - return !$this->isSyntacticClass; - } - - /** - * Whether this is a valid `:host` selector. - * - * @internal - */ - public function isHost(): bool - { - return $this->isClass && $this->name === 'host'; - } - - /** - * Whether this is a valid `:host-context` selector. - * - * @internal - */ - public function isHostContext(): bool - { - return $this->isClass && $this->name === 'host-context' && $this->selector !== null; - } - - public function getArgument(): ?string - { - return $this->argument; - } - - public function getSelector(): ?SelectorList - { - return $this->selector; - } - - public function getSpecificity(): int - { - if ($this->specificity === null) { - $this->specificity = $this->computeSpecificity(); - } - - return $this->specificity; - } - - private function computeSpecificity(): int - { - if ($this->isElement()) { - return 1; - } - - $selector = $this->selector; - - if ($selector === null) { - return parent::getSpecificity(); - } - - // https://www.w3.org/TR/selectors-4/#specificity-rules - switch ($this->normalizedName) { - case 'where': - return 0; - case 'is': - case 'not': - case 'has': - case 'matches': - $maxSpecificity = 0; - - foreach ($selector->getComponents() as $complex) { - $maxSpecificity = max($maxSpecificity, $complex->getSpecificity()); - } - - return $maxSpecificity; - case 'nth-child': - case 'nth-last-child': - $maxSpecificity = 0; - - foreach ($selector->getComponents() as $complex) { - $maxSpecificity = max($maxSpecificity, $complex->getSpecificity()); - } - - return parent::getSpecificity() + $maxSpecificity; - default: - return parent::getSpecificity(); - } - } - - public function withSelector(SelectorList $selector): PseudoSelector - { - return new PseudoSelector($this->name, $this->isElement(), $this->argument, $selector); - } - - public function addSuffix(string $suffix): SimpleSelector - { - if ($this->argument !== null || $this->selector !== null) { - parent::addSuffix($suffix); - } - - return new PseudoSelector($this->name . $suffix, $this->isElement()); - } - - public function unify(array $compound): ?array - { - if ($this->name === 'host' || $this->name === 'host-context') { - foreach ($compound as $simple) { - if (!$simple instanceof PseudoSelector || (!$simple->isHost() && $simple->selector === null)) { - return null; - } - } - } elseif (\count($compound) === 1) { - $other = $compound[0]; - - if ($other instanceof UniversalSelector || $other instanceof PseudoSelector && ($other->isHost() || $other->isHostContext())) { - return $other->unify([$this]); - } - } - - if (EquatableUtil::listContains($compound, $this)) { - return $compound; - } - - $result = []; - $addedThis = false; - - foreach ($compound as $simple) { - if ($simple instanceof PseudoSelector && $simple->isElement()) { - // A given compound selector may only contain one pseudo element. If - // $compound has a different one than $this, unification fails. - if ($this->isElement()) { - return null; - } - - // Otherwise, this is a pseudo selector and should come before pseudo - // elements. - $result[] = $this; - $addedThis = true; - } - - $result[] = $simple; - } - - if (!$addedThis) { - $result[] = $this; - } - - return $result; - } - - public function isSuperselector(SimpleSelector $other): bool - { - if (parent::isSuperselector($other)) { - return true; - } - - $selector = $this->selector; - - if ($selector === null) { - return $this === $other || $this->equals($other); - } - - if ($other instanceof PseudoSelector && $this->isElement() && $other->isElement() && $this->normalizedName === 'slotted' && $other->name === $this->name) { - if ($other->getSelector() !== null) { - return $selector->isSuperselector($other->getSelector()); - } - - return false; - } - - // Fall back to the logic defined in ExtendUtil, which knows how to - // compare selector pseudoclasses against raw selectors. - return (new CompoundSelector([$this]))->isSuperselector(new CompoundSelector([$other])); - } - - public function accept(SelectorVisitor $visitor) - { - return $visitor->visitPseudoSelector($this); - } - - public function equals(object $other): bool - { - return $other instanceof PseudoSelector && - $other->name === $this->name && - $other->isClass === $this->isClass && - $other->argument === $this->argument && - ($this->selector === $other->selector || ($this->selector !== null && $other->selector !== null && $this->selector->equals($other->selector))); - } -} diff --git a/scssphp/src/Ast/Selector/QualifiedName.php b/scssphp/src/Ast/Selector/QualifiedName.php deleted file mode 100644 index e250001..0000000 --- a/scssphp/src/Ast/Selector/QualifiedName.php +++ /dev/null @@ -1,69 +0,0 @@ -name = $name; - $this->namespace = $namespace; - } - - public function getName(): string - { - return $this->name; - } - - public function getNamespace(): ?string - { - return $this->namespace; - } - - public function __toString(): string - { - return $this->namespace === null ? $this->name : $this->namespace . '|' . $this->name; - } - - public function equals(object $other): bool - { - return $other instanceof QualifiedName && $other->name === $this->name && $other->namespace === $this->namespace; - } -} diff --git a/scssphp/src/Ast/Selector/Selector.php b/scssphp/src/Ast/Selector/Selector.php deleted file mode 100644 index ab36188..0000000 --- a/scssphp/src/Ast/Selector/Selector.php +++ /dev/null @@ -1,109 +0,0 @@ -accept(new IsInvisibleVisitor(true)); - } - - /** - * Whether this selector would be invisible even if it didn't have bogus - * combinators. - */ - public function isInvisibleOtherThanBogusCombinators(): bool - { - return $this->accept(new IsInvisibleVisitor(false)); - } - - /** - * Whether this selector is not valid CSS. - * - * This includes both selectors that are useful exclusively for build-time - * nesting (`> .foo)` and selectors with invalid combinators that are still - * supported for backwards-compatibility reasons (`.foo + ~ .bar`). - */ - public function isBogus(): bool - { - return $this->accept(new IsBogusVisitor(true)); - } - - /** - * Whether this selector is bogus other than having a leading combinator. - */ - public function isBogusOtherThanLeadingCombinator(): bool - { - return $this->accept(new IsBogusVisitor(false)); - } - - /** - * Whether this is a useless selector (that is, it's bogus _and_ it can't be - * transformed into valid CSS by `@extend` or nesting). - */ - public function isUseless(): bool - { - return $this->accept(new IsUselessVisitor()); - } - - /** - * Prints a warning if $this is a bogus selector. - * - * This may only be called from within a custom Sass function. This will - * throw a {@see SassScriptException} in a future major version. - */ - public function assertNotBogus(?string $name = null): void - { - if (!$this->isBogus()) { - return; - } - - Warn::deprecation(($name === null ? '' : "\$$name: ") . "$this is not valid CSS.\nThis will be an error in Dart Sass 2.0.0.\n\nMore info: https://sass-lang.com/d/bogus-combinators"); - } - - /** - * Calls the appropriate visit method on $visitor. - * - * @template T - * - * @param SelectorVisitor $visitor - * - * @return T - * - * @internal - */ - abstract public function accept(SelectorVisitor $visitor); - - final public function __toString(): string - { - return Serializer::serializeSelector($this, true); - } -} diff --git a/scssphp/src/Ast/Selector/SelectorList.php b/scssphp/src/Ast/Selector/SelectorList.php deleted file mode 100644 index d2a9222..0000000 --- a/scssphp/src/Ast/Selector/SelectorList.php +++ /dev/null @@ -1,355 +0,0 @@ - - * @readonly - */ - private $components; - - /** - * Parses a selector list from $contents. - * - * If passed, $url is the name of the file from which $contents comes. - * $allowParent and $allowPlaceholder control whether {@see ParentSelector}s or - * {@see PlaceholderSelector}s are allowed in this selector, respectively. - * - * @throws SassFormatException if parsing fails. - */ - public static function parse(string $contents, ?LoggerInterface $logger = null, ?string $url = null, bool $allowParent = true, bool $allowPlaceholder = true): SelectorList - { - return (new SelectorParser($contents, $logger, $url, $allowParent, $allowPlaceholder))->parse(); - } - - /** - * @param list $components - */ - public function __construct(array $components) - { - if ($components === []) { - throw new \InvalidArgumentException('components may not be empty.'); - } - - $this->components = $components; - } - - /** - * @return list - */ - public function getComponents(): array - { - return $this->components; - } - - /** - * Returns a SassScript list that represents this selector. - * - * This has the same format as a list returned by `selector-parse()`. - */ - public function asSassList(): SassList - { - return new SassList(array_map(static function (ComplexSelector $complex) { - $result = []; - foreach ($complex->getLeadingCombinators() as $combinator) { - $result[] = new SassString($combinator, false); - } - foreach ($complex->getComponents() as $component) { - $result[] = new SassString((string) $component->getSelector(), false); - - foreach ($component->getCombinators() as $combinator) { - $result[] = new SassString($combinator, false); - } - } - - return new SassList($result, ListSeparator::SPACE); - }, $this->components), ListSeparator::COMMA); - } - - public function accept(SelectorVisitor $visitor) - { - return $visitor->visitSelectorList($this); - } - - /** - * Returns a {@see SelectorList} that matches only elements that are matched by - * both this and $other. - * - * If no such list can be produced, returns `null`. - */ - public function unify(SelectorList $other): ?SelectorList - { - $contents = []; - - foreach ($this->components as $complex1) { - foreach ($other->components as $complex2) { - $unified = ExtendUtil::unifyComplex([$complex1, $complex2]); - - if ($unified === null) { - continue; - } - - foreach ($unified as $complex) { - $contents[] = $complex; - } - } - } - - return \count($contents) === 0 ? null : new SelectorList($contents); - } - - /** - * Returns a new list with all {@see ParentSelector}s replaced with $parent. - * - * If $implicitParent is true, this treats [ComplexSelector]s that don't - * contain an explicit {@see ParentSelector} as though they began with one. - * - * The given $parent may be `null`, indicating that this has no parents. If - * so, this list is returned as-is if it doesn't contain any explicit - * {@see ParentSelector}s. If it does, this throws a {@see SassScriptException}. - */ - public function resolveParentSelectors(?SelectorList $parent, bool $implicitParent = true): SelectorList - { - if ($parent === null) { - if (!$this->containsParentSelector()) { - return $this; - } - - throw new SassScriptException('Top-level selectors may not contain the parent selector "&".'); - } - - return new SelectorList(ListUtil::flattenVertically(array_map(function (ComplexSelector $complex) use ($parent, $implicitParent) { - if (!self::complexContainsParentSelector($complex)) { - if (!$implicitParent) { - return [$complex]; - } - - return array_map(function (ComplexSelector $parentComplex) use ($complex) { - return $parentComplex->concatenate($complex); - }, $parent->getComponents()); - } - - /** @var list $newComplexes */ - $newComplexes = []; - - foreach ($complex->getComponents() as $component) { - $resolved = self::resolveParentSelectorsCompound($component, $parent); - if ($resolved === null) { - if (\count($newComplexes) === 0) { - $newComplexes[] = new ComplexSelector($complex->getLeadingCombinators(), [$component], false); - } else { - foreach ($newComplexes as $i => $newComplex) { - $newComplexes[$i] = $newComplex->withAdditionalComponent($component); - } - } - } elseif (\count($newComplexes) === 0) { - $newComplexes = $resolved; - } else { - $previousComplexes = $newComplexes; - $newComplexes = []; - - foreach ($previousComplexes as $newComplex) { - foreach ($resolved as $resolvedComplex) { - $newComplexes[] = $newComplex->concatenate($resolvedComplex); - } - } - } - } - - return $newComplexes; - }, $this->components))); - } - - /** - * Whether this is a superselector of $other. - * - * That is, whether this matches every element that $other matches, as well - * as possibly additional elements. - */ - public function isSuperselector(SelectorList $other): bool - { - return ExtendUtil::listIsSuperselector($this->components, $other->components); - } - - public function equals(object $other): bool - { - return $other instanceof SelectorList && EquatableUtil::listEquals($this->components, $other->components); - } - - /** - * Whether this contains a {@see ParentSelector}. - */ - private function containsParentSelector(): bool - { - foreach ($this->components as $component) { - if (self::complexContainsParentSelector($component)) { - return true; - } - } - - return false; - } - - /** - * Returns whether $complex contains a {@see ParentSelector}. - */ - private static function complexContainsParentSelector(ComplexSelector $complex): bool - { - foreach ($complex->getComponents() as $component) { - foreach ($component->getSelector()->getComponents() as $simple) { - if ($simple instanceof ParentSelector) { - return true; - } - - if (!$simple instanceof PseudoSelector) { - continue; - } - - $selector = $simple->getSelector(); - if ($selector !== null && $selector->containsParentSelector()) { - return true; - } - } - } - - return false; - } - - /** - * Returns a new selector list based on $component with all - * {@see ParentSelector}s replaced with $parent. - * - * Returns `null` if $component doesn't contain any {@see ParentSelector}s. - * - * @return list|null - */ - private static function resolveParentSelectorsCompound(ComplexSelectorComponent $component, SelectorList $parent): ?array - { - $simples = $component->getSelector()->getComponents(); - $containsSelectorPseudo = false; - foreach ($simples as $simple) { - if (!$simple instanceof PseudoSelector) { - continue; - } - $selector = $simple->getSelector(); - - if ($selector !== null && $selector->containsParentSelector()) { - $containsSelectorPseudo = true; - break; - } - } - - if (!$containsSelectorPseudo && !$simples[0] instanceof ParentSelector) { - return null; - } - - if ($containsSelectorPseudo) { - $resolvedSimples = array_map(function (SimpleSelector $simple) use ($parent): SimpleSelector { - if (!$simple instanceof PseudoSelector) { - return $simple; - } - - $selector = $simple->getSelector(); - if ($selector === null) { - return $simple; - } - if (!$selector->containsParentSelector()) { - return $simple; - } - - return $simple->withSelector($selector->resolveParentSelectors($parent, false)); - }, $simples); - } else { - $resolvedSimples = $simples; - } - - $parentSelector = $simples[0]; - - if (!$parentSelector instanceof ParentSelector) { - return [new ComplexSelector([], [new ComplexSelectorComponent(new CompoundSelector($resolvedSimples), $component->getCombinators())])]; - } - - if (\count($simples) === 1 && $parentSelector->getSuffix() === null) { - return $parent->withAdditionalCombinators($component->getCombinators())->getComponents(); - } - - return array_map(function (ComplexSelector $complex) use ($parentSelector, $resolvedSimples, $component) { - $lastComponent = $complex->getLastComponent(); - - if (\count($lastComponent->getCombinators()) !== 0) { - throw new SassScriptException("Parent \"$complex\" is incompatible with this selector."); - } - - $suffix = $parentSelector->getSuffix(); - $lastSimples = $lastComponent->getSelector()->getComponents(); - - if ($suffix !== null) { - $last = new CompoundSelector(array_merge( - ListUtil::exceptLast($lastSimples), - [ListUtil::last($lastSimples)->addSuffix($suffix)], - array_slice($resolvedSimples, 1) - )); - } else { - $last = new CompoundSelector(array_merge($lastSimples, array_slice($resolvedSimples, 1))); - } - - $components = ListUtil::exceptLast($complex->getComponents()); - $components[] = new ComplexSelectorComponent($last, $component->getCombinators()); - - return new ComplexSelector($complex->getLeadingCombinators(), $components, $complex->getLineBreak()); - }, $parent->getComponents()); - } - - /** - * Returns a copy of `this` with $combinators added to the end of each - * complex selector in {@see components}]. - * - * @param list $combinators - * - * @phpstan-param list $combinators - */ - public function withAdditionalCombinators(array $combinators): SelectorList - { - if ($combinators === []) { - return $this; - } - - return new SelectorList(array_map(function (ComplexSelector $complex) use ($combinators) { - return $complex->withAdditionalCombinators($combinators); - }, $this->components)); - } -} diff --git a/scssphp/src/Ast/Selector/SimpleSelector.php b/scssphp/src/Ast/Selector/SimpleSelector.php deleted file mode 100644 index d532792..0000000 --- a/scssphp/src/Ast/Selector/SimpleSelector.php +++ /dev/null @@ -1,161 +0,0 @@ -parseSimpleSelector(); - } - - /** - * This selector's specificity. - * - * Specificity is represented in base 1000. The spec says this should be - * "sufficiently high"; it's extremely unlikely that any single selector - * sequence will contain 1000 simple selectors. - */ - public function getSpecificity(): int - { - return 1000; - } - - /** - * Returns a new {@see SimpleSelector} based on $this, as though it had been - * written with $suffix at the end. - * - * Assumes $suffix is a valid identifier suffix. If this wouldn't produce a - * valid SimpleSelector, throws a {@see SassScriptException}. - * - * @throws SassScriptException - */ - public function addSuffix(string $suffix): SimpleSelector - { - throw new SassScriptException("Invalid parent selector \"$this\""); - } - - /** - * Returns the components of a {@see CompoundSelector} that matches only elements - * matched by both this and $compound. - * - * By default, this just returns a copy of $compound with this selector - * added to the end, or returns the original array if this selector already - * exists in it. - * - * Returns `null` if unification is impossible—for example, if there are - * multiple ID selectors. - * - * @param list $compound - * - * @return list|null - */ - public function unify(array $compound): ?array - { - if (\count($compound) === 1) { - $other = $compound[0]; - - if ($other instanceof UniversalSelector || $other instanceof PseudoSelector && ($other->isHost() || $other->isHostContext())) { - return $other->unify([$this]); - } - } - - if (EquatableUtil::listContains($compound, $this)) { - return $compound; - } - - $result = []; - $addedThis = false; - - foreach ($compound as $simple) { - // Make sure pseudo selectors always come last. - if (!$addedThis && $simple instanceof PseudoSelector) { - $result[] = $this; - $addedThis = true; - } - - $result[] = $simple; - } - - if (!$addedThis) { - $result[] = $this; - } - - return $result; - } - - public function isSuperselector(SimpleSelector $other): bool - { - if ($this === $other || $this->equals($other)) { - return true; - } - - if ($other instanceof PseudoSelector && $other->isClass()) { - $list = $other->getSelector(); - - if ($list !== null && \in_array($other->getNormalizedName(), self::SUBSELECTOR_PSEUDOS, true)) { - foreach ($list->getComponents() as $complex) { - if (\count($complex->getComponents()) === 0) { - return false; - } - - foreach (ListUtil::last($complex->getComponents())->getSelector()->getComponents() as $simple) { - if ($this->isSuperselector($simple)) { - continue 2; - } - } - - return false; - } - - return true; - } - } - - return false; - } -} diff --git a/scssphp/src/Ast/Selector/TypeSelector.php b/scssphp/src/Ast/Selector/TypeSelector.php deleted file mode 100644 index c6b586e..0000000 --- a/scssphp/src/Ast/Selector/TypeSelector.php +++ /dev/null @@ -1,86 +0,0 @@ -name = $name; - } - - public function getName(): QualifiedName - { - return $this->name; - } - - public function getSpecificity(): int - { - return 1; - } - - public function accept(SelectorVisitor $visitor) - { - return $visitor->visitTypeSelector($this); - } - - public function addSuffix(string $suffix): SimpleSelector - { - return new TypeSelector(new QualifiedName($this->name->getName() . $suffix, $this->name->getNamespace())); - } - - public function unify(array $compound): ?array - { - $first = $compound[0] ?? null; - - if ($first instanceof UniversalSelector || $first instanceof TypeSelector) { - $unified = ExtendUtil::unifyUniversalAndElement($this, $first); - - if ($unified === null) { - return null; - } - - $compound[0] = $unified; - - return $compound; - } - - return array_merge([$this], $compound); - } - - public function isSuperselector(SimpleSelector $other): bool - { - return parent::isSuperselector($other) || ($other instanceof TypeSelector && $this->name->getName() === $other->getName()->getName() && ($this->name->getNamespace() === '*' || $this->name->getNamespace() === $other->getName()->getNamespace())); - } - - public function equals(object $other): bool - { - return $other instanceof TypeSelector && $other->name->equals($this->name); - } -} diff --git a/scssphp/src/Ast/Selector/UniversalSelector.php b/scssphp/src/Ast/Selector/UniversalSelector.php deleted file mode 100644 index 5259027..0000000 --- a/scssphp/src/Ast/Selector/UniversalSelector.php +++ /dev/null @@ -1,109 +0,0 @@ -namespace = $namespace; - } - - public function getNamespace(): ?string - { - return $this->namespace; - } - - public function getSpecificity(): int - { - return 0; - } - - public function accept(SelectorVisitor $visitor) - { - return $visitor->visitUniversalSelector($this); - } - - public function unify(array $compound): ?array - { - $first = $compound[0] ?? null; - - if ($first instanceof UniversalSelector || $first instanceof TypeSelector) { - $unified = ExtendUtil::unifyUniversalAndElement($this, $first); - - if ($unified === null) { - return null; - } - - $compound[0] = $unified; - - return $compound; - } - - if (\count($compound) === 1 && $first instanceof PseudoSelector && ($first->isHost() || $first->isHostContext())) { - return null; - } - - if ($this->namespace !== null && $this->namespace !== '*') { - return array_merge([$this], $compound); - } - - // Not-empty compound list - if ($first !== null) { - return $compound; - } - - return [$this]; - } - - public function isSuperselector(SimpleSelector $other): bool - { - if ($this->namespace === '*') { - return true; - } - - if ($other instanceof TypeSelector) { - return $this->namespace === $other->getName()->getNamespace(); - } - - if ($other instanceof UniversalSelector) { - return $this->namespace === $other->namespace; - } - - return $this->namespace === null || parent::isSuperselector($other); - } - - public function equals(object $other): bool - { - return $other instanceof UniversalSelector && $other->namespace === $this->namespace; - } -} diff --git a/scssphp/src/Collection/Map.php b/scssphp/src/Collection/Map.php deleted file mode 100644 index 4b6f3df..0000000 --- a/scssphp/src/Collection/Map.php +++ /dev/null @@ -1,163 +0,0 @@ - - */ -final class Map implements \Countable, \IteratorAggregate -{ - /** - * @var bool - */ - private $modifiable = true; - - // TODO implement a better internal storage to allow reading keys in O(1). - - /** - * @var array - */ - private $pairs = []; - - /** - * Returns a modifiable version of the Map. - * - * @template V - * @param Map $map - * - * @return Map - */ - public static function of(Map $map): Map - { - $modifiableMap = clone $map; - $modifiableMap->modifiable = true; - - return $modifiableMap; - } - - /** - * Returns an unmodifiable version of the Map. - * - * All mutators will throw a LogicException when trying to use them. - * - * @template V - * @param Map $map - * - * @return Map - */ - public static function unmodifiable(Map $map): Map - { - if (!$map->modifiable) { - return $map; - } - - $unmodifiableMap = clone $map; - $unmodifiableMap->modifiable = false; - - return $unmodifiableMap; - } - - public function getIterator(): \Traversable - { - foreach ($this->pairs as $pair) { - yield $pair[0] => $pair[1]; - } - } - - public function count(): int - { - return \count($this->pairs); - } - - /** - * The value for the given key, or `null` if $key is not in the map. - * - * @return T|null - */ - public function get(Value $key) - { - foreach ($this->pairs as $pair) { - if ($key->equals($pair[0])) { - return $pair[1]; - } - } - - return null; - } - - /** - * Associates the key with the given value. - * - * If the key was already in the map, its associated value is changed. - * Otherwise the key/value pair is added to the map. - * - * @param T $value - */ - public function put(Value $key, $value): void - { - $this->assertModifiable(); - - foreach ($this->pairs as $i => $pair) { - if ($key->equals($pair[0])) { - $this->pairs[$i][1] = $value; - - return; - } - } - - $this->pairs[] = [$key, $value]; - } - - /** - * Removes $key and its associated value, if present, from the map. - * - * Returns the value associated with `key` before it was removed. - * Returns `null` if `key` was not in the map. - * - * Note that some maps allow `null` as a value, - * so a returned `null` value doesn't always mean that the key was absent. - * - * @return T|null - */ - public function remove(Value $key) - { - $this->assertModifiable(); - - foreach ($this->pairs as $i => $pair) { - if ($key->equals($pair[0])) { - unset($this->pairs[$i]); - - return $pair[1]; - } - } - - return null; - } - - private function assertModifiable(): void - { - if (!$this->modifiable) { - throw new \LogicException('Mutating an unmodifiable Map is not supported. Use Map::of to create a modifiable copy.'); - } - } -} diff --git a/scssphp/src/Exception/SassFormatException.php b/scssphp/src/Exception/SassFormatException.php deleted file mode 100644 index e0100a3..0000000 --- a/scssphp/src/Exception/SassFormatException.php +++ /dev/null @@ -1,54 +0,0 @@ -originalMessage = $message; - $this->span = $span; - - parent::__construct($span->message($message), 0, $previous); - } - - /** - * Gets the original message without the location info in it. - */ - public function getOriginalMessage(): string - { - return $this->originalMessage; - } - - public function getSpan(): FileSpan - { - return $this->span; - } -} diff --git a/scssphp/src/Exception/SassRuntimeException.php b/scssphp/src/Exception/SassRuntimeException.php deleted file mode 100644 index c799ce7..0000000 --- a/scssphp/src/Exception/SassRuntimeException.php +++ /dev/null @@ -1,54 +0,0 @@ -originalMessage = $message; - $this->span = $span; - - parent::__construct($span->message($message), 0, $previous); - } - - /** - * Gets the original message without the location info in it. - */ - public function getOriginalMessage(): string - { - return $this->originalMessage; - } - - public function getSpan(): FileSpan - { - return $this->span; - } -} diff --git a/scssphp/src/Extend/ExtendUtil.php b/scssphp/src/Extend/ExtendUtil.php deleted file mode 100644 index 44d1b24..0000000 --- a/scssphp/src/Extend/ExtendUtil.php +++ /dev/null @@ -1,1256 +0,0 @@ - $complexes - * - * @return list|null - */ - public static function unifyComplex(array $complexes): ?array - { - if (\count($complexes) === 1) { - return $complexes; - } - - $unifiedBase = null; - $leadingCombinator = null; - $trailingCombinator = null; - - foreach ($complexes as $complex) { - if ($complex->isUseless()) { - return null; - } - - if (\count($complex->getComponents()) === 1 && \count($complex->getLeadingCombinators()) !== 0) { - $newLeadingCombinator = \count($complex->getLeadingCombinators()) === 1 ? $complex->getLeadingCombinators()[0] : null; - if ($leadingCombinator !== null && $newLeadingCombinator !== $leadingCombinator) { - return null; - } - - $leadingCombinator = $newLeadingCombinator; - } - - $base = $complex->getLastComponent(); - - if (\count($base->getCombinators()) !== 0) { - $newTrailingCombinator = \count($base->getCombinators()) === 1 ? $base->getCombinators()[0] : null; - - if ($trailingCombinator !== null && $newTrailingCombinator !== $trailingCombinator) { - return null; - } - - $trailingCombinator = $newTrailingCombinator; - } - - if ($unifiedBase === null) { - $unifiedBase = $base->getSelector()->getComponents(); - } else { - foreach ($base->getSelector()->getComponents() as $simple) { - $unifiedBase = $simple->unify($unifiedBase); - - if ($unifiedBase === null) { - return null; - } - } - } - } - - $withoutBases = []; - $hasLineBreak = false; - foreach ($complexes as $complex) { - if (\count($complex->getComponents()) > 1) { - $withoutBases[] = new ComplexSelector($complex->getLeadingCombinators(), array_slice($complex->getComponents(), 0, \count($complex->getComponents()) - 1), $complex->getLineBreak()); - } - - if ($complex->getLineBreak()) { - $hasLineBreak = true; - } - } - - \assert($unifiedBase !== null); - - $base = new ComplexSelector( - $leadingCombinator === null ? [] : [$leadingCombinator], - [new ComplexSelectorComponent(new CompoundSelector($unifiedBase), $trailingCombinator === null ? [] : [$trailingCombinator])], - $hasLineBreak - ); - - return self::weave($withoutBases === [] ? [$base] : array_merge(ListUtil::exceptLast($withoutBases), [ListUtil::last($withoutBases)->concatenate($base)])); - } - - /** - * Returns a {@see CompoundSelector} that matches only elements that are matched by - * both $compound1 and $compound2. - * - * If no such selector can be produced, returns `null`. - * - * @param list $compound1 - * @param list $compound2 - * - * @return CompoundSelector|null - */ - public static function unifyCompound(array $compound1, array $compound2): ?CompoundSelector - { - $result = $compound2; - - foreach ($compound1 as $simple) { - $unified = $simple->unify($result); - - if ($unified === null) { - return null; - } - - $result = $unified; - } - - return new CompoundSelector($result); - } - - /** - * Returns a {@see SimpleSelector} that matches only elements that are matched by - * both $selector1 and $selector2, which must both be either - * {@see UniversalSelector}s or {@see TypeSelector}s. - * - * If no such selector can be produced, returns `null`. - */ - public static function unifyUniversalAndElement(SimpleSelector $selector1, SimpleSelector $selector2): ?SimpleSelector - { - $name1 = null; - if ($selector1 instanceof UniversalSelector) { - $namespace1 = $selector1->getNamespace(); - } elseif ($selector1 instanceof TypeSelector) { - $namespace1 = $selector1->getName()->getNamespace(); - $name1 = $selector1->getName()->getName(); - } else { - throw new \InvalidArgumentException('selector1 must be a UniversalSelector or a TypeSelector'); - } - - $name2 = null; - if ($selector2 instanceof UniversalSelector) { - $namespace2 = $selector2->getNamespace(); - } elseif ($selector2 instanceof TypeSelector) { - $namespace2 = $selector2->getName()->getNamespace(); - $name2 = $selector2->getName()->getName(); - } else { - throw new \InvalidArgumentException('selector2 must be a UniversalSelector or a TypeSelector'); - } - - if ($namespace1 === $namespace2 || $namespace2 === '*') { - $namespace = $namespace1; - } elseif ($namespace1 === '*') { - $namespace = $namespace2; - } else { - return null; - } - - if ($name1 === $name2 || $name2 === null) { - $name = $name1; - } elseif ($name1 === null) { - $name = $name2; - } else { - return null; - } - - if ($name === null) { - return new UniversalSelector($namespace); - } - - return new TypeSelector(new QualifiedName($name, $namespace)); - } - - /** - * Expands "parenthesized selectors" in $complexes. - * - * That is, if we have `.A .B {@extend .C}` and `.D .C {...}`, this - * conceptually expands into `.D .C, .D (.A .B)`, and this function translates - * `.D (.A .B)` into `.D .A .B, .A .D .B`. For thoroughness, `.A.D .B` would - * also be required, but including merged selectors results in exponential - * output for very little gain. - * - * The selector `.D (.A .B)` is represented as the list `[.D, .A .B]`. - * - * If $forceLineBreak is `true`, this will mark all returned complex selectors - * as having line breaks. - * - * @param list $complexes - * - * @return list - */ - public static function weave(array $complexes, bool $forceLineBreak = false): array - { - if (\count($complexes) === 1) { - $complex = $complexes[0]; - - if (!$forceLineBreak || $complex->getLineBreak()) { - return $complexes; - } - - return [ - new ComplexSelector($complex->getLeadingCombinators(), $complex->getComponents(), true), - ]; - } - - $prefixes = [$complexes[0]]; - - foreach (array_slice($complexes, 1) as $complex) { - $target = ListUtil::last($complex->getComponents()); - - if (\count($complex->getComponents()) === 1) { - foreach ($prefixes as $i => $prefix) { - $prefixes[$i] = $prefix->concatenate($complex, $forceLineBreak); - } - - continue; - } - - $newPrefixes = []; - - foreach ($prefixes as $prefix) { - foreach (self::weaveParents($prefix, $complex) ?? [] as $parentPrefix) { - $newPrefixes[] = $parentPrefix->withAdditionalComponent($target, $forceLineBreak); - } - } - - $prefixes = $newPrefixes; - } - - return $prefixes; - } - - /** - * Interweaves $prefix's components with $base's components _other than - * the last_. - * - * Returns all possible orderings of the selectors in the inputs (including - * using unification) that maintain the relative ordering of the input. For - * example, given `.foo .bar` and `.baz .bang div`, this would return `.foo - * .bar .baz .bang div`, `.foo .bar.baz .bang div`, `.foo .baz .bar .bang div`, - * `.foo .baz .bar.bang div`, `.foo .baz .bang .bar div`, and so on until `.baz - * .bang .foo .bar div`. - * - * Semantically, for selectors `P` and `C`, this returns all selectors `PC_i` - * such that the union over all `i` of elements matched by `PC_i` is identical - * to the intersection of all elements matched by `C` and all descendants of - * elements matched by `P`. Some `PC_i` are elided to reduce the size of the - * output. - * - * Returns `null` if this intersection is empty. - * - * @param ComplexSelector $prefix - * @param ComplexSelector $base - * - * @return list|null - */ - private static function weaveParents(ComplexSelector $prefix, ComplexSelector $base): ?array - { - $leadingCombinators = self::mergeLeadingCombinators($prefix->getLeadingCombinators(), $base->getLeadingCombinators()); - if ($leadingCombinators === null) { - return null; - } - - // Make queues of _only_ the parent selectors. The prefix only contains - // parents, but the complex selector has a target that we don't want to weave - // in. - $queue1 = $prefix->getComponents(); - $queue2 = ListUtil::exceptLast($base->getComponents()); - - $finalCombinators = self::mergeTrailingCombinators($queue1, $queue2); - if ($finalCombinators === null) { - return null; - } - - // Make sure all selectors that are required to be at the root are unified - // with one another. - $rootish1 = self::firstIfRootish($queue1); - $rootish2 = self::firstIfRootish($queue2); - - if ($rootish1 !== null && $rootish2 !== null) { - $rootish = self::unifyCompound($rootish1->getSelector()->getComponents(), $rootish2->getSelector()->getComponents()); - - if ($rootish === null) { - return null; - } - - array_unshift($queue1, new ComplexSelectorComponent($rootish, $rootish1->getCombinators())); - array_unshift($queue2, new ComplexSelectorComponent($rootish, $rootish2->getCombinators())); - } elseif ($rootish1 !== null || $rootish2 !== null) { - // If there's only one rootish selector, it should only appear in the first - // position of the resulting selector. We can ensure that happens by adding - // it to the beginning of _both_ queues. - $rootish = $rootish1 ?? $rootish2; - array_unshift($queue1, $rootish); - array_unshift($queue2, $rootish); - } - - $groups1 = self::groupSelectors($queue1); - $groups2 = self::groupSelectors($queue2); - - /** @phpstan-var list> $lcs */ - $lcs = ListUtil::longestCommonSubsequence($groups2, $groups1, function ($group1, $group2) { - if (EquatableUtil::listEquals($group1, $group2)) { - return $group1; - } - - if (self::complexIsParentSuperselector($group1, $group2)) { - return $group2; - } - - if (self::complexIsParentSuperselector($group2, $group1)) { - return $group1; - } - - if (!self::mustUnify($group1, $group2)) { - return null; - } - - $unified = self::unifyComplex([new ComplexSelector([], $group1), new ComplexSelector([], $group2)]); - - if ($unified === null) { - return null; - } - if (\count($unified) > 1) { - return null; - } - - return $unified[0]->getComponents(); - }); - - $choices = []; - - foreach ($lcs as $group) { - $newChoice = []; - /** @var list>> $chunks */ - $chunks = self::chunks($groups1, $groups2, function ($sequence) use ($group) { - return self::complexIsParentSuperselector($sequence[0], $group); - }); - foreach ($chunks as $chunk) { - $flattened = []; - foreach ($chunk as $chunkGroup) { - $flattened = array_merge($flattened, $chunkGroup); - } - $newChoice[] = $flattened; - } - - /** @var list> $groups1 */ - /** @var list> $groups2 */ - $choices[] = $newChoice; - $choices[] = [$group]; - array_shift($groups1); - array_shift($groups2); - } - - $newChoice = []; - /** @var list>> $chunks */ - $chunks = self::chunks($groups1, $groups2, function ($sequence) { - return count($sequence) === 0; - }); - foreach ($chunks as $chunk) { - $flattened = []; - foreach ($chunk as $chunkGroup) { - $flattened = array_merge($flattened, $chunkGroup); - } - $newChoice[] = $flattened; - } - - $choices[] = $newChoice; - - foreach ($finalCombinators as $finalCombinator) { - $choices[] = $finalCombinator; - } - - $choices = array_filter($choices, function ($choice) { - return $choice !== []; - }); - - $paths = self::paths($choices); - - return array_map(function (array $path) use ($leadingCombinators, $prefix, $base) { - $result = []; - - foreach ($path as $group) { - $result = array_merge($result, $group); - } - - return new ComplexSelector($leadingCombinators, $result, $prefix->getLineBreak() || $base->getLineBreak()); - }, $paths); - } - - /** - * If the first element of $queue has a `:root` selector, removes and returns - * that element. - * - * @param list $queue - * - * @return ComplexSelectorComponent|null - */ - private static function firstIfRootish(array &$queue): ?ComplexSelectorComponent - { - if (empty($queue)) { - return null; - } - - $first = $queue[0]; - - foreach ($first->getSelector()->getComponents() as $simple) { - if ($simple instanceof PseudoSelector && $simple->isClass() && \in_array($simple->getNormalizedName(), self::ROOTISH_PSEUDO_CLASSES, true)) { - array_shift($queue); - - return $first; - } - } - - return null; - } - - /** - * Returns a leading combinator list that's compatible with both $combinators1 - * and $combinators2. - * - * Returns `null` if the combinator lists can't be unified. - * - * @param list|null $combinators1 - * @param list|null $combinators2 - * - * @return list|null - * - * @phpstan-param list $combinators1 - * @phpstan-param list $combinators2 - * - * @phpstan-return list|null - */ - private static function mergeLeadingCombinators(?array $combinators1, ?array $combinators2): ?array - { - if ($combinators1 === null) { - return null; - } - - if ($combinators2 === null) { - return null; - } - - if (\count($combinators1) > 1) { - return null; - } - - if (\count($combinators2) > 1) { - return null; - } - - if (\count($combinators1) === 0) { - return $combinators2; - } - - if (\count($combinators2) === 0) { - return $combinators1; - } - - return $combinators1 === $combinators2 ? $combinators1 : null; - } - - /** - * Extracts trailing {@see ComplexSelectorComponent}s with trailing combinators from - * $components1 and $components2 and merges them together into a single list. - * - * Each element in the returned list is a set of choices for a particular - * position in a complex selector. Each choice is the contents of a complex - * selector, which is to say a list of complex selector components. The union - * of each path through these choices will match the full set of necessary - * elements. - * - * If there are no combinators to be merged, returns an empty list. If the - * sequences can't be merged, returns `null`. - * - * @param list $components1 - * @param list $components2 - * @param list>> $result - * - * @return list>>|null - */ - private static function mergeTrailingCombinators(array &$components1, array &$components2, array $result = []): ?array - { - $combinators1 = \count($components1) === 0 ? [] : ListUtil::last($components1)->getCombinators(); - $combinators2 = \count($components2) === 0 ? [] : ListUtil::last($components2)->getCombinators(); - - if (\count($combinators1) === 0 && \count($combinators2) === 0) { - return $result; - } - - if (count($combinators1) > 1 || count($combinators2) > 1) { - return null; - } - - // This code looks complicated, but it's actually just a bunch of special - // cases for interactions between different combinators. - $combinator1 = $combinators1[0] ?? null; - $combinator2 = $combinators2[0] ?? null; - - if ($combinator1 !== null && $combinator2 !== null) { - $component1 = array_pop($components1); - assert($component1 instanceof ComplexSelectorComponent); - $component2 = array_pop($components2); - assert($component2 instanceof ComplexSelectorComponent); - - if ($combinator1 === Combinator::FOLLOWING_SIBLING && $combinator2 === Combinator::FOLLOWING_SIBLING) { - if ($component1->getSelector()->isSuperselector($component2->getSelector())) { - array_unshift($result, [[$component2]]); - } elseif ($component2->getSelector()->isSuperselector($component1->getSelector())) { - array_unshift($result, [[$component1]]); - } else { - $choices = [ - [$component1, $component2], - [$component2, $component1], - ]; - - $unified = self::unifyCompound($component1->getSelector()->getComponents(), $component2->getSelector()->getComponents()); - - if ($unified !== null) { - $choices[] = [new ComplexSelectorComponent($unified, [Combinator::FOLLOWING_SIBLING])]; - } - - array_unshift($result, $choices); - } - } elseif (($combinator1 === Combinator::FOLLOWING_SIBLING && $combinator2 === Combinator::NEXT_SIBLING) || ($combinator1 === Combinator::NEXT_SIBLING && $combinator2 === Combinator::FOLLOWING_SIBLING)) { - $followingSiblingComponent = $combinator1 === Combinator::FOLLOWING_SIBLING ? $component1 : $component2; - $nextSiblingComponent = $combinator1 === Combinator::FOLLOWING_SIBLING ? $component2 : $component1; - - if ($followingSiblingComponent->getSelector()->isSuperselector($nextSiblingComponent->getSelector())) { - array_unshift($result, [[$nextSiblingComponent]]); - } else { - $unified = self::unifyCompound($component1->getSelector()->getComponents(), $component2->getSelector()->getComponents()); - - $choices = [ - [$followingSiblingComponent, $nextSiblingComponent], - ]; - - if ($unified !== null) { - $choices[] = [new ComplexSelectorComponent($unified, [Combinator::NEXT_SIBLING])]; - } - - array_unshift($result, $choices); - } - } elseif ($combinator1 === Combinator::CHILD && ($combinator2 === Combinator::NEXT_SIBLING || $combinator2 === Combinator::FOLLOWING_SIBLING)) { - array_unshift($result, [[$component2]]); - $components1[] = $component1; - } elseif ($combinator2 === Combinator::CHILD && ($combinator1 === Combinator::NEXT_SIBLING || $combinator1 === Combinator::FOLLOWING_SIBLING)) { - array_unshift($result, [[$component1]]); - $components2[] = $component2; - } elseif ($combinator1 === $combinator2) { - $unified = self::unifyCompound($component1->getSelector()->getComponents(), $component2->getSelector()->getComponents()); - - if ($unified === null) { - return null; - } - - array_unshift($result, [[new ComplexSelectorComponent($unified, [$combinator1])]]); - } else { - return null; - } - - return self::mergeTrailingCombinators($components1, $components2, $result); - } - - if ($combinator1 !== null) { - $component1 = array_pop($components1); - \assert($component1 instanceof ComplexSelectorComponent); - - if ($combinator1 === Combinator::CHILD && \count($components2) > 0 && ListUtil::last($components2)->getSelector()->isSuperselector($component1->getSelector())) { - array_pop($components2); - } - - array_unshift($result, [[$component1]]); - - return self::mergeTrailingCombinators($components1, $components2, $result); - } - - $component2 = array_pop($components2); - \assert($component2 instanceof ComplexSelectorComponent); - assert($combinator2 !== null); - - if ($combinator2 === Combinator::CHILD && \count($components1) > 0 && ListUtil::last($components1)->getSelector()->isSuperselector($component2->getSelector())) { - array_pop($components1); - } - - array_unshift($result, [[$component2]]); - - return self::mergeTrailingCombinators($components2, $components1, $result); - } - - /** - * Returns whether $complex1 and $complex2 need to be unified to produce a - * valid combined selector. - * - * This is necessary when both selectors contain the same unique simple - * selector, such as an ID. - * - * @param list $complex1 - * @param list $complex2 - * - * @return bool - */ - private static function mustUnify(array $complex1, array $complex2): bool - { - $uniqueSelectors = []; - foreach ($complex1 as $component) { - foreach ($component->getSelector()->getComponents() as $simple) { - if (self::isUnique($simple)) { - $uniqueSelectors[] = $simple; - } - } - } - - if (\count($uniqueSelectors) === 0) { - return false; - } - - foreach ($complex2 as $component) { - foreach ($component->getSelector()->getComponents() as $simple) { - if (self::isUnique($simple) && EquatableUtil::listContains($uniqueSelectors, $simple)) { - return true; - } - } - } - - return false; - } - - /** - * Returns whether a {@see CompoundSelector} may contain only one simple selector of - * the same type as $simple. - */ - private static function isUnique(SimpleSelector $simple): bool - { - return $simple instanceof IDSelector || ($simple instanceof PseudoSelector && $simple->isElement()); - } - - /** - * Returns all orderings of initial subsequences of $queue1 and $queue2. - * - * The $done callback is used to determine the extent of the initial - * subsequences. It's called with each queue until it returns `true`. - * - * This destructively removes the initial subsequences of $queue1 and - * $queue2. - * - * For example, given `(A B C | D E)` and `(1 2 | 3 4 5)` (with `|` denoting - * the boundary of the initial subsequence), this would return `[(A B C 1 2), - * (1 2 A B C)]`. The queues would then contain `(D E)` and `(3 4 5)`. - * - * @template T - * - * @param list $queue1 - * @param list $queue2 - * @param callable(list): bool $done - * - * @return list> - */ - private static function chunks(array &$queue1, array &$queue2, callable $done): array - { - $chunk1 = []; - while (!$done($queue1)) { - $element = array_shift($queue1); - if ($element === null) { - throw new \LogicException('Cannot remove an element from an empty queue'); - } - - $chunk1[] = $element; - } - - $chunk2 = []; - while (!$done($queue2)) { - $element = array_shift($queue2); - if ($element === null) { - throw new \LogicException('Cannot remove an element from an empty queue'); - } - - $chunk2[] = $element; - } - - if (empty($chunk1) && empty($chunk2)) { - return []; - } - - if (empty($chunk1)) { - return [$chunk2]; - } - - if (empty($chunk2)) { - return [$chunk1]; - } - - return [ - array_merge($chunk1, $chunk2), - array_merge($chunk2, $chunk1), - ]; - } - - /** - * Returns a list of all possible paths through the given lists. - * - * For example, given `[[1, 2], [3, 4], [5]]`, this returns: - * - * ``` - * [[1, 3, 5], - * [2, 3, 5], - * [1, 4, 5], - * [2, 4, 5]] - * ``` - * - * @template T - * - * @param array> $choices - * - * @return list> - */ - public static function paths(array $choices): array - { - return array_reduce($choices, function (array $paths, array $choice) { - $newPaths = []; - - foreach ($choice as $option) { - foreach ($paths as $path) { - $path[] = $option; - $newPaths[] = $path; - } - } - - return $newPaths; - }, [[]]); - } - - /** - * Returns $complex, grouped into the longest possible sub-lists such that - * {@see ComplexSelectorComponent}s without combinators only appear at the end of - * sub-lists. - * - * For example, `(A B > C D + E ~ G)` is grouped into - * `[(A) (B > C) (D + E ~ G)]`. - * - * @param iterable $complex - * - * @return list> - */ - private static function groupSelectors(iterable $complex): array - { - $groups = []; - $group = []; - - foreach ($complex as $component) { - $group[] = $component; - - if (\count($component->getCombinators()) === 0) { - $groups[] = $group; - $group = []; - } - } - - if ($group !== []) { - $groups[] = $group; - } - - return $groups; - } - - /** - * Returns whether $list1 is a superselector of $list2. - * - * That is, whether $list1 matches every element that $list2 matches, as well - * as possibly additional elements. - * - * @param list $list1 - * @param list $list2 - * - * @return bool - */ - public static function listIsSuperselector(array $list1, array $list2): bool - { - foreach ($list2 as $complex1) { - foreach ($list1 as $complex2) { - if ($complex2->isSuperselector($complex1)) { - continue 2; - } - } - - return false; - } - - return true; - } - - /** - * Like {@see complexIsSuperselector}, but compares $complex1 and $complex2 as - * though they shared an implicit base {@see SimpleSelector}. - * - * For example, `B` is not normally a superselector of `B A`, since it doesn't - * match elements that match `A`. However, it *is* a parent superselector, - * since `B X` is a superselector of `B A X`. - * - * @param list $complex1 - * @param list $complex2 - * - * @return bool - */ - private static function complexIsParentSuperselector(array $complex1, array $complex2): bool - { - if (\count($complex1) > \count($complex2)) { - return false; - } - - $base = new ComplexSelectorComponent(new CompoundSelector([new PlaceholderSelector('')]), []); - $complex1[] = $base; - $complex2[] = $base; - - return self::complexIsSuperselector($complex1, $complex2); - } - - /** - * Returns whether $complex1 is a superselector of $complex2. - * - * That is, whether $complex1 matches every element that $complex2 matches, as well - * as possibly additional elements. - * - * @param list $complex1 - * @param list $complex2 - * - * @return bool - */ - public static function complexIsSuperselector(array $complex1, array $complex2): bool - { - // Selectors with trailing operators are neither superselectors nor - // subselectors. - if (\count(ListUtil::last($complex1)->getCombinators()) !== 0) { - return false; - } - if (\count(ListUtil::last($complex2)->getCombinators()) !== 0) { - return false; - } - - $i1 = 0; - $i2 = 0; - - while (true) { - $remaining1 = \count($complex1) - $i1; - $remaining2 = \count($complex2) - $i2; - - if ($remaining1 === 0 || $remaining2 === 0) { - return false; - } - - // More complex selectors are never superselectors of less complex ones. - if ($remaining1 > $remaining2) { - return false; - } - - $component1 = $complex1[$i1]; - if (\count($component1->getCombinators()) > 1) { - return false; - } - if ($remaining1 === 1) { - $parents = array_slice($complex2, $i2, -1); - foreach ($parents as $parent) { - if (\count($parent->getCombinators()) > 1) { - return false; - } - } - - return self::compoundIsSuperselector($component1->getSelector(), ListUtil::last($complex2)->getSelector(), $parents); - } - - // Find the first index $endOfSubselector in $complex2 such that - // `complex2.sublist(i2, endOfSubselector + 1)` is a subselector of - // `$component1->getSelector()`. - $endOfSubselector = $i2; - $parents = null; - while (true) { - $component2 = $complex2[$endOfSubselector]; - if (\count($component2->getCombinators()) > 1) { - return false; - } - if (self::compoundIsSuperselector($component1->getSelector(), $component2->getSelector(), $parents)) { - break; - } - - $endOfSubselector++; - - if ($endOfSubselector === \count($complex2) - 1) { - // Stop before the superselector would encompass all of $complex2 - // because we know $complex1 has more than one element, and consuming - // all of $complex2 wouldn't leave anything for the rest of $complex1 - // to match. - return false; - } - - $parents[] = $component2; - } - - $component2 = $complex2[$endOfSubselector]; - $combinator1 = $component1->getCombinators()[0] ?? null; - $combinator2 = $component2->getCombinators()[0] ?? null; - - if (!self::isSupercombinator($combinator1, $combinator2)) { - return false; - } - - $i1++; - $i2 = $endOfSubselector + 1; - - if (\count($complex1) - $i1 === 1) { - if ($combinator1 === Combinator::FOLLOWING_SIBLING) { - // The selector `.foo ~ .bar` is only a superselector of selectors that - // *exclusively* contain subcombinators of `~`. - for ($index = $i2; $index < \count($complex2) - 1; $index++) { - $component = $complex2[$index]; - - if (!self::isSupercombinator($combinator1, $component->getCombinators()[0] ?? null)) { - return false; - } - } - } elseif ($combinator1 !== null) { - // `.foo > .bar` and `.foo + bar` aren't superselectors of any selectors - // with more than one combinator. - if (\count($complex2) - $i2 > 1) { - return false; - } - } - } - } - } - - /** - * Returns whether $combinator1 is a supercombinator of $combinator2. - * - * That is, whether `X $combinator1 Y` is a superselector of `X $combinator2 Y`. - * - * @phpstan-param Combinator::*|null $combinator1 - * @phpstan-param Combinator::*|null $combinator2 - */ - private static function isSupercombinator(?string $combinator1, ?string $combinator2): bool - { - return $combinator1 === $combinator2 || ($combinator1 === null && $combinator2 === Combinator::CHILD) || ($combinator1 === Combinator::FOLLOWING_SIBLING && $combinator2 === Combinator::NEXT_SIBLING); - } - - /** - * Returns whether $compound1 is a superselector of $compound2. - * - * That is, whether $compound1 matches every element that $compound2 matches, as well - * as possibly additional elements. - * - * If $parents is passed, it represents the parents of $compound2. This is - * relevant for pseudo selectors with selector arguments, where we may need to - * know if the parent selectors in the selector argument match $parents. - * - * @param CompoundSelector $compound1 - * @param CompoundSelector $compound2 - * @param list|null $parents - * - * @return bool - */ - public static function compoundIsSuperselector(CompoundSelector $compound1, CompoundSelector $compound2, ?array $parents = null): bool - { - // Pseudo elements effectively change the target of a compound selector rather - // than narrowing the set of elements to which it applies like other - // selectors. As such, if either selector has a pseudo element, they both must - // have the _same_ pseudo element. - // - // In addition, order matters when pseudo-elements are involved. The selectors - // before them must - $tuple1 = self::findPseudoElementIndexed($compound1); - $tuple2 = self::findPseudoElementIndexed($compound2); - - if ($tuple1 !== null && $tuple2 !== null) { - return $tuple1[0]->isSuperselector($tuple2[0]) && - self::compoundComponentsIsSuperselector( - array_slice($compound1->getComponents(), 0, $tuple1[1]), - array_slice($compound2->getComponents(), 0, $tuple2[1]), - $parents - ) && - self::compoundComponentsIsSuperselector( - array_slice($compound1->getComponents(), $tuple1[1] + 1), - array_slice($compound2->getComponents(), $tuple2[1] + 1), - $parents - ); - } elseif ($tuple1 !== null || $tuple2 !== null) { - return false; - } - - // Every selector in `$compound1->getComponents()` must have a matching selector in - // `$compound2->getComponents()`. - foreach ($compound1->getComponents() as $simple1) { - if ($simple1 instanceof PseudoSelector && $simple1->getSelector() !== null) { - if (!self::selectorPseudoIsSuperselector($simple1, $compound2, $parents)) { - return false; - } - } else { - foreach ($compound2->getComponents() as $simple2) { - if ($simple1->isSuperselector($simple2)) { - continue 2; - } - } - - return false; - } - } - - return true; - } - - /** - * If $compound contains a pseudo-element, returns it and its index in - * `$compound->getComponents()`. - * - * @return array{PseudoSelector, int}|null - */ - private static function findPseudoElementIndexed(CompoundSelector $compound): ?array - { - foreach ($compound->getComponents() as $i => $simple) { - if ($simple instanceof PseudoSelector && $simple->isElement()) { - return [$simple, $i]; - } - } - - return null; - } - - /** - * Like {@see compoundIsSuperselector} but operates on the underlying lists of - * simple selectors. - * - * @param list $compound1 - * @param list $compound2 - * @param list|null $parents - * - * @return bool - */ - private static function compoundComponentsIsSuperselector(array $compound1, array $compound2, ?array $parents = null): bool - { - if (\count($compound1) === 0) { - return true; - } - - if (\count($compound2) === 0) { - $compound2 = [new UniversalSelector('*')]; - } - - return self::compoundIsSuperselector(new CompoundSelector($compound1), new CompoundSelector($compound2), $parents); - } - - /** - * Returns whether $pseudo1 is a superselector of $compound2. - * - * That is, whether $pseudo1 matches every element that $compound2 matches, as well - * as possibly additional elements. - * - * This assumes that $pseudo1's `selector` argument is not `null`. - * - * If $parents is passed, it represents the parents of $compound2. This is - * relevant for pseudo selectors with selector arguments, where we may need to - * know if the parent selectors in the selector argument match $parents. - * - * @param list|null $parents - */ - private static function selectorPseudoIsSuperselector(PseudoSelector $pseudo1, CompoundSelector $compound2, ?array $parents): bool - { - $selector1 = $pseudo1->getSelector(); - - if ($selector1 === null) { - throw new \InvalidArgumentException("Selector $pseudo1 must have a selector argument."); - } - - switch ($pseudo1->getNormalizedName()) { - case 'is': - case 'matches': - case 'any': - case 'where': - $selectors = self::selectorPseudoArgs($compound2, $pseudo1->getName()); - - foreach ($selectors as $selector2) { - if ($selector1->isSuperselector($selector2)) { - return true; - } - } - - $componentWithParents = $parents; - $componentWithParents[] = new ComplexSelectorComponent($compound2, []); - - foreach ($selector1->getComponents() as $complex1) { - if (\count($complex1->getLeadingCombinators()) === 0 && self::complexIsSuperselector($complex1->getComponents(), $componentWithParents)) { - return true; - } - } - - return false; - - case 'has': - case 'host': - case 'host-context': - $selectors = self::selectorPseudoArgs($compound2, $pseudo1->getName()); - - foreach ($selectors as $selector2) { - if ($selector1->isSuperselector($selector2)) { - return true; - } - } - - return false; - - case 'slotted': - $selectors = self::selectorPseudoArgs($compound2, $pseudo1->getName(), false); - - foreach ($selectors as $selector2) { - if ($selector1->isSuperselector($selector2)) { - return true; - } - } - - return false; - - case 'not': - foreach ($selector1->getComponents() as $complex) { - if ($complex->isBogus()) { - return false; - } - - foreach ($compound2->getComponents() as $simple2) { - if ($simple2 instanceof TypeSelector) { - foreach ($complex->getLastComponent()->getSelector()->getComponents() as $simple1) { - if ($simple1 instanceof TypeSelector && !$simple1->equals($simple2)) { - continue 3; - } - } - } elseif ($simple2 instanceof IDSelector) { - foreach ($complex->getLastComponent()->getSelector()->getComponents() as $simple1) { - if ($simple1 instanceof IDSelector && !$simple1->equals($simple2)) { - continue 3; - } - } - } elseif ($simple2 instanceof PseudoSelector && $simple2->getName() === $pseudo1->getName()) { - $selector2 = $simple2->getSelector(); - if ($selector2 === null) { - continue; - } - - if (self::listIsSuperselector($selector2->getComponents(), [$complex])) { - continue 2; - } - } - } - - return false; - } - - return true; - - case 'current': - $selectors = self::selectorPseudoArgs($compound2, $pseudo1->getName()); - - foreach ($selectors as $selector2) { - if ($selector1->equals($selector2)) { - return true; - } - } - - return false; - - case 'nth-child': - case 'nth-last-child': - foreach ($compound2->getComponents() as $pseudo2) { - if (!$pseudo2 instanceof PseudoSelector) { - continue; - } - - if ($pseudo2->getName() !== $pseudo1->getName()) { - continue; - } - - if ($pseudo2->getArgument() !== $pseudo1->getArgument()) { - continue; - } - - $selector2 = $pseudo2->getSelector(); - - if ($selector2 === null) { - continue; - } - - if ($selector1->isSuperselector($selector2)) { - return true; - } - } - - return false; - - default: - throw new \LogicException('unreachache'); - } - } - - /** - * Returns all the selector arguments of pseudo selectors in $compound with - * the given $name. - * - * @return SelectorList[] - */ - private static function selectorPseudoArgs(CompoundSelector $compound, string $name, bool $isClass = true): array - { - $selectors = []; - - foreach ($compound->getComponents() as $simple) { - if (!$simple instanceof PseudoSelector) { - continue; - } - - if ($simple->isClass() !== $isClass || $simple->getName() !== $name) { - continue; - } - - if ($simple->getSelector() === null) { - continue; - } - - $selectors[] = $simple->getSelector(); - } - - return $selectors; - } -} diff --git a/scssphp/src/Logger/AdaptingLogger.php b/scssphp/src/Logger/AdaptingLogger.php deleted file mode 100644 index df19219..0000000 --- a/scssphp/src/Logger/AdaptingLogger.php +++ /dev/null @@ -1,76 +0,0 @@ -logger = $logger; - } - - public static function adaptLogger(LoggerInterface $logger): LocationAwareLoggerInterface - { - if ($logger instanceof LocationAwareLoggerInterface) { - return $logger; - } - - return new self($logger); - } - - public function warn(string $message, bool $deprecation = false, ?FileSpan $span = null, ?Trace $trace = null) - { - if ($span === null) { - $formattedMessage = $message; - } elseif ($trace !== null) { - // If there's a span and a trace, the span's location information is - // probably duplicated in the trace, so we just use it for highlighting. - $formattedMessage = $message; - // TODO implement the highlight of a span - } else { - $formattedMessage = ' on ' . $span->message("\n" . $message); - } - - if ($trace !== null) { - $formattedMessage .= "\n" . Util::indent(rtrim($trace->getFormattedTrace()), 4); - } - - $this->logger->warn($formattedMessage, $deprecation); - } - - public function debug(string $message, ?FileSpan $span = null) - { - $location = ''; - if ($span !== null) { - $url = $span->getStart()->getSourceUrl() === null ? '-' : Path::prettyUri($span->getStart()->getSourceUrl()); - $line = $span->getStart()->getLine() + 1; - $location = "$url:$line "; - } - - $this->logger->debug(sprintf("%sDEBUG: %s", $location, $message)); - } -} diff --git a/scssphp/src/Logger/LocationAwareLoggerInterface.php b/scssphp/src/Logger/LocationAwareLoggerInterface.php deleted file mode 100644 index 78fb841..0000000 --- a/scssphp/src/Logger/LocationAwareLoggerInterface.php +++ /dev/null @@ -1,49 +0,0 @@ -scanner->expectChar('('); - $this->whitespace(); - $include = $this->scanIdentifier('with'); - if (!$include) { - $this->expectIdentifier('without', '"with" or "without"'); - } - $this->whitespace(); - $this->scanner->expectChar(':'); - $this->whitespace(); - - $atRules = []; - - do { - $atRules[] = strtolower($this->identifier()); - $this->whitespace(); - } while ($this->lookingAtIdentifier()); - - $this->scanner->expectChar(')'); - $this->scanner->expectDone(); - - return AtRootQuery::create($atRules, $include); - } catch (FormatException $e) { - throw $this->wrapException($e); - } - } -} diff --git a/scssphp/src/Parser/CssParser.php b/scssphp/src/Parser/CssParser.php deleted file mode 100644 index af2d1df..0000000 --- a/scssphp/src/Parser/CssParser.php +++ /dev/null @@ -1,173 +0,0 @@ - true, 'rgba' => true, 'hsl' => true, 'hsla' => true, 'grayscale' => true, - 'invert' => true, 'alpha' => true, 'opacity' => true, 'saturate' => true, - ]; - - protected function isPlainCss(): bool - { - return true; - } - - protected function silentComment(): void - { - $start = $this->scanner->getPosition(); - parent::silentComment(); - $this->error("Silent comments aren't allowed in plain CSS.", $this->scanner->spanFrom($start)); - } - - protected function atRule(callable $child, bool $root = false): Statement - { - $start = $this->scanner->getPosition(); - - $this->scanner->expectChar('@'); - $name = $this->interpolatedIdentifier(); - $this->whitespace(); - - switch ($name->getAsPlain()) { - case 'at-root': - case 'content': - case 'debug': - case 'each': - case 'error': - case 'extend': - case 'for': - case 'function': - case 'if': - case 'include': - case 'mixin': - case 'return': - case 'warn': - case 'while': - $this->almostAnyValue(); - $this->error("This at-rule isn't allowed in plain CSS.", $this->scanner->spanFrom($start)); - - case 'import': - return $this->cssImportRule($start); - - case 'media': - return $this->mediaRule($start); - - case '-moz-document': - return $this->mozDocumentRule($start, $name); - - case 'supports': - return $this->supportsRule($start); - - default: - return $this->unknownAtRule($start, $name); - - } - } - - private function cssImportRule(int $start): ImportRule - { - $urlStart = $this->scanner->getPosition(); - $next = $this->scanner->peekChar(); - - if ($next === 'u' || $next === 'U') { - $url = $this->dynamicUrl(); - } else { - $url = new StringExpression($this->interpolatedString()->asInterpolation(true)); - } - $urlSpan = $this->scanner->spanFrom($urlStart); - - $this->whitespace(); - $modifiers = $this->tryImportModifiers(); - $this->expectStatementSeparator('@import rule'); - - return new ImportRule([ - new StaticImport(new Interpolation([$url], $urlSpan), $this->scanner->spanFrom($start), $modifiers) - ], $this->scanner->spanFrom($start)); - } - - protected function identifierLike(): Expression - { - $start = $this->scanner->getPosition(); - $identifier = $this->interpolatedIdentifier(); - $plain = $identifier->getAsPlain(); - assert($plain !== null); // CSS doesn't allow non-plain identifiers - - $lower = strtolower($plain); - $specialFunction = $this->trySpecialFunction($lower, $start); - - if ($specialFunction !== null) { - return $specialFunction; - } - - $beforeArguments = $this->scanner->getPosition(); - if (!$this->scanner->scanChar('(')) { - return new StringExpression($identifier); - } - - $allowEmptySecondArg = $lower === 'var'; - $arguments = []; - - if (!$this->scanner->scanChar(')')) { - do { - $this->whitespace(); - - if ($allowEmptySecondArg && \count($arguments) === 1 && $this->scanner->peekChar() === ')') { - $arguments[] = StringExpression::plain('', $this->scanner->getEmptySpan()); - break; - } - - $arguments[] = $this->expressionUntilComma(true); - $this->whitespace(); - } while ($this->scanner->scanChar(',')); - $this->scanner->expectChar(')'); - } - - if ($plain === 'if' || (!isset(self::CSS_ALLOWED_FUNCTIONS[$plain]) && Compiler::isNativeFunction($plain))) { - $this->error("This function isn't allowed in plain CSS.", $this->scanner->spanFrom($start)); - } - - return new InterpolatedFunctionExpression( - // Create a fake interpolation to force the function to be interpreted - // as plain CSS, rather than calling a user-defined function. - new Interpolation([new StringExpression($identifier)], $identifier->getSpan()), - new ArgumentInvocation($arguments, [], $this->scanner->spanFrom($beforeArguments)), - $this->scanner->spanFrom($start) - ); - } - - protected function namespacedExpression(string $namespace, int $start): Expression - { - $expression = parent::namespacedExpression($namespace, $start); - - $this->error("Module namespaces aren't allowed in plain CSS.", $expression->getSpan()); - } -} diff --git a/scssphp/src/Parser/FormatException.php b/scssphp/src/Parser/FormatException.php deleted file mode 100644 index 6d0b045..0000000 --- a/scssphp/src/Parser/FormatException.php +++ /dev/null @@ -1,38 +0,0 @@ -span = $span; - parent::__construct($message, 0, $previous); - } - - public function getSpan(): FileSpan - { - return $this->span; - } -} diff --git a/scssphp/src/Parser/InterpolationBuffer.php b/scssphp/src/Parser/InterpolationBuffer.php deleted file mode 100644 index fc0f378..0000000 --- a/scssphp/src/Parser/InterpolationBuffer.php +++ /dev/null @@ -1,109 +0,0 @@ - - */ - private $contents = []; - - /** - * Returns the substring of the buffer string after the last interpolation. - */ - public function getTrailingString(): string - { - return $this->text; - } - - public function isEmpty(): bool - { - return $this->text === '' && \count($this->contents) === 0; - } - - public function write(string $string): void - { - $this->text .= $string; - } - - public function add(Expression $expression): void - { - $this->flushText(); - $this->contents[] = $expression; - } - - public function addInterpolation(Interpolation $interpolation): void - { - $contents = $interpolation->getContents(); - - if (empty($contents)) { - return; - } - - if (is_string($contents[0])) { - $this->text .= $contents[0]; - - array_shift($contents); - } - - $this->flushText(); - - foreach ($contents as $content) { - $this->contents[] = $content; - } - - if (\is_string($this->contents[\count($this->contents) - 1])) { - $this->text = $this->contents[\count($this->contents) - 1]; - array_pop($this->contents); - } - } - - public function buildInterpolation(FileSpan $span): Interpolation - { - $contents = $this->contents; - - if ($this->text !== '') { - $contents[] = $this->text; - } - - return new Interpolation($contents, $span); - } - - /** - * Flushes {@see self::$text} to {@see self::$contents} if necessary. - */ - private function flushText(): void - { - if ($this->text === '') { - return; - } - - $this->contents[] = $this->text; - $this->text = ''; - } -} diff --git a/scssphp/src/Parser/KeyframeSelectorParser.php b/scssphp/src/Parser/KeyframeSelectorParser.php deleted file mode 100644 index 58055c5..0000000 --- a/scssphp/src/Parser/KeyframeSelectorParser.php +++ /dev/null @@ -1,105 +0,0 @@ - - * - * @throws SassFormatException - */ - public function parse(): array - { - try { - $selectors = []; - - do { - $this->whitespace(); - if ($this->lookingAtIdentifier()) { - if ($this->scanIdentifier('from')) { - $selectors[] = 'from'; - } else { - $this->expectIdentifier('to', '"to" or "from"'); - $selectors[] = 'to'; - } - } else { - $selectors[] = $this->percentage(); - } - $this->whitespace(); - } while ($this->scanner->scanChar(',')); - $this->scanner->expectDone(); - - return $selectors; - } catch (FormatException $e) { - throw $this->wrapException($e); - } - } - - private function percentage(): string - { - $buffer = ''; - - if ($this->scanner->scanChar('+')) { - $buffer .= '+'; - } - - $second = $this->scanner->peekChar(); - - if (!Character::isDigit($second) && $second !== '.') { - $this->scanner->error('Expected number.'); - } - - while (Character::isDigit($this->scanner->peekChar())) { - $buffer .= $this->scanner->readChar(); - } - - if ($this->scanner->peekChar() === '.') { - $buffer .= $this->scanner->readChar(); - - while (Character::isDigit($this->scanner->peekChar())) { - $buffer .= $this->scanner->readChar(); - } - } - - if ($this->scanIdentChar('e')) { - $buffer .= 'e'; - $next = $this->scanner->peekChar(); - - if ($next === '+' || $next === '-') { - $buffer .= $this->scanner->readChar(); - } - - if (!Character::isDigit($this->scanner->peekChar())) { - $this->scanner->error('Expected digit.'); - } - - while (Character::isDigit($this->scanner->peekChar())) { - $buffer .= $this->scanner->readChar(); - } - } - - $this->scanner->expectChar('%'); - $buffer .= '%'; - - return $buffer; - } -} diff --git a/scssphp/src/Parser/LineScanner.php b/scssphp/src/Parser/LineScanner.php deleted file mode 100644 index f55f195..0000000 --- a/scssphp/src/Parser/LineScanner.php +++ /dev/null @@ -1,163 +0,0 @@ -peekChar(-1) === "\r" && $this->peekChar() === "\n"; - } - - public function setPosition(int $position): void - { - $newPosition = $position; - $oldPosition = $this->getPosition(); - parent::setPosition($position); - - if ($newPosition > $oldPosition) { - $newlines = $this->newlinesIn($this->substring($oldPosition, $newPosition)); - $this->line += \count($newlines); - - if ($newlines === []) { - $this->column += $newPosition - $oldPosition; - } else { - $last = $newlines[\count($newlines) - 1]; - $end = $last[1] + \strlen($last[0]); - - $this->column = $newPosition - $end; - } - } else { - $newlines = $this->newlinesIn($this->substring($newPosition, $oldPosition)); - - if ($this->betweenCRLF()) { - array_pop($newlines); - } - $this->line -= \count($newlines); - - if ($newlines === []) { - $this->column -= $oldPosition - $newPosition; - } else { - // TODO check that - $this->column = $newPosition - strrpos($this->getString(), "\n", $newPosition) - 1; - } - } - } - - /** - * @phpstan-impure - */ - public function scanChar(string $char): bool - { - if (!parent::scanChar($char)) { - return false; - } - - $this->adjustLineAndColumn($char); - return true; - } - - /** - * @phpstan-impure - */ - public function readChar(): string - { - $character = parent::readChar(); - $this->adjustLineAndColumn($character); - - return $character; - } - - /** - * @phpstan-impure - */ - public function readUtf8Char(): string - { - $character = parent::readUtf8Char(); - $this->adjustLineAndColumn($character); - - return $character; - } - - /** - * Adjusts {@see line} and {@see column} after having consumed $character. - */ - private function adjustLineAndColumn(string $character): void - { - if ($character === "\n" || ($character === "\r" && $this->peekChar() !== "\n")) { - $this->line += 1; - $this->column = 0; - } else { - $this->column += \strlen($character); - } - } - - /** - * @phpstan-impure - */ - public function scan(string $string): bool - { - if (!parent::scan($string)) { - return false; - } - - $newlines = $this->newlinesIn($string); - $this->line += \count($newlines); - - if ($newlines === []) { - $this->column += \strlen($string); - } else { - $last = $newlines[\count($newlines) - 1]; - $end = $last[1] + \strlen($last[0]); - - $this->column = \strlen($string) - $end; - } - - return true; - } - - /** - * @phpstan-return list - */ - private function newlinesIn(string $text): array - { - preg_match_all('/\r\n?|\n/', $text, $matches, PREG_OFFSET_CAPTURE); - - $newlines = $matches[0]; - - if ($this->betweenCRLF()) { - array_pop($newlines); - } - - return $newlines; - } -} diff --git a/scssphp/src/Parser/MediaQueryParser.php b/scssphp/src/Parser/MediaQueryParser.php deleted file mode 100644 index f59ca8e..0000000 --- a/scssphp/src/Parser/MediaQueryParser.php +++ /dev/null @@ -1,154 +0,0 @@ - - * - * @throws SassFormatException when parsing fails - */ - public function parse(): array - { - try { - $queries = []; - - do { - $this->whitespace(); - $queries[] = $this->mediaQuery(); - $this->whitespace(); - } while ($this->scanner->scanChar(',')); - $this->scanner->expectDone(); - - return $queries; - } catch (FormatException $e) { - throw $this->wrapException($e); - } - } - - /** - * Consumes a single media query. - */ - private function mediaQuery(): CssMediaQuery - { - if ($this->scanner->peekChar() === '(') { - $conditions = [$this->mediaInParens()]; - $this->whitespace(); - - $conjunction = true; - - if ($this->scanIdentifier('and')) { - $this->expectWhitespace(); - $conditions = array_merge($conditions, $this->mediaLogicSequence('and')); - } elseif ($this->scanIdentifier('or')) { - $this->expectWhitespace(); - $conjunction = false; - $conditions = array_merge($conditions, $this->mediaLogicSequence('or')); - } - - return CssMediaQuery::condition($conditions, $conjunction); - } - $modifier = null; - $type = null; - - $identifier1 = $this->identifier(); - - if (strtolower($identifier1) === 'not') { - $this->expectWhitespace(); - - if (!$this->lookingAtIdentifier()) { - // For example, "@media not (...) {" - return CssMediaQuery::condition(['(not ' . $this->mediaInParens() . ')']); - } - } - - $this->whitespace(); - - if (!$this->lookingAtIdentifier()) { - // For example, "@media screen {" - return CssMediaQuery::type($identifier1); - } - - $identifier2 = $this->identifier(); - - if (strtolower($identifier2) === 'and') { - $this->expectWhitespace(); - // For example, "@media screen and ..." - $type = $identifier1; - } else { - $this->whitespace(); - $modifier = $identifier1; - $type = $identifier2; - - if ($this->scanIdentifier('and')) { - // For example, "@media only screen and ..." - $this->expectWhitespace(); - } else { - // For example, "@media only screen {" - return CssMediaQuery::type($type, $modifier); - } - } - - // We've consumed either `IDENTIFIER "and"` or - // `IDENTIFIER IDENTIFIER "and"`. - - if ($this->scanIdentifier('not')) { - // For example, "@media screen and not (...) {" - return CssMediaQuery::type($type, $modifier, ['(not ' . $this->mediaInParens() . ')']); - } - - return CssMediaQuery::type($type, $modifier, $this->mediaLogicSequence('and')); - } - - /** - * Consumes one or more `` expressions separated by - * $operator and returns them. - * - * @return list - */ - private function mediaLogicSequence(string $operator): array - { - $result = []; - while (true) { - $result[] = $this->mediaInParens(); - $this->whitespace(); - - if (!$this->scanIdentifier($operator)) { - return $result; - } - $this->expectWhitespace(); - } - } - - /** - * Consumes a `` expression and returns it, parentheses - * included. - */ - private function mediaInParens(): string - { - $this->scanner->expectChar('(', 'media condition in parentheses'); - $result = '(' . $this->declarationValue() . ')'; - $this->scanner->expectChar(')'); - - return $result; - } -} diff --git a/scssphp/src/Parser/Parser.php b/scssphp/src/Parser/Parser.php deleted file mode 100644 index 10f4217..0000000 --- a/scssphp/src/Parser/Parser.php +++ /dev/null @@ -1,958 +0,0 @@ -doParseIdentifier(); - } - - /** - * Returns whether $text is a valid CSS identifier. - */ - public static function isIdentifier(string $text, ?LoggerInterface $logger = null): bool - { - try { - self::parseIdentifier($text, $logger); - - return true; - } catch (SassFormatException $e) { - return false; - } - } - - public function __construct(string $contents, ?LoggerInterface $logger = null, ?string $sourceUrl = null) - { - $this->scanner = new StringScanner($contents); - $this->logger = AdaptingLogger::adaptLogger($logger ?? new QuietLogger()); - $this->sourceUrl = $sourceUrl; - } - - /** - * @throws SassFormatException - */ - private function doParseIdentifier(): string - { - try { - $result = $this->identifier(); - $this->scanner->expectDone(); - - return $result; - } catch (FormatException $e) { - throw $this->wrapException($e); - } - } - - /** - * Consumes whitespace, including any comments. - */ - protected function whitespace(): void - { - do { - $this->whitespaceWithoutComments(); - } while ($this->scanComment()); - } - - /** - * Consumes whitespace, but not comments. - */ - protected function whitespaceWithoutComments(): void - { - while (!$this->scanner->isDone() && Character::isWhitespace($this->scanner->peekChar())) { - $this->scanner->readChar(); - } - } - - /** - * Consumes spaces and tabs. - */ - protected function spaces(): void - { - while (!$this->scanner->isDone() && Character::isSpaceOrTab($this->scanner->peekChar())) { - $this->scanner->readChar(); - } - } - - /** - * Consumes and ignores a comment if possible. - * - * Returns whether the comment was consumed. - */ - protected function scanComment(): bool - { - if ($this->scanner->peekChar() !== '/') { - return false; - } - - $next = $this->scanner->peekChar(1); - - if ($next === '/') { - $this->silentComment(); - - return true; - } - - if ($next === '*') { - $this->loudComment(); - return true; - } - - return false; - } - - /** - * Like {@see whitespace}, but throws an error if no whitespace is consumed. - */ - protected function expectWhitespace(): void - { - if ($this->scanner->isDone() || !(Character::isWhitespace($this->scanner->peekChar()) || $this->scanComment())) { - $this->scanner->error('Expected whitespace.'); - } - - $this->whitespace(); - } - - /** - * Consumes and ignores a silent (Sass-style) comment. - */ - protected function silentComment(): void - { - $this->scanner->expect('//'); - - while (!$this->scanner->isDone() && !Character::isNewline($this->scanner->peekChar())) { - $this->scanner->readChar(); - } - } - - /** - * Consumes and ignores a loud (CSS-style) comment. - */ - protected function loudComment(): void - { - $this->scanner->expect('/*'); - - while (true) { - $next = $this->scanner->readChar(); - - if ($next !== '*') { - continue; - } - - do { - $next = $this->scanner->readChar(); - } while ($next === '*'); - - if ($next === '/') { - break; - } - } - } - - /** - * Consumes a plain CSS identifier. - * - * If $normalize is `true`, this converts underscores into hyphens. - * - * If $unit is `true`, this doesn't parse a `-` followed by a digit. This - * ensures that `1px-2px` parses as subtraction rather than the unit - * `px-2px`. - */ - protected function identifier(bool $normalize = false, bool $unit = false): string - { - $text = ''; - - if ($this->scanner->scanChar('-')) { - $text .= '-'; - - - if ($this->scanner->scanChar('-')) { - $text .= '-'; - $text .= $this->consumeIdentifierBody($normalize, $unit); - - return $text; - } - } - - $first = $this->scanner->peekChar(); - - if ($first === null) { - $this->scanner->error('Expected identifier.'); - } - - if ($normalize && $first === '_') { - $this->scanner->readChar(); - $text .= '-'; - } elseif (Character::isNameStart($first)) { - $text .= $this->scanner->readUtf8Char(); - } elseif ($first === '\\') { - $text .= $this->escape(true); - } else { - $this->scanner->error('Expected identifier.'); - } - - $text .= $this->consumeIdentifierBody($normalize, $unit); - - return $text; - } - - /** - * Consumes a chunk of a plain CSS identifier after the name start. - */ - public function identifierBody(): string - { - $text = $this->consumeIdentifierBody(); - - if ($text === '') { - $this->scanner->error('Expected identifier body.'); - } - - return $text; - } - - private function consumeIdentifierBody(bool $normalize = false, bool $unit = false): string - { - $text = ''; - - while (true) { - $next = $this->scanner->peekChar(); - - if ($next === null) { - break; - } - - if ($unit && $next === '-') { - $second = $this->scanner->peekChar(1); - - if ($second !== null && ($second === '.' || Character::isDigit($second))) { - break; - } - - $text .= $this->scanner->readChar(); - } elseif ($normalize && $next === '_') { - $this->scanner->readChar(); - $text .= '-'; - } elseif (Character::isName($next)) { - $text .= $this->scanner->readUtf8Char(); - } elseif ($next === '\\') { - $text .= $this->escape(); - } else { - break; - } - } - - return $text; - } - - /** - * Consumes a plain CSS string. - * - * This returns the parsed contents of the string—that is, it doesn't include - * quotes and its escapes are resolved. - */ - protected function string(): string - { - $quote = $this->scanner->readChar(); - - if ($quote !== '"' && $quote !== "'") { - $this->scanner->error('Expected string.'); - } - - $buffer = ''; - - while (true) { - $next = $this->scanner->peekChar(); - - if ($next === $quote) { - $this->scanner->readChar(); - break; - } - - if ($next === null || Character::isNewline($next)) { - $this->scanner->error("Expected $quote."); - } - - if ($next === '\\') { - $second = $this->scanner->peekChar(1); - - if ($second !== null && Character::isNewline($second)) { - $this->scanner->readChar(); - $this->scanner->readChar(); - } else { - $buffer .= $this->escapeCharacter(); - } - } else { - $buffer .= $this->scanner->readUtf8Char(); - } - } - - return $buffer; - } - - /** - * Consumes and returns a natural number (that is, a non-negative integer) as a double. - * - * Doesn't support scientific notation. - */ - protected function naturalNumber(): float - { - $first = $this->scanner->readChar(); - - if (!Character::isDigit($first)) { - $this->scanner->error('Expected digit.', $this->scanner->getPosition() - 1); - } - - $number = (float) intval($first); - - while (Character::isDigit($this->scanner->peekChar())) { - $number *= 10; - $number += intval($this->scanner->readChar()); - } - - return $number; - } - - /** - * Consumes tokens until it reaches a top-level `";"`, `")"`, `"]"`, - * or `"}"` and returns their contents as a string. - * - * If $allowEmpty is `false` (the default), this requires at least one token. - */ - protected function declarationValue(bool $allowEmpty = false): string - { - $buffer = ''; - $brackets = []; - $wroteNewline = false; - - while (true) { - $next = $this->scanner->peekChar(); - - if ($next === null) { - break; - } - - switch ($next) { - case '\\': - $buffer .= $this->escape(true); - $wroteNewline = false; - break; - - case '"': - case "'": - $buffer .= $this->rawText([$this, 'string']); - $wroteNewline = false; - break; - - case '/': - if ($this->scanner->peekChar(1) === '*') { - $buffer .= $this->rawText([$this, 'loudComment']); - } else { - $buffer .= $this->scanner->readChar(); - } - $wroteNewline = false; - break; - - case ' ': - case "\t": - $second = $this->scanner->peekChar(1); - if ($wroteNewline || $second === null || !Character::isWhitespace($second)) { - $buffer .= ' '; - } - $this->scanner->readChar(); - break; - - case "\n": - case "\r": - case "\f": - $prev = $this->scanner->peekChar(-1); - if ($prev === null || !Character::isNewline($prev)) { - $buffer .= "\n"; - } - $this->scanner->readChar(); - $wroteNewline = true; - break; - - case '(': - case '{': - case '[': - $buffer .= $next; - $brackets[] = Character::opposite($this->scanner->readChar()); - $wroteNewline = false; - break; - - case ')': - case '}': - case ']': - if (empty($brackets)) { - break 2; - } - - $buffer .= $next; - $this->scanner->expectChar(array_pop($brackets)); - $wroteNewline = false; - break; - - case ';': - if (empty($brackets)) { - break 2; - } - - $buffer .= $this->scanner->readChar(); - break; - - case 'u': - case 'U': - $url = $this->tryUrl(); - - if ($url !== null) { - $buffer .= $url; - } else { - $buffer .= $this->scanner->readChar(); - } - - $wroteNewline = false; - break; - - default: - if ($this->lookingAtIdentifier()) { - $buffer .= $this->identifier(); - } else { - $buffer .= $this->scanner->readUtf8Char(); - } - $wroteNewline = false; - break; - } - } - - if (!empty($brackets)) { - $this->scanner->expectChar(array_pop($brackets)); - } - - if (!$allowEmpty && $buffer === '') { - $this->scanner->error('Expected token.'); - } - - return $buffer; - } - - /** - * Consumes a `url()` token if possible, and returns `null` otherwise. - */ - protected function tryUrl(): ?string - { - $start = $this->scanner->getPosition(); - - if (!$this->scanIdentifier('url')) { - return null; - } - - if (!$this->scanner->scanChar('(')) { - $this->scanner->setPosition($start); - - return null; - } - - $this->whitespace(); - - $buffer = 'url('; - - while (true) { - $next = $this->scanner->peekChar(); - - if ($next === null) { - break; - } - - $nextCharCode = \ord($next); - - if ($next === '\\') { - $buffer .= $this->escape(); - } elseif ($next === '%' || $next === '&' || $next === '#' || ($nextCharCode >= \ord('*') && $nextCharCode <= \ord('~')) || $nextCharCode >= 0x80) { - $buffer .= $this->scanner->readUtf8Char(); - } elseif (Character::isWhitespace($next)) { - $this->whitespace(); - - if ($this->scanner->peekChar() !== ')') { - break; - } - } elseif ($next === ')') { - $buffer .= $this->scanner->readChar(); - - return $buffer; - } else { - break; - } - } - - $this->scanner->setPosition($start); - - return null; - } - - /** - * Consumes a Sass variable name, and returns its name without the dollar sign. - */ - protected function variableName(): string - { - $this->scanner->expectChar('$'); - - return $this->identifier(true); - } - - /** - * Consumes an escape sequence and returns the text that defines it. - * - * If $identifierStart is true, this normalizes the escape sequence as - * though it were at the beginning of an identifier. - */ - protected function escape(bool $identifierStart = false): string - { - $start = $this->scanner->getPosition(); - - $this->scanner->expectChar('\\'); - - $first = $this->scanner->peekChar(); - - if ($first === null) { - $this->scanner->error('Expected escape sequence.'); - } - - if (Character::isNewline($first)) { - $this->scanner->error('Expected escape sequence.'); - } - - if (Character::isHex($first)) { - $value = 0; - for ($i = 0; $i < 6; $i++) { - $next = $this->scanner->peekChar(); - - if ($next === null || !Character::isHex($next)) { - break; - } - - $value *= 16; - $value += hexdec($this->scanner->readChar()); - assert(\is_int($value)); - } - - $this->scanCharIf([Character::class, 'isWhitespace']); - $valueText = Util::mbChr($value); - } else { - $valueText = $this->scanner->readUtf8Char(); - $value = Util::mbOrd($valueText); - } - - if ($identifierStart ? Character::isNameStart($valueText) : Character::isName($valueText)) { - if ($value > 0x10ffff) { - $this->scanner->error('Invalid Unicode code point.', $start); - } - - return $valueText; - } - - if ($value < 0x1f || $valueText === "\x7f" || ($identifierStart && Character::isDigit($valueText))) { - return '\\' . bin2hex($valueText) . ' '; - } - - return '\\' . $valueText; - } - - /** - * Consumes an escape sequence and returns the character it represents. - */ - protected function escapeCharacter(): string - { - return ParserUtil::consumeEscapedCharacter($this->scanner); - } - - /** - * @param callable(string): bool $condition - * - * @phpstan-impure - */ - protected function scanCharIf(callable $condition): bool - { - $next = $this->scanner->peekChar(); - - if ($next === null || !$condition($next)) { - return false; - } - - $this->scanner->readChar(); - - return true; - } - - /** - * Consumes the next character or escape sequence if it matches $character. - * - * Matching will be case-insensitive unless $caseSensitive is true. - * When matching case-insensitively, $character must be passed in lowercase. - * - * This only supports ASCII identifier characters. - */ - protected function scanIdentChar(string $character, bool $caseSensitive = false): bool - { - $matches = function (string $actual) use ($character, $caseSensitive): bool { - if ($caseSensitive) { - return $actual === $character; - } - - return \strtolower($actual) === $character; - }; - - $next = $this->scanner->peekChar(); - - if ($next !== null && $matches($next)) { - $this->scanner->readChar(); - - return true; - } - - if ($next === '\\') { - $start = $this->scanner->getPosition(); - - if ($matches($this->escapeCharacter())) { - return true; - } - - $this->scanner->setPosition($start); - } - - return false; - } - - /** - * Consumes the next character or escape sequence and asserts it matches $char. - * - * Matching will be case-insensitive unless $caseSensitive is true. - * When matching case-insensitively, $char must be passed in lowercase. - * - * This only supports ASCII identifier characters. - */ - protected function expectIdentChar(string $char, bool $caseSensitive = false): void - { - if ($this->scanIdentChar($char, $caseSensitive)) { - return; - } - - $this->scanner->error("Expected \"$char\""); - } - - /** - * Returns whether the scanner is immediately before a number. - * - * This follows [the CSS algorithm][]. - * - * [the CSS algorithm]: https://drafts.csswg.org/css-syntax-3/#starts-with-a-number - */ - protected function lookingAtNumber(): bool - { - $first = $this->scanner->peekChar(); - - if ($first === null) { - return false; - } - - if (Character::isDigit($first)) { - return true; - } - - if ($first === '.') { - $second = $this->scanner->peekChar(1); - - return $second !== null && Character::isDigit($second); - } - - if ($first === '+' || $first === '-') { - $second = $this->scanner->peekChar(1); - - if ($second === null) { - return false; - } - - if (Character::isDigit($second)) { - return true; - } - - if ($second !== '.') { - return false; - } - - $third = $this->scanner->peekChar(2); - - return $third !== null && Character::isDigit($third); - } - - return false; - } - - /** - * Returns whether the scanner is immediately before a plain CSS identifier. - * - * If $forward is passed, this looks that many characters forward instead. - * - * This is based on [the CSS algorithm][], but it assumes all backslashes - * start escapes. - * - * [the CSS algorithm]: https://drafts.csswg.org/css-syntax-3/#would-start-an-identifier - */ - protected function lookingAtIdentifier(int $forward = 0): bool - { - $first = $this->scanner->peekChar($forward); - - if ($first === null) { - return false; - } - - if ($first === '\\' || Character::isNameStart($first)) { - return true; - } - - if ($first !== '-') { - return false; - } - - $second = $this->scanner->peekChar($forward + 1); - - if ($second === null) { - return false; - } - - return $second === '\\' || $second === '-' || Character::isNameStart($second); - } - - /** - * Returns whether the scanner is immediately before a sequence of characters - * that could be part of a plain CSS identifier body. - */ - protected function lookingAtIdentifierBody(): bool - { - $next = $this->scanner->peekChar(); - - return $next !== null && ($next === '\\' || Character::isName($next)); - } - - /** - * Consumes an identifier if its name exactly matches $text. - * - * When matching case-insensitively, $text must be passed in lowercase. - * - * This only supports ASCII identifiers. - */ - protected function scanIdentifier(string $text, bool $caseSensitive = false): bool - { - if (!$this->lookingAtIdentifier()) { - return false; - } - - $start = $this->scanner->getPosition(); - - if ($this->consumeIdentifier($text, $caseSensitive) && !$this->lookingAtIdentifierBody()) { - return true; - } - - $this->scanner->setPosition($start); - - return false; - } - - /** - * Returns whether an identifier whose name exactly matches $text is at the - * current scanner position. - * - * This doesn't move the scan pointer forward - */ - protected function matchesIdentifier(string $text, bool $caseSensitive = false): bool - { - if (!$this->lookingAtIdentifier()) { - return false; - } - - $start = $this->scanner->getPosition(); - $result = $this->consumeIdentifier($text, $caseSensitive) && !$this->lookingAtIdentifierBody(); - $this->scanner->setPosition($start); - - return $result; - } - - /** - * Consumes $text as an identifer, but doesn't verify whether there's - * additional identifier text afterwards. - * - * Returns `true` if the full $text is consumed and `false` otherwise, but - * doesn't reset the scan pointer. - */ - private function consumeIdentifier(string $text, bool $caseSensitive): bool - { - for ($i = 0; $i < \strlen($text); $i++) { - if (!$this->scanIdentChar($text[$i], $caseSensitive)) { - return false; - } - } - - return true; - } - - /** - * Consumes an identifier asserts that its name exactly matches $text. - * - * When matching case-insensitively, $text must be passed in lowercase. - * - * This only supports ASCII identifiers. - */ - protected function expectIdentifier(string $text, ?string $name = null, bool $caseSensitive = false): void - { - $name = $name ?? "\"$text\""; - - $start = $this->scanner->getPosition(); - - for ($i = 0; $i < \strlen($text); $i++) { - if ($this->scanIdentChar($text[$i], $caseSensitive)) { - continue; - } - - $this->scanner->error("Expected $name.", $start); - } - - if (!$this->lookingAtIdentifierBody()) { - return; - } - - $this->scanner->error("Expected $name.", $start); - } - - /** - * Runs $consumer and returns the source text that it consumes. - * - * @param callable(): void $consumer - */ - protected function rawText(callable $consumer): string - { - $start = $this->scanner->getPosition(); - $consumer(); - - return $this->scanner->substring($start); - } - - /** - * Prints a warning to standard error, associated with $span. - */ - protected function warn(string $message, FileSpan $span): void - { - $this->logger->warn($message, false, $span); - } - - /** - * Throws an error associated with $position. - * - * @throws FormatException - * - * @return never-returns - */ - protected function error(string $message, FileSpan $span, ?\Throwable $previous = null): void - { - throw new FormatException($message, $span, $previous); - } - - protected function wrapException(FormatException $error): SassFormatException - { - $span = $error->getSpan(); - - if ($span->getLength() === 0 && 0 === stripos($error->getMessage(), 'expected')) { - $startPosition = $this->firstNewlineBefore($span->getStart()->getOffset()); - - if ($startPosition !== $span->getStart()->getOffset()) { - $span = $span->getFile()->span($startPosition, $startPosition); - } - } - - return new SassFormatException($error->getMessage(), $span, $error); - } - - /** - * If [position] is separated from the previous non-whitespace character in - * `$scanner->getString()` by one or more newlines, returns the offset of the last - * separating newline. - * - * Otherwise returns $position. - * - * This helps avoid missing token errors pointing at the next closing bracket - * rather than the line where the problem actually occurred. - * - * @param int $position - * - * @return int - */ - private function firstNewlineBefore(int $position): int - { - $index = $position - 1; - $lastNewline = null; - $string = $this->scanner->getString(); - - while ($index >= 0) { - $char = $string[$index]; - - if (!Character::isWhitespace($char)) { - return $lastNewline ?? $position; - } - - if (Character::isNewline($char)) { - $lastNewline = $index; - } - $index--; - } - - // If the document *only* contains whitespace before $position, always - // return $position. - - return $position; - } -} diff --git a/scssphp/src/Parser/ScssParser.php b/scssphp/src/Parser/ScssParser.php deleted file mode 100644 index 7390bea..0000000 --- a/scssphp/src/Parser/ScssParser.php +++ /dev/null @@ -1,275 +0,0 @@ -almostAnyValue(); - } - - protected function expectStatementSeparator(?string $name = null): void - { - $this->whitespaceWithoutComments(); - - if ($this->scanner->isDone()) { - return; - } - - $next = $this->scanner->peekChar(); - - if ($next === ';' || $next === '}') { - return; - } - - $this->scanner->expectChar(';'); - } - - protected function atEndOfStatement(): bool - { - $next = $this->scanner->peekChar(); - - return $next === null || $next === ';' || $next === '}' || $next === '{'; - } - - protected function lookingAtChildren(): bool - { - return $this->scanner->peekChar() === '{'; - } - - protected function scanElse(int $ifIndentation): bool - { - $start = $this->scanner->getPosition(); - $this->whitespace(); - $beforeAt = $this->scanner->getPosition(); - - if ($this->scanner->scanChar('@')) { - if ($this->scanIdentifier('else', true)) { - return true; - } - - if ($this->scanIdentifier('elseif', true)) { - $this->logger->warn("@elseif is deprecated and will not be supported in future Sass versions.\n\nRecommendation: @else if", true, $this->scanner->spanFrom($beforeAt)); - - $this->scanner->setPosition($this->scanner->getPosition() - 2); - - return true; - } - } - - $this->scanner->setPosition($start); - - return false; - } - - protected function children(callable $child): array - { - $this->scanner->expectChar('{'); - $this->whitespaceWithoutComments(); - $children = []; - - while (true) { - switch ($this->scanner->peekChar()) { - case '$': - $children[] = $this->variableDeclarationWithoutNamespace(); - break; - - case '/': - switch ($this->scanner->peekChar(1)) { - case '/': - $children[] = $this->silentCommentStatement(); - $this->whitespaceWithoutComments(); - break; - - case '*': - $children[] = $this->loudCommentStatement(); - $this->whitespaceWithoutComments(); - break; - - default: - $children[] = $child(); - break; - } - break; - - case ';': - $this->scanner->readChar(); - $this->whitespaceWithoutComments(); - break; - - case '}': - $this->scanner->expectChar('}'); - - return $children; - - default: - $children[] = $child(); - break; - } - } - } - - protected function statements(callable $statement): array - { - $statements = []; - $this->whitespaceWithoutComments(); - - while (!$this->scanner->isDone()) { - switch ($this->scanner->peekChar()) { - case '$': - $statements[] = $this->variableDeclarationWithoutNamespace(); - break; - - case '/': - switch ($this->scanner->peekChar(1)) { - case '/': - $statements[] = $this->silentCommentStatement(); - $this->whitespaceWithoutComments(); - break; - - case '*': - $statements[] = $this->loudCommentStatement(); - $this->whitespaceWithoutComments(); - break; - - default: - $child = $statement(); - - if ($child !== null) { - $statements[] = $child; - } - break; - } - break; - - case ';': - $this->scanner->readChar(); - $this->whitespaceWithoutComments(); - break; - - default: - $child = $statement(); - - if ($child !== null) { - $statements[] = $child; - } - break; - } - } - - return $statements; - } - - /** - * Consumes a statement-level silent comment block. - */ - private function silentCommentStatement(): SilentComment - { - $start = $this->scanner->getPosition(); - - $this->scanner->expect('//'); - - do { - while (!$this->scanner->isDone() && !Character::isNewline($this->scanner->readChar())) { - // Ignore the content of the comment - } - - if ($this->scanner->isDone()) { - break; - } - - $this->whitespaceWithoutComments(); - } while ($this->scanner->scan('//')); - - if ($this->isPlainCss()) { - $this->error('Silent comments aren\'t allowed in plain CSS.', $this->scanner->spanFrom($start)); - } - - $this->lastSilentComment = new SilentComment($this->scanner->substring($start), $this->scanner->spanFrom($start)); - - return $this->lastSilentComment; - } - - /** - * Consumes a statement-level loud comment block. - */ - private function loudCommentStatement(): LoudComment - { - $start = $this->scanner->getPosition(); - - $this->scanner->expect('/*'); - - $buffer = new InterpolationBuffer(); - $buffer->write('/*'); - - while (true) { - switch ($this->scanner->peekChar()) { - case '#': - if ($this->scanner->peekChar(1) === '{') { - $buffer->add($this->singleInterpolation()); - } else { - $buffer->write($this->scanner->readChar()); - } - break; - - case '*': - $buffer->write($this->scanner->readChar()); - - if ($this->scanner->peekChar() !== '/') { - break; - } - - $buffer->write($this->scanner->readChar()); - - return new LoudComment($buffer->buildInterpolation($this->scanner->spanFrom($start))); - - case "\r": - $this->scanner->readChar(); - - if ($this->scanner->peekChar() !== "\n") { - $buffer->write("\n"); - } - break; - - case "\f": - $this->scanner->readChar(); - $buffer->write("\n"); - break; - - default: - $buffer->write($this->scanner->readUtf8Char()); - } - } - } -} diff --git a/scssphp/src/Parser/SelectorParser.php b/scssphp/src/Parser/SelectorParser.php deleted file mode 100644 index 5864043..0000000 --- a/scssphp/src/Parser/SelectorParser.php +++ /dev/null @@ -1,574 +0,0 @@ -allowParent = $allowParent; - $this->allowPlaceholder = $allowPlaceholder; - parent::__construct($contents, $logger, $url); - } - - public function parse(): SelectorList - { - try { - $selector = $this->selectorList(); - - if (!$this->scanner->isDone()) { - $this->scanner->error('expected selector.'); - } - - return $selector; - } catch (FormatException $e) { - throw $this->wrapException($e); - } - } - - public function parseComplexSelector(): ComplexSelector - { - try { - $complex = $this->complexSelector(); - - if (!$this->scanner->isDone()) { - $this->scanner->error('expected selector.'); - } - - return $complex; - } catch (FormatException $e) { - throw $this->wrapException($e); - } - } - - public function parseCompoundSelector(): CompoundSelector - { - try { - $compound = $this->compoundSelector(); - - if (!$this->scanner->isDone()) { - $this->scanner->error('expected selector.'); - } - - return $compound; - } catch (FormatException $e) { - throw $this->wrapException($e); - } - } - - public function parseSimpleSelector(): SimpleSelector - { - try { - $simple = $this->simpleSelector(); - - if (!$this->scanner->isDone()) { - $this->scanner->error('unexpected token.'); - } - - return $simple; - } catch (FormatException $e) { - throw $this->wrapException($e); - } - } - - /** - * Consumes a selector list. - */ - private function selectorList(): SelectorList - { - $previousLine = $this->scanner->getLine(); - $components = [$this->complexSelector()]; - - $this->whitespace(); - while ($this->scanner->scanChar(',')) { - $this->whitespace(); - $next = $this->scanner->peekChar(); - - if ($next === ',') { - continue; - } - - if ($this->scanner->isDone()) { - break; - } - - $lineBreak = $this->scanner->getLine() !== $previousLine; - - if ($lineBreak) { - $previousLine = $this->scanner->getLine(); - } - - $components[] = $this->complexSelector($lineBreak); - } - - return new SelectorList($components); - } - - /** - * Consumes a complex selector. - * - * If $lineBreak is `true`, that indicates that there was a line break - * before this selector. - */ - private function complexSelector(bool $lineBreak = false): ComplexSelector - { - $lastCompound = null; - $combinators = []; - - $initialCombinators = null; - $components = []; - - while (true) { - $this->whitespace(); - - $next = $this->scanner->peekChar(); - - switch ($next) { - case '+': - $this->scanner->readChar(); - $combinators[] = Combinator::NEXT_SIBLING; - break; - - case '>': - $this->scanner->readChar(); - $combinators[] = Combinator::CHILD; - break; - - case '~': - $this->scanner->readChar(); - $combinators[] = Combinator::FOLLOWING_SIBLING; - break; - - default: - if ($next === null || (!\in_array($next, ['[', '.', '#', '%', ':', '&', '*', '|'], true) && !$this->lookingAtIdentifier())) { - break 2; - } - - if ($lastCompound !== null) { - $components[] = new ComplexSelectorComponent($lastCompound, $combinators); - } elseif (\count($combinators) !== 0) { - \assert($initialCombinators === null); - $initialCombinators = $combinators; - } - $lastCompound = $this->compoundSelector(); - $combinators = []; - - if ($this->scanner->peekChar() === '&') { - $this->scanner->error('"&" may only used at the beginning of a compound selector.'); - } - break; - } - } - - if ($lastCompound !== null) { - $components[] = new ComplexSelectorComponent($lastCompound, $combinators); - } elseif (\count($combinators) !== 0) { - $initialCombinators = $combinators; - } else { - $this->scanner->error('expected selector.'); - } - - return new ComplexSelector($initialCombinators ?? [], $components, $lineBreak); - } - - /** - * Consumes a compound selector. - */ - private function compoundSelector(): CompoundSelector - { - $components = [$this->simpleSelector()]; - - while (Character::isSimpleSelectorStart($this->scanner->peekChar())) { - $components[] = $this->simpleSelector(false); - } - - return new CompoundSelector($components); - } - - /** - * Consumes a simple selector. - * - * If $allowParent is passed, it controls whether the parent selector `&` is - * allowed. Otherwise, it defaults to {@see allowParent}. - */ - private function simpleSelector(?bool $allowParent = null): SimpleSelector - { - $start = $this->scanner->getPosition(); - $allowParent = $allowParent ?? $this->allowParent; - - switch ($this->scanner->peekChar()) { - case '[': - return $this->attributeSelector(); - - case '.': - return $this->classSelector(); - - case '#': - return $this->idSelector(); - - case '%': - $selector = $this->placeholderSelector(); - if (!$this->allowPlaceholder) { - $this->error("Placeholder selectors aren't allowed here.", $this->scanner->spanFrom($start)); - } - return $selector; - - case ':': - return $this->pseudoSelector(); - - case '&': - $selector = $this->parentSelector(); - if (!$allowParent) { - $this->error("Parent selectors aren't allowed here.", $this->scanner->spanFrom($start)); - } - return $selector; - - default: - return $this->typeOrUniversalSelector(); - } - } - - /** - * Consumes an attribute selector. - */ - private function attributeSelector(): AttributeSelector - { - $this->scanner->expectChar('['); - $this->whitespace(); - - $name = $this->attributeName(); - $this->whitespace(); - - if ($this->scanner->scanChar(']')) { - return AttributeSelector::create($name); - } - - $operator = $this->attributeOperator(); - $this->whitespace(); - - $next = $this->scanner->peekChar(); - $value = $next === "'" || $next === '"' ? $this->string() : $this->identifier(); - $this->whitespace(); - - $next = $this->scanner->peekChar(); - $modifier = $next !== null && Character::isAlphabetic($next) ? $this->scanner->readChar() : null; - - $this->scanner->expectChar(']'); - - return AttributeSelector::withOperator($name, $operator, $value, $modifier); - } - - /** - * Consumes a qualified name as part of an attribute selector. - */ - private function attributeName(): QualifiedName - { - if ($this->scanner->scanChar('*')) { - $this->scanner->expectChar('|'); - - return new QualifiedName($this->identifier(), '*'); - } - - $nameOrNamespace = $this->identifier(); - - if ($this->scanner->peekChar() !== '|' || $this->scanner->peekChar(1) === '=') { - return new QualifiedName($nameOrNamespace); - } - - $this->scanner->readChar(); - - return new QualifiedName($this->identifier(), $nameOrNamespace); - } - - /** - * Consumes an attribute selector's operator. - * - * @phpstan-return AttributeOperator::* - */ - private function attributeOperator(): string - { - $start = $this->scanner->getPosition(); - - switch ($this->scanner->readChar()) { - case '=': - return AttributeOperator::EQUAL; - - case '~': - $this->scanner->expectChar('='); - return AttributeOperator::INCLUDE; - - case '|': - $this->scanner->expectChar('='); - return AttributeOperator::DASH; - - case '^': - $this->scanner->expectChar('='); - return AttributeOperator::PREFIX; - - case '$': - $this->scanner->expectChar('='); - return AttributeOperator::SUFFIX; - - case '*': - $this->scanner->expectChar('='); - return AttributeOperator::SUBSTRING; - - default: - $this->scanner->error('Expected "]".', $start); - } - } - - /** - * Consumes a class selector. - */ - private function classSelector(): ClassSelector - { - $this->scanner->expectChar('.'); - $name = $this->identifier(); - - return new ClassSelector($name); - } - - /** - * Consumes an ID selector. - */ - private function idSelector(): IDSelector - { - $this->scanner->expectChar('#'); - $name = $this->identifier(); - - return new IDSelector($name); - } - - /** - * Consumes a placeholder selector. - */ - private function placeholderSelector(): PlaceholderSelector - { - $this->scanner->expectChar('%'); - $name = $this->identifier(); - - return new PlaceholderSelector($name); - } - - /** - * Consumes a parent selector. - */ - private function parentSelector(): ParentSelector - { - $this->scanner->expectChar('&'); - $suffix = $this->lookingAtIdentifierBody() ? $this->identifierBody() : null; - - return new ParentSelector($suffix); - } - - /** - * Consumes a pseudo selector. - */ - private function pseudoSelector(): PseudoSelector - { - $this->scanner->expectChar(':'); - $element = $this->scanner->scanChar(':'); - $name = $this->identifier(); - - if (!$this->scanner->scanChar('(')) { - return new PseudoSelector($name, $element); - } - $this->whitespace(); - - $unvendored = Util::unvendor($name); - $argument = null; - $selector = null; - - if ($element) { - if (\in_array($unvendored, self::SELECTOR_PSEUDO_ELEMENTS, true)) { - $selector = $this->selectorList(); - } else { - $argument = $this->declarationValue(true); - } - } elseif (\in_array($unvendored, self::SELECTOR_PSEUDO_CLASSES, true)) { - $selector = $this->selectorList(); - } elseif ($unvendored === 'nth-child' || $unvendored === 'nth-last-child') { - $argument = $this->aNPlusB(); - $this->whitespace(); - - if (Character::isWhitespace($this->scanner->peekChar(-1)) && $this->scanner->peekChar() !== ')') { - $this->expectIdentifier('of'); - $argument .= ' of'; - $this->whitespace(); - - $selector = $this->selectorList(); - } - } else { - $argument = rtrim($this->declarationValue(true)); - } - - $this->scanner->expectChar(')'); - - return new PseudoSelector($name, $element, $argument, $selector); - } - - /** - * Consumes an [`An+B` production][An+B] and returns its text. - * - * [An+B]: https://drafts.csswg.org/css-syntax-3/#anb-microsyntax - */ - private function aNPlusB(): string - { - $buffer = ''; - - switch ($this->scanner->peekChar()) { - case 'e': - case 'E': - $this->expectIdentifier('even'); - return 'even'; - - case 'o': - case 'O': - $this->expectIdentifier('odd'); - return 'odd'; - - case '+': - case '-': - $buffer .= $this->scanner->readChar(); - break; - } - - $first = $this->scanner->peekChar(); - - if ($first !== null && Character::isDigit($first)) { - while (Character::isDigit($this->scanner->peekChar())) { - $buffer .= $this->scanner->readChar(); - } - $this->whitespace(); - - if (!$this->scanIdentChar('n')) { - return $buffer; - } - } else { - $this->expectIdentChar('n'); - } - $buffer .= 'n'; - $this->whitespace(); - - $next = $this->scanner->peekChar(); - if ($next !== '+' && $next !== '-') { - return $buffer; - } - $buffer .= $this->scanner->readChar(); - $this->whitespace(); - - $last = $this->scanner->peekChar(); - if ($last === null || !Character::isDigit($last)) { - $this->scanner->error('Expected a number.'); - } - while (Character::isDigit($this->scanner->peekChar())) { - $buffer .= $this->scanner->readChar(); - } - - return $buffer; - } - - /** - * Consumes a type selector or a universal selector. - * - * These are combined because either one could start with `*`. - */ - private function typeOrUniversalSelector(): SimpleSelector - { - $first = $this->scanner->peekChar(); - - if ($first === '*') { - $this->scanner->readChar(); - - if (!$this->scanner->scanChar('|')) { - return new UniversalSelector(); - } - - if ($this->scanner->scanChar('*')) { - return new UniversalSelector('*'); - } - - return new TypeSelector(new QualifiedName($this->identifier(), '*')); - } - - if ($first === '|') { - $this->scanner->readChar(); - - if ($this->scanner->scanChar('*')) { - return new UniversalSelector(''); - } - - return new TypeSelector(new QualifiedName($this->identifier(), '')); - } - - $nameOrNamespace = $this->identifier(); - - if (!$this->scanner->scanChar('|')) { - return new TypeSelector(new QualifiedName($nameOrNamespace)); - } - - if ($this->scanner->scanChar('*')) { - return new UniversalSelector($nameOrNamespace); - } - - return new TypeSelector(new QualifiedName($this->identifier(), $nameOrNamespace)); - } -} diff --git a/scssphp/src/Parser/StringScanner.php b/scssphp/src/Parser/StringScanner.php deleted file mode 100644 index 572269c..0000000 --- a/scssphp/src/Parser/StringScanner.php +++ /dev/null @@ -1,348 +0,0 @@ -string = $content; - $this->sourceFile = new SourceFile($content, $sourceUrl); - } - - public function getString(): string - { - return $this->string; - } - - public function getPosition(): int - { - return $this->position; - } - - public function setPosition(int $position): void - { - $this->position = $position; - } - - public function spanFrom(int $start, ?int $end = null): FileSpan - { - $end = $end ?? $this->position; - - return $this->sourceFile->span($start, $end); - } - - /** - * Returns an empty span at the current location. - */ - public function getEmptySpan(): FileSpan - { - return $this->sourceFile->span($this->position, $this->position); - } - - public function isDone(): bool - { - return $this->position === \strlen($this->string); - } - - /** - * @throws FormatException if the end of the string is reached - * - * @phpstan-impure - */ - public function readChar(): string - { - if ($this->position === \strlen($this->string)) { - $this->fail('more input'); - } - - return $this->string[$this->position++]; - } - - /** - * @throws FormatException if the end of the string is reached - * - * @phpstan-impure - */ - public function readUtf8Char(): string - { - if ($this->position === \strlen($this->string)) { - $this->fail('more input'); - } - - if (\ord($this->string[$this->position]) < 0x80) { - return $this->string[$this->position++]; - } - - if (!preg_match('/./usA', $this->string, $m, 0, $this->position)) { - $this->fail('utf-8 char'); - } - - $this->position += \strlen($m[0]); - - return $m[0]; - } - - /** - * Consumes the next character in the string if it the provided character. - * - * @param string $char - * - * @return bool Whether the character was consumed. - * - * @phpstan-impure - */ - public function scanChar(string $char): bool - { - if ($this->position === \strlen($this->string)) { - return false; - } - - if ($this->string[$this->position] !== $char) { - return false; - } - - ++$this->position; - - return true; - } - - /** - * Consumes the provided string if it appears at the current position. - * - * @param string $string - * - * @return bool Whether the string was consumed. - * - * @phpstan-impure - */ - public function scan(string $string): bool - { - if (!$this->matches($string)) { - return false; - } - - $this->position += \strlen($string); - - return true; - } - - /** - * Returns whether or not the provided string appears at the current position. - * - * This doesn't move the scan pointer forward. - */ - public function matches(string $string): bool - { - if ($this->position - 1 + \strlen($string) >= \strlen($this->string)) { - return false; - } - - return substr($this->string, $this->position, \strlen($string)) === $string; - } - - /** - * If the next character in the string is $character, consumes it. - * - * If $character could not be consumed, throws an exception - * describing the position of the failure. $name is used in this error as - * the expected name of the character being matched; if it's `null`, the - * character itself is used instead. - * - * @param string $character - * @param string|null $name - * - * @return void - * - * @throws FormatException - * - * @phpstan-impure - */ - public function expectChar(string $character, ?string $name = null): void - { - if ($this->scanChar($character)) { - return; - } - - if ($name === null) { - $name = '"' . $character . '"'; - } - - $this->fail($name); - } - - /** - * @param string $string - * - * @return void - * - * @throws FormatException - * - * @phpstan-impure - */ - public function expect(string $string): void - { - if ($this->scan($string)) { - return; - } - - $this->fail('"' . $string . '"'); - } - - /** - * @throws FormatException - */ - public function expectDone(): void - { - if ($this->isDone()) { - return; - } - - $this->fail('no more input'); - } - - /** - * Returns the character at the given offset of the current position. - * - * The offset can be negative to peek already seen characters. - * Returns null if the offset goes out of range. - * This does not affect the position or the last match. - * - * @param int $offset - * - * @return string|null - */ - public function peekChar(int $offset = 0): ?string - { - $pos = $this->position + $offset; - - if ($pos < 0 || $pos >= \strlen($this->string)) { - return null; - } - - return $this->string[$pos]; - } - - /** - * Returns the substring of the string between $start and $end (excluded). - * - * $end defaults to the current position. - * - * @param int $start - * @param int|null $end - * - * @return string - */ - public function substring(int $start, ?int $end = null): string - { - if ($end === null) { - $end = $this->position; - } - - if ($end < $start) { - return ''; - } - - return substr($this->string, $start, $end - $start); - } - - /** - * The scanner's current (zero-based) line number. - * - * @return int - */ - public function getLine(): int - { - return $this->sourceFile->getLine($this->position); - } - - /** - * The scanner's current (zero-based) column number. - * - * @return int - */ - public function getColumn(): int - { - return $this->sourceFile->getColumn($this->position); - } - - /** - * @param string $message - * @param int|null $position - * @param int|null $length - * - * @throws FormatException - * - * @return never-returns - */ - public function error(string $message, ?int $position = null, ?int $length = null) - { - $position = $position ?? $this->position; - $length = $length ?? 0; - - $span = $this->sourceFile->span($position, $position + $length); - - throw new FormatException($message, $span); - } - - /** - * @param string $message - * - * @throws FormatException - * - * @return never-returns - */ - private function fail(string $message) - { - $this->error("expected $message."); - } -} diff --git a/scssphp/src/Parser/StylesheetParser.php b/scssphp/src/Parser/StylesheetParser.php deleted file mode 100644 index 00af6f1..0000000 --- a/scssphp/src/Parser/StylesheetParser.php +++ /dev/null @@ -1,4544 +0,0 @@ - - */ - private $globalVariables = []; - - /** - * @var \Closure - * @readonly - */ - private $statementCallable; - - /** - * @var \Closure - * @readonly - */ - private $declarationChildCallable; - - /** - * @var \Closure - * @readonly - */ - private $functionChildCallable; - - public function __construct(string $contents, ?LoggerInterface $logger = null, ?string $sourceUrl = null) - { - parent::__construct($contents, $logger, $sourceUrl); - - // Store callables for some private methods, to ensure they pass callable typehints when passed - // to parent methods expecting a callable, due to the semantic of PHP array callables. - $this->statementCallable = \Closure::fromCallable([$this, 'statement']); - $this->declarationChildCallable = \Closure::fromCallable([$this, 'declarationChild']); - $this->functionChildCallable = \Closure::fromCallable([$this, 'functionChild']); - } - - /** - * @throws SassFormatException when parsing fails - */ - public function parse(): Stylesheet - { - try { - $start = $this->scanner->getPosition(); - - // Allow a byte-order mark at the beginning of the document. - $this->scanner->scan("\u{FEFF}"); - - $statements = $this->statements(function () { - // Handle this specially so that {@see atRule} always returns a non-nullable Statement. - if ($this->scanner->scan('@charset')) { - $this->whitespace(); - $this->string(); - - return null; - } - - return $this->statement(true); - }); - - $this->scanner->expectDone(); - - // Ensure that all global variable assignments produce a variable in this - // stylesheet, even if they aren't evaluated. See sass/language#50. - foreach ($this->globalVariables as $declaration) { - $statements[] = new VariableDeclaration($declaration->getName(), new NullExpression($declaration->getExpression()->getSpan()), $declaration->getSpan(), null, true); - } - - return new Stylesheet($statements, $this->scanner->spanFrom($start), $this->isPlainCss()); - } catch (FormatException $e) { - throw $this->wrapException($e); - } - } - - public function parseArgumentDeclaration(): ArgumentDeclaration - { - try { - $this->scanner->expectChar('@', '@-rule'); - $this->identifier(); - $this->whitespace(); - $this->identifier(); - $arguments = $this->argumentDeclaration(); - $this->whitespace(); - $this->scanner->expectChar('{'); - - $this->scanner->expectDone(); - - return $arguments; - } catch (FormatException $e) { - throw $this->wrapException($e); - } - } - - /** - * Consumes a statement that's allowed at the top level of the stylesheet or - * within nested style and at rules. - * - * If $root is `true`, this parses at-rules that are allowed only at the - * root of the stylesheet. - */ - private function statement(bool $root = false): Statement - { - switch ($this->scanner->peekChar()) { - case '@': - return $this->atRule($this->statementCallable, $root); - - case '+': - if (!$this->isIndented()) { - return $this->styleRule(); - } - - throw new \BadMethodCallException('The parsing of the indented syntax is not implemented.'); - - case '=': - if (!$this->isIndented()) { - return $this->styleRule(); - } - - throw new \BadMethodCallException('The parsing of the indented syntax is not implemented.'); - - case '}': - $this->scanner->error('unmatched "}".'); - - default: - if ($this->inStyleRule || $this->inUnknownAtRule || $this->inMixin || $this->inContentBlock) { - return $this->declarationOrStyleRule(); - } - - return $this->variableDeclarationOrStyleRule(); - } - } - - /** - * Consumes a namespaced variable declaration. - * - * @throws FormatException - */ - private function variableDeclarationWithNamespace(): VariableDeclaration - { - $start = $this->scanner->getPosition(); - $namespace = $this->identifier(); - $this->scanner->expectChar('.'); - - return $this->variableDeclarationWithoutNamespace($namespace, $start); - } - - /** - * Consumes a variable declaration. - */ - protected function variableDeclarationWithoutNamespace(?string $namespace = null, ?int $start = null): VariableDeclaration - { - $precedingComment = $this->lastSilentComment; - $this->lastSilentComment = null; - $start = $start ?? $this->scanner->getPosition(); - - $name = $this->variableName(); - - if ($namespace !== null) { - $this->assertPublic($name, function () use ($start) { - return $this->scanner->spanFrom($start); - }); - } - - $this->whitespace(); - $this->scanner->expectChar(':'); - $this->whitespace(); - - $value = $this->expression(); - - $guarded = false; - $global = false; - $flagStart = $this->scanner->getPosition(); - - while ($this->scanner->scanChar('!')) { - $flag = $this->identifier(); - if ($flag === 'default') { - $guarded = true; - } elseif ($flag === 'global') { - if ($namespace !== null) { - $this->error("!global isn't allowed for variables in other modules.", $this->scanner->spanFrom($flagStart)); - } - - $global = true; - } else { - $this->error('Invalid flag name.', $this->scanner->spanFrom($flagStart)); - } - - $this->whitespace(); - $flagStart = $this->scanner->getPosition(); - } - - $this->expectStatementSeparator('variable declaration'); - - // TODO remove this when implementing modules - if ($namespace !== null) { - $this->error('Sass modules are not implemented yet.', $this->scanner->spanFrom($start)); - } - - $declaration = new VariableDeclaration($name, $value, $this->scanner->spanFrom($start), $namespace, $guarded, $global, $precedingComment); - - if ($global && !isset($this->globalVariables[$name])) { - $this->globalVariables[$name] = $declaration; - } - - return $declaration; - } - - private function variableDeclarationOrStyleRule(): Statement - { - if ($this->isPlainCss()) { - return $this->styleRule(); - } - - if (!$this->lookingAtIdentifier()) { - return $this->styleRule(); - } - - $start = $this->scanner->getPosition(); - $variableOrInterpolation = $this->variableDeclarationOrInterpolation(); - - if ($variableOrInterpolation instanceof VariableDeclaration) { - return $variableOrInterpolation; - } - - $buffer = new InterpolationBuffer(); - $buffer->addInterpolation($variableOrInterpolation); - - return $this->styleRule($buffer, $start); - } - - /** - * Consumes a {@see VariableDeclaration}, a {@see Declaration}, or a {@see StyleRule}. - * - * @throws FormatException - */ - private function declarationOrStyleRule(): Statement - { - if ($this->isPlainCss() && $this->inStyleRule && !$this->inUnknownAtRule) { - return $this->propertyOrVariableDeclaration(); - } - - $start = $this->scanner->getPosition(); - - $declarationBuffer = $this->declarationOrBuffer(); - - if ($declarationBuffer instanceof Statement) { - return $declarationBuffer; - } - - return $this->styleRule($declarationBuffer, $start); - } - - /** - * Tries to parse a variable or property declaration, and returns the value - * parsed so far if it fails. - * - * This can return either an {@see InterpolationBuffer}, indicating that it - * couldn't consume a declaration and that selector parsing should be - * attempted; or it can return a {@see Declaration} or a {@see VariableDeclaration}, - * indicating that it successfully consumed a declaration. - * - * @return Statement|InterpolationBuffer - */ - private function declarationOrBuffer() - { - $start = $this->scanner->getPosition(); - $nameBuffer = new InterpolationBuffer(); - $first = $this->scanner->peekChar(); - $startsWithPunctuation = false; - - // Allow the "*prop: val", ":prop: val", "#prop: val", and ".prop: val" - // hacks. - if ($first === ':' || $first === '*' || $first === '.' || ($first === '#' && $this->scanner->peekChar(1) !== '{')) { - $startsWithPunctuation = true; - $nameBuffer->write($this->scanner->readChar()); - $nameBuffer->write($this->rawText([$this, 'whitespace'])); - } - - if (!$this->lookingAtInterpolatedIdentifier()) { - return $nameBuffer; - } - - $variableOrInterpolation = $startsWithPunctuation ? $this->interpolatedIdentifier() : $this->variableDeclarationOrInterpolation(); - - if ($variableOrInterpolation instanceof VariableDeclaration) { - return $variableOrInterpolation; - } - - $nameBuffer->addInterpolation($variableOrInterpolation); - - $this->isUseAllowed = false; - - if ($this->scanner->matches('/*')) { - $nameBuffer->write($this->rawText([$this, 'loudComment'])); - } - - $midBuffer = $this->rawText([$this, 'whitespace']); - $beforeColon = $this->scanner->getPosition(); - - if (!$this->scanner->scanChar(':')) { - if ($midBuffer !== '') { - $nameBuffer->write(' '); - } - - return $nameBuffer; - } - - $midBuffer .= ':'; - - // Parse custom properties as declarations no matter what. - $name = $nameBuffer->buildInterpolation($this->scanner->spanFrom($start, $beforeColon)); - - if (0 === strpos($name->getInitialPlain(), '--')) { - $value = new StringExpression($this->interpolatedDeclarationValue()); - $this->expectStatementSeparator('custom property'); - - return Declaration::create($name, $value, $this->scanner->spanFrom($start)); - } - - if ($this->scanner->scanChar(':')) { - $nameBuffer->write($midBuffer); - $nameBuffer->write(':'); - - return $nameBuffer; - } - - if ($this->isIndented() && $this->lookingAtInterpolatedIdentifier()) { - $nameBuffer->write($midBuffer); - - return $nameBuffer; - } - - $postColonWhitespace = $this->rawText([$this, 'whitespace']); - - if ($this->lookingAtChildren()) { - return $this->withChildren($this->declarationChildCallable, $start, function (array $children, FileSpan $span) use ($name) { - return Declaration::nested($name, $children, $span); - }); - } - - $midBuffer .= $postColonWhitespace; - $couldBeSelector = $postColonWhitespace === '' && $this->lookingAtInterpolatedIdentifier(); - - $beforeDeclaration = $this->scanner->getPosition(); - - try { - $value = $this->expression(); - - if ($this->lookingAtChildren()) { - // Properties that are ambiguous with selectors can't have additional - // properties nested beneath them, so we force an error. This will be - // caught below and cause the text to be reparsed as a selector. - if ($couldBeSelector) { - $this->expectStatementSeparator(); - } - } elseif (!$this->atEndOfStatement()) { - // Force an exception if there isn't a valid end-of-property character - // but don't consume that character. This will also cause the text to be - // reparsed. - $this->expectStatementSeparator(); - } - - } catch (FormatException $e) { - if (!$couldBeSelector) { - throw $e; - } - - // If the value would be followed by a semicolon, it's definitely supposed - // to be a property, not a selector. - $this->scanner->setPosition($beforeDeclaration); - - $additional = $this->almostAnyValue(); - - if (!$this->isIndented() && $this->scanner->peekChar() === ';') { - throw $e; - } - - $nameBuffer->write($midBuffer); - $nameBuffer->addInterpolation($additional); - - return $nameBuffer; - } - - if ($this->lookingAtChildren()) { - return $this->withChildren($this->declarationChildCallable, $start, function (array $children, FileSpan $span) use ($name, $value) { - return Declaration::nested($name, $children, $span, $value); - }); - } - - $this->expectStatementSeparator(); - - return Declaration::create($name, $value, $this->scanner->spanFrom($start)); - } - - /** - * Tries to parse a namespaced {@see VariableDeclaration}, and returns the value - * parsed so far if it fails. - * - * This can return either an {@see Interpolation}, indicating that it couldn't - * consume a variable declaration and that property declaration or selector - * parsing should be attempted; or it can return a {@see VariableDeclaration}, - * indicating that it successfully consumed a variable declaration. - * - * @return Interpolation|VariableDeclaration - */ - private function variableDeclarationOrInterpolation() - { - if (!$this->lookingAtIdentifier()) { - return $this->interpolatedIdentifier(); - } - - $start = $this->scanner->getPosition(); - $identifier = $this->identifier(); - - if ($this->scanner->matches('.$')) { - $this->scanner->readChar(); - - return $this->variableDeclarationWithoutNamespace($identifier, $start); - } - - $buffer = new InterpolationBuffer(); - $buffer->write($identifier); - - // Parse the rest of an interpolated identifier if one exists, so callers - // don't have to. - if ($this->lookingAtInterpolatedIdentifierBody()) { - $buffer->addInterpolation($this->interpolatedIdentifier()); - } - - return $buffer->buildInterpolation($this->scanner->spanFrom($start)); - } - - /** - * Consumes a StyleRule - */ - private function styleRule(?InterpolationBuffer $buffer = null, ?int $start = null): StyleRule - { - $start = $start ?? $this->scanner->getPosition(); - $interpolation = $this->styleRuleSelector(); - - if ($buffer !== null) { - $buffer->addInterpolation($interpolation); - $interpolation = $buffer->buildInterpolation($this->scanner->spanFrom($start)); - } - - if (!$interpolation->getContents()) { - $this->scanner->error('expected "}".'); - } - - $wasInStyleRule = $this->inStyleRule; - $this->inStyleRule = true; - - return $this->withChildren($this->statementCallable, $start, function (array $children) use ($wasInStyleRule, $start, $interpolation) { - $this->inStyleRule = $wasInStyleRule; - - return new StyleRule($interpolation, $children, $this->scanner->spanFrom($start)); - }); - } - - /** - * Consumes either a property declaration or a namespaced variable declaration. - * - * This is only used in contexts where declarations are allowed but style - * rules are not, such as nested declarations. Otherwise, - * {@see declarationOrStyleRule} is used instead. - * - * If $parseCustomProperties is `true`, properties that begin with `--` will - * be parsed using custom property parsing rules. - */ - private function propertyOrVariableDeclaration(bool $parseCustomProperties = true): Statement - { - $start = $this->scanner->getPosition(); - - // Allow the "*prop: val", ":prop: val", "#prop: val", and ".prop: val" - // hacks. - $first = $this->scanner->peekChar(); - if ($first === ':' || $first === '*' || $first === '.' || ($first === '#' && $this->scanner->peekChar(1) !== '{')) { - $nameBuffer = new InterpolationBuffer(); - $nameBuffer->write($this->scanner->readChar()); - $nameBuffer->write($this->rawText([$this, 'whitespace'])); - $nameBuffer->addInterpolation($this->interpolatedIdentifier()); - $name = $nameBuffer->buildInterpolation($this->scanner->spanFrom($start)); - } elseif (!$this->isPlainCss()) { - $variableOrInterpolation = $this->variableDeclarationOrInterpolation(); - - if ($variableOrInterpolation instanceof VariableDeclaration) { - return $variableOrInterpolation; - } - - $name = $variableOrInterpolation; - } else { - $name = $this->interpolatedIdentifier(); - } - - $this->whitespace(); - $this->scanner->expectChar(':'); - - if ($parseCustomProperties && 0 === strpos($name->getInitialPlain(), '--')) { - $value = new StringExpression($this->interpolatedDeclarationValue()); - $this->expectStatementSeparator('custom property'); - - return Declaration::create($name, $value, $this->scanner->spanFrom($start)); - } - - $this->whitespace(); - - if ($this->lookingAtChildren()) { - if ($this->isPlainCss()) { - $this->scanner->error("Nested declarations aren't allowed in plain CSS."); - } - - return $this->withChildren($this->declarationChildCallable, $start, function (array $children, FileSpan $span) use ($name) { - return Declaration::nested($name, $children, $span); - }); - } - - $value = $this->expression(); - - if ($this->lookingAtChildren()) { - if ($this->isPlainCss()) { - $this->scanner->error("Nested declarations aren't allowed in plain CSS."); - } - - return $this->withChildren($this->declarationChildCallable, $start, function (array $children, FileSpan $span) use ($name, $value) { - return Declaration::nested($name, $children, $span, $value); - }); - } - - $this->expectStatementSeparator(); - - return Declaration::create($name, $value, $this->scanner->spanFrom($start)); - } - - /** - * Consumes a statement that's allowed within a declaration. - */ - private function declarationChild(): Statement - { - if ($this->scanner->peekChar() === '@') { - return $this->declarationAtRule(); - } - - return $this->propertyOrVariableDeclaration(false); - } - - /** - * Consumes an at-rule. - * - * This consumes at-rules that are allowed at all levels of the document; the - * $child parameter is called to consume any at-rules that are specifically - * allowed in the caller's context. - * - * If $root is `true`, this parses at-rules that are allowed only at the - * root of the stylesheet. - * - * @param callable(): Statement $child - */ - protected function atRule(callable $child, bool $root = false): Statement - { - $start = $this->scanner->getPosition(); - $this->scanner->expectChar('@', '@-rule'); - $name = $this->interpolatedIdentifier(); - $this->whitespace(); - - $wasUseAllowed = $this->isUseAllowed; - $this->isUseAllowed = false; - - switch ($name->getAsPlain()) { - case 'at-root': - return $this->atRootRule($start); - case 'content': - return $this->contentRule($start); - case 'debug': - return $this->debugRule($start); - case 'each': - return $this->eachRule($start, $child); - case 'else': - $this->disallowedAtRule($start); - case 'error': - return $this->errorRule($start); - case 'extend': - return $this->extendRule($start); - case 'for': - return $this->forRule($start, $child); - case 'forward': - $this->isUseAllowed = $wasUseAllowed; - - if (!$root) { - $this->disallowedAtRule($start); - } - - // TODO remove this when implementing modules - $this->error('Sass modules are not implemented yet.', $this->scanner->spanFrom($start)); - case 'function': - return $this->functionRule($start); - case 'if': - return $this->ifRule($start, $child); - case 'import': - return $this->importRule($start); - case 'include': - return $this->includeRule($start); - case 'media': - return $this->mediaRule($start); - case 'mixin': - return $this->mixinRule($start); - case '-moz-document': - return $this->mozDocumentRule($start, $name); - case 'return': - $this->disallowedAtRule($start); - case 'supports': - return $this->supportsRule($start); - case 'use': - $this->isUseAllowed = $wasUseAllowed; - - if (!$root) { - $this->disallowedAtRule($start); - } - - // TODO remove this when implementing modules - $this->error('Sass modules are not implemented yet.', $this->scanner->spanFrom($start)); - case 'warn': - return $this->warnRule($start); - case 'while': - return $this->whileRule($start, $child); - default: - return $this->unknownAtRule($start, $name); - } - } - - /** - * Consumes an at-rule allowed within a property declaration. - */ - private function declarationAtRule(): Statement - { - $start = $this->scanner->getPosition(); - $name = $this->plainAtRuleName(); - - switch ($name) { - case 'content': - return $this->contentRule($start); - case 'debug': - return $this->debugRule($start); - case 'each': - return $this->eachRule($start, $this->declarationChildCallable); - case 'else': - $this->disallowedAtRule($start); - case 'error': - return $this->errorRule($start); - case 'for': - return $this->forRule($start, $this->declarationChildCallable); - case 'if': - return $this->ifRule($start, $this->declarationChildCallable); - case 'include': - return $this->includeRule($start); - case 'warn': - return $this->warnRule($start); - case 'while': - return $this->whileRule($start, $this->declarationChildCallable); - default: - $this->disallowedAtRule($start); - } - } - - /** - * Consumes a statement allowed within a function. - */ - private function functionChild(): Statement - { - if ($this->scanner->peekChar() !== '@') { - $start = $this->scanner->getPosition(); - - try { - return $this->variableDeclarationWithNamespace(); - } catch (FormatException $variableDeclarationError) { - // TODO remove this when implementing modules - if ($variableDeclarationError->getMessage() === 'Sass modules are not implemented yet.') { - throw $variableDeclarationError; - } - - $this->scanner->setPosition($start); - - // If a variable declaration failed to parse, it's possible the user - // thought they could write a style rule or property declaration in a - // function. If so, throw a more helpful error message. - try { - $statement = $this->declarationOrStyleRule(); - } catch (FormatException $e) { - throw $variableDeclarationError; - } - - $this->error('@function rules may not contain ' . ($statement instanceof StyleRule ? 'style rules.' : 'declarations.'), $statement->getSpan()); - } - } - - $start = $this->scanner->getPosition(); - - switch ($this->plainAtRuleName()) { - case 'debug': - return $this->debugRule($start); - case 'each': - return $this->eachRule($start, $this->functionChildCallable); - case 'else': - $this->disallowedAtRule($start); - case 'error': - return $this->errorRule($start); - case 'for': - return $this->forRule($start, $this->functionChildCallable); - case 'if': - return $this->ifRule($start, $this->functionChildCallable); - case 'return': - return $this->returnRule($start); - case 'warn': - return $this->warnRule($start); - case 'while': - return $this->whileRule($start, $this->functionChildCallable); - default: - $this->disallowedAtRule($start); - } - } - - /** - * Consumes an at-rule's name, with interpolation disallowed. - */ - private function plainAtRuleName(): string - { - $this->scanner->expectChar('@', '@-rule'); - - $name = $this->identifier(); - $this->whitespace(); - - return $name; - } - - /** - * Consumes an `@at-root` rule. - * - * $start should point before the `@`. - */ - private function atRootRule(int $start): AtRootRule - { - if ($this->scanner->peekChar() === '(') { - $query = $this->atRootQuery(); - $this->whitespace(); - - return $this->withChildren($this->statementCallable, $start, function (array $children, FileSpan $span) use ($query) { - return new AtRootRule($children, $span, $query); - }); - } - - if ($this->lookingAtChildren()) { - return $this->withChildren($this->statementCallable, $start, function (array $children, FileSpan $span) { - return new AtRootRule($children, $span); - }); - } - - $child = $this->styleRule(); - - return new AtRootRule([$child], $this->scanner->spanFrom($start)); - } - - /** - * Consumes a query expression of the form `(foo: bar)`. - */ - private function atRootQuery(): Interpolation - { - if ($this->scanner->peekChar() === '#') { - $interpolation = $this->singleInterpolation(); - - return new Interpolation([$interpolation], $interpolation->getSpan()); - } - - $start = $this->scanner->getPosition(); - $buffer = new InterpolationBuffer(); - $this->scanner->expectChar('('); - $buffer->write('('); - $this->whitespace(); - - $buffer->add($this->expression()); - - if ($this->scanner->scanChar(':')) { - $this->whitespace(); - $buffer->write(': '); - $buffer->add($this->expression()); - } - - $this->scanner->expectChar(')'); - $this->whitespace(); - $buffer->write(')'); - - return $buffer->buildInterpolation($this->scanner->spanFrom($start)); - } - - /** - * Consumes a `@content` rule. - * - * $start should point before the `@`. - */ - private function contentRule(int $start): ContentRule - { - if (!$this->inMixin) { - $this->error('@content is only allowed within mixin declarations.', $this->scanner->spanFrom($start)); - } - - $this->whitespace(); - - $arguments = $this->scanner->peekChar() === '(' ? $this->argumentInvocation(true) : ArgumentInvocation::createEmpty($this->scanner->getEmptySpan()); - - $this->expectStatementSeparator('@content rule'); - - return new ContentRule($arguments, $this->scanner->spanFrom($start)); - } - - /** - * Consumes a `@debug` rule. - * - * $start should point before the `@`. - */ - private function debugRule(int $start): DebugRule - { - $value = $this->expression(); - $this->expectStatementSeparator('@debug rule'); - - return new DebugRule($value, $this->scanner->spanFrom($start)); - } - - /** - * Consumes a `@each` rule. - * - * $start should point before the `@`. $child is called to consume any - * children that are specifically allowed in the caller's context. - * - * @param callable(): Statement $child - */ - private function eachRule(int $start, callable $child): EachRule - { - $wasInControlDirective = $this->inControlDirective; - $this->inControlDirective = true; - - $variables = [$this->variableName()]; - $this->whitespace(); - - while ($this->scanner->scanChar(',')) { - $this->whitespace(); - $variables[] = $this->variableName(); - $this->whitespace(); - } - - $this->expectIdentifier('in'); - $this->whitespace(); - - $list = $this->expression(); - - return $this->withChildren($child, $start, function (array $children, FileSpan $span) use ($variables, $wasInControlDirective, $list) { - $this->inControlDirective = $wasInControlDirective; - - return new EachRule($variables, $list, $children, $span); - }); - } - - /** - * Consumes a `@error` rule. - * - * $start should point before the `@`. - */ - private function errorRule(int $start): ErrorRule - { - $value = $this->expression(); - $this->expectStatementSeparator('@error rule'); - - return new ErrorRule($value, $this->scanner->spanFrom($start)); - } - - /** - * Consumes a `@extend` rule. - * - * $start should point before the `@`. - */ - private function extendRule(int $start): ExtendRule - { - if (!$this->inStyleRule && !$this->inMixin && !$this->inContentBlock) { - $this->error('@extend may only be used within style rules.', $this->scanner->spanFrom($start)); - } - - $value = $this->almostAnyValue(); - $optional = $this->scanner->scanChar('!'); - - if ($optional) { - $this->expectIdentifier('optional'); - } - - $this->expectStatementSeparator('@extend rule'); - - return new ExtendRule($value, $this->scanner->spanFrom($start), $optional); - } - - /** - * Consumes a function declaration. - * - * $start should point before the `@`. - */ - private function functionRule(int $start): FunctionRule - { - $precedingComment = $this->lastSilentComment; - $this->lastSilentComment = null; - - $name = $this->identifier(true); - $this->whitespace(); - $arguments = $this->argumentDeclaration(); - - if ($this->inMixin || $this->inContentBlock) { - $this->error('Mixins may not contain function declarations.', $this->scanner->spanFrom($start)); - } - - if ($this->inControlDirective) { - $this->error('Functions may not be declared in control directives.', $this->scanner->spanFrom($start)); - } - - switch (Util::unvendor($name)) { - case 'calc': - case 'element': - case 'expression': - case 'url': - case 'and': - case 'or': - case 'not': - case 'clamp': - $this->error('Invalid function name.', $this->scanner->spanFrom($start)); - } - - $this->whitespace(); - - return $this->withChildren($this->functionChildCallable, $start, function (array $children, FileSpan $span) use ($name, $precedingComment, $arguments) { - return new FunctionRule($name, $arguments, $span, $children, $precedingComment); - }); - } - - /** - * Consumes a `@for` rule. - * - * $start should point before the `@`. $child is called to consume any - * children that are specifically allowed in the caller's context. - * - * @param callable(): Statement $child - */ - private function forRule(int $start, callable $child): ForRule - { - $wasInControlDirective = $this->inControlDirective; - $this->inControlDirective = true; - - $variable = $this->variableName(); - $this->whitespace(); - - $this->expectIdentifier('from'); - $this->whitespace(); - - $exclusive = null; - $from = $this->expression(function () use (&$exclusive) { - if (!$this->lookingAtIdentifier()) { - return false; - } - - if ($this->scanIdentifier('to')) { - $exclusive = true; - - return true; - } - - if ($this->scanIdentifier('through')) { - $exclusive = false; - - return true; - } - - return false; - }); - - if ($exclusive === null) { - $this->scanner->error('Expected "to" or "through".'); - } - - $this->whitespace(); - $to = $this->expression(); - - return $this->withChildren($child, $start, function (array $children, FileSpan $span) use ($variable, $from, $to, $exclusive, $wasInControlDirective) { - $this->inControlDirective = $wasInControlDirective; - - return new ForRule($variable, $from, $to, $children, $span, $exclusive); - }); - } - - /** - * Consumes a `@if` rule. - * - * $start should point before the `@`. $child is called to consume any - * children that are specifically allowed in the caller's context. - * - * @param callable(): Statement $child - */ - private function ifRule(int $start, callable $child): IfRule - { - $ifIndentation = $this->getCurrentIndentation(); - $wasInControlDirective = $this->inControlDirective; - $this->inControlDirective = true; - - $condition = $this->expression(); - $children = $this->children($child); - $this->whitespaceWithoutComments(); - - $clauses = [new IfClause($condition, $children)]; - $lastClause = null; - - while ($this->scanElse($ifIndentation)) { - $this->whitespace(); - - if ($this->scanIdentifier('if')) { - $this->whitespace(); - $clauses[] = new IfClause($this->expression(), $this->children($child)); - } else { - $lastClause = new ElseClause($this->children($child)); - break; - } - } - - $this->inControlDirective = $wasInControlDirective; - $span = $this->scanner->spanFrom($start); - $this->whitespaceWithoutComments(); - - return new IfRule($clauses, $span, $lastClause); - } - - /** - * Consumes an `@import` rule. - * - * $start should point before the `@`. - */ - private function importRule(int $start): ImportRule - { - $imports = []; - - do { - $this->whitespace(); - $argument = $this->importArgument(); - - if (($this->inControlDirective || $this->inMixin) && $argument instanceof DynamicImport) { - $this->disallowedAtRule($start); - } - - $imports[] = $argument; - $this->whitespace(); - } while ($this->scanner->scanChar(',')); - - $this->expectStatementSeparator('@import rule'); - - return new ImportRule($imports, $this->scanner->spanFrom($start)); - } - - /** - * Consumes an argument to an `@import` rule. - */ - protected function importArgument(): Import - { - $start = $this->scanner->getPosition(); - $next = $this->scanner->peekChar(); - - if ($next === 'u' || $next === 'U') { - $url = $this->dynamicUrl(); - $this->whitespace(); - $modifiers = $this->tryImportModifiers(); - - return new StaticImport(new Interpolation([$url], $this->scanner->spanFrom($start)), $this->scanner->spanFrom($start), $modifiers); - } - - $url = $this->string(); - $urlSpan = $this->scanner->spanFrom($start); - $this->whitespace(); - $modifiers = $this->tryImportModifiers(); - - if ($this->isPlainImportUrl($url) || $modifiers !== null) { - return new StaticImport(new Interpolation([$urlSpan->getText()], $urlSpan), $this->scanner->spanFrom($start), $modifiers); - } - - try { - return new DynamicImport($this->parseImportUrl($url), $urlSpan); - } catch (SyntaxError $e) { - $this->error('Invalid URL: ' . $e->getMessage(), $urlSpan, $e); - } - } - - /** - * Parses $url as an import URL. - * - * @throws SyntaxError - */ - protected function parseImportUrl(string $url): string - { - // Backwards-compatibility for implementations that allow absolute Windows - // paths in imports. - if (Path::isWindowsAbsolute($url) && !self::isRootRelativeUrl($url)) { - return (string) Uri::createFromWindowsPath($url); - } - - Uri::createFromString($url); - return $url; - } - - private static function isRootRelativeUrl(string $path): bool - { - return $path !== '' && $path[0] === '/'; - } - - /** - * Returns whether $url indicates that an `@import` is a plain CSS import. - */ - protected function isPlainImportUrl(string $url): bool - { - if (\strlen($url) < 5) { - return false; - } - - if (substr($url, -4) === '.css') { - return true; - } - - if ($url[0] === '/') { - return $url[1] === '/'; - } - - if ($url[0] !== 'h') { - return false; - } - - return 0 === strpos($url, 'http://') || 0 === strpos($url, 'https://'); - } - - /** - * Returns `null` if there are no modifiers. - */ - protected function tryImportModifiers(): ?Interpolation - { - // Exit before allocating anything if we're not looking at any modifiers, as - // is the most common case. - if (!$this->lookingAtInterpolatedIdentifier() && $this->scanner->peekChar() !== '(') { - return null; - } - - $start = $this->scanner->getPosition(); - $buffer = new InterpolationBuffer(); - - while (true) { - if ($this->lookingAtInterpolatedIdentifier()) { - if (!$buffer->isEmpty()) { - $buffer->write(' '); - } - - $identifier = $this->interpolatedIdentifier(); - $buffer->addInterpolation($identifier); - - $name = $identifier->getAsPlain() !== null ? strtolower($identifier->getAsPlain()) : null; - - if ($name !== 'and' && $this->scanner->scanChar('(')) { - if ($name === 'supports') { - $query = $this->importSupportsQuery(); - - if (!$query instanceof SupportsDeclaration) { - $buffer->write('('); - } - - $buffer->add(new SupportsExpression($query)); - - if (!$query instanceof SupportsDeclaration) { - $buffer->write(')'); - } - } else { - $buffer->write('('); - $buffer->addInterpolation($this->interpolatedDeclarationValue(true, true)); - $buffer->write(')'); - } - - $this->scanner->expectChar(')'); - $this->whitespace(); - } else { - $this->whitespace(); - if ($this->scanner->scanChar(',')) { - $buffer->write(', '); - $buffer->addInterpolation($this->mediaQueryList()); - - return $buffer->buildInterpolation($this->scanner->spanFrom($start)); - } - } - } elseif ($this->scanner->peekChar() === '(') { - if (!$buffer->isEmpty()) { - $buffer->write(' '); - } - $buffer->addInterpolation($this->mediaQueryList()); - - return $buffer->buildInterpolation($this->scanner->spanFrom($start)); - } else { - return $buffer->buildInterpolation($this->scanner->spanFrom($start)); - } - } - } - - /** - * Consumes the contents of a `supports()` function after an `@import` rule - * (but not the function name or parentheses). - */ - private function importSupportsQuery(): SupportsCondition - { - if ($this->scanIdentifier('not')) { - $this->whitespace(); - $start = $this->scanner->getPosition(); - - return new SupportsNegation($this->supportsConditionInParens(), $this->scanner->spanFrom($start)); - } - - if ($this->scanner->peekChar() === '(') { - return $this->supportsCondition(); - } - - $function = $this->tryImportSupportsFunction(); - - if ($function !== null) { - return $function; - } - - $start = $this->scanner->getPosition(); - $name = $this->expression(); - $this->scanner->expectChar(':'); - - return $this->supportsDeclarationValue($name, $start); - } - - /** - * Consumes a function call within a `supports()` function after an - * `@import` if available. - */ - private function tryImportSupportsFunction(): ?SupportsCondition - { - if (!$this->lookingAtInterpolatedIdentifier()) { - return null; - } - - $start = $this->scanner->getPosition(); - $name = $this->interpolatedIdentifier(); - assert($name->getAsPlain() !== 'not'); - - if (!$this->scanner->scanChar('(')) { - $this->scanner->setPosition($start); - - return null; - } - - $value = $this->interpolatedDeclarationValue(true, true); - $this->scanner->expectChar(')'); - - return new SupportsFunction($name, $value, $this->scanner->spanFrom($start)); - } - - /** - * Consumes a `@include` rule. - * - * $start should point before the `@`. - */ - private function includeRule(int $start): IncludeRule - { - $namespace = null; - $name = $this->identifier(); - - if ($this->scanner->scanChar('.')) { - $namespace = $name; - $name = $this->publicIdentifier(); - } else { - $name = str_replace('_', '-', $name); - } - - $this->whitespace(); - - $arguments = $this->scanner->peekChar() === '(' ? $this->argumentInvocation(true) : ArgumentInvocation::createEmpty($this->scanner->getEmptySpan()); - $this->whitespace(); - - $contentArguments = null; - if ($this->scanIdentifier('using')) { - $this->whitespace(); - $contentArguments = $this->argumentDeclaration(); - $this->whitespace(); - } - - $content = null; - if ($contentArguments !== null || $this->lookingAtChildren()) { - $contentArguments = $contentArguments ?? ArgumentDeclaration::createEmpty($this->scanner->getEmptySpan()); - $wasInContentBlock = $this->inContentBlock; - $this->inContentBlock = true; - - $content = $this->withChildren($this->statementCallable, $start, function (array $children, FileSpan $span) use ($contentArguments) { - return new ContentBlock($contentArguments, $children, $span); - }); - - $this->inContentBlock = $wasInContentBlock; - } else { - $this->expectStatementSeparator(); - } - - $span = $this->scanner->spanFrom($start, $start)->expand(($content ?? $arguments)->getSpan()); - - // TODO remove this when implementing modules - if ($namespace !== null) { - $this->error('Sass modules are not implemented yet.', $this->scanner->spanFrom($start)); - } - - return new IncludeRule($name, $arguments, $span, $namespace, $content); - } - - /** - * Consumes a `@media` rule. - * - * $start should point before the `@`. - */ - protected function mediaRule(int $start): MediaRule - { - $query = $this->mediaQueryList(); - - return $this->withChildren($this->statementCallable, $start, function (array $children, FileSpan $span) use ($query) { - return new MediaRule($query, $children, $span); - }); - } - - /** - * Consumes a mixin declaration. - * - * $start should point before the `@`. - */ - private function mixinRule(int $start): MixinRule - { - $precedingComment = $this->lastSilentComment; - $this->lastSilentComment = null; - - $name = $this->identifier(true); - $this->whitespace(); - - $arguments = $this->scanner->peekChar() === '(' ? $this->argumentDeclaration() : ArgumentDeclaration::createEmpty($this->scanner->getEmptySpan()); - - if ($this->inMixin || $this->inContentBlock) { - $this->error('Mixins may not contain mixin declarations.', $this->scanner->spanFrom($start)); - } - - if ($this->inControlDirective) { - $this->error('Mixins may not be declared in control directives.', $this->scanner->spanFrom($start)); - } - - $this->whitespace(); - $this->inMixin = true; - - return $this->withChildren($this->statementCallable, $start, function (array $children, FileSpan $span) use ($name, $arguments, $precedingComment) { - $this->inMixin = false; - - return new MixinRule($name, $arguments, $span, $children, $precedingComment); - }); - } - - /** - * Consumes a `@moz-document` rule. - * - * Gecko's `@-moz-document` diverges from [the specification][] allows the - * `url-prefix` and `domain` functions to omit quotation marks, contrary to - * the standard. - * - * [the specification]: https://www.w3.org/TR/css3-conditional/ - */ - protected function mozDocumentRule(int $start, Interpolation $name): AtRule - { - $valueStart = $this->scanner->getPosition(); - $buffer = new InterpolationBuffer(); - $needsDeprecationWarning = false; - - while (true) { - if ($this->scanner->peekChar() === '#') { - $buffer->add($this->singleInterpolation()); - $needsDeprecationWarning = true; - } else { - $identifierStart = $this->scanner->getPosition(); - $identifier = $this->identifier(); - - switch ($identifier) { - case 'url': - case 'url-prefix': - case 'domain': - $contents = $this->tryUrlContents($identifierStart, $identifier); - - if ($contents !== null) { - $buffer->addInterpolation($contents); - } else { - $this->scanner->expectChar('('); - $this->whitespace(); - $argument = $this->interpolatedString(); - $this->scanner->expectChar(')'); - - $buffer->write($identifier); - $buffer->write('('); - $buffer->addInterpolation($argument->asInterpolation()); - $buffer->write(')'); - } - - // A url-prefix with no argument, or with an empty string as an - // argument, is not (yet) deprecated. - $trailing = $buffer->getTrailingString(); - if (!StringUtil::endsWith($trailing, 'url-prefix()') && !StringUtil::endsWith($trailing, "url-prefix('')") && !StringUtil::endsWith($trailing, 'url-prefix("")')) { - $needsDeprecationWarning = true; - } - break; - - case 'regexp': - $buffer->write('regexp('); - $this->scanner->expectChar('('); - $buffer->addInterpolation($this->interpolatedString()->asInterpolation()); - $this->scanner->expectChar(')'); - $buffer->write(')'); - $needsDeprecationWarning = true; - break; - - default: - $this->error('Invalid function name.', $this->scanner->spanFrom($identifierStart)); - } - } - - $this->whitespace(); - - if (!$this->scanner->scanChar(',')) { - break; - } - - $buffer->write(','); - $buffer->write($this->rawText([$this, 'whitespace'])); - } - - $value = $buffer->buildInterpolation($this->scanner->spanFrom($valueStart)); - - return $this->withChildren($this->statementCallable, $start, function (array $children, FileSpan $span) use ($name, $value, $needsDeprecationWarning) { - if ($needsDeprecationWarning) { - $this->logger->warn("@-moz-document is deprecated and support will be removed in Dart Sass 2.0.0.\n\nFor details, see https://sass-lang.com/d/moz-document.", true, $span); - } - - return new AtRule($name, $span, $value, $children); - }); - } - - /** - * Consumes a `@return` rule. - * - * $start should point before the `@`. - */ - private function returnRule(int $start): ReturnRule - { - $value = $this->expression(); - $this->expectStatementSeparator('@return rule'); - - return new ReturnRule($value, $this->scanner->spanFrom($start)); - } - - /** - * Consumes a `@supports` rule. - * - * $start should point before the `@`. - */ - protected function supportsRule(int $start): SupportsRule - { - $condition = $this->supportsCondition(); - $this->whitespace(); - - return $this->withChildren($this->statementCallable, $start, function (array $children, FileSpan $span) use ($condition) { - return new SupportsRule($condition, $children, $span); - }); - } - - /** - * Consumes a `@warn` rule. - * - * $start should point before the `@`. - */ - private function warnRule(int $start): WarnRule - { - $value = $this->expression(); - $this->expectStatementSeparator('@warn rule'); - - return new WarnRule($value, $this->scanner->spanFrom($start)); - } - - /** - * Consumes a `@while` rule. - * - * $start should point before the `@`. $child is called to consume any - * children that are specifically allowed in the caller's context. - * - * @param callable(): Statement $child - */ - private function whileRule(int $start, callable $child): WhileRule - { - $wasInControlDirective = $this->inControlDirective; - $this->inControlDirective = true; - - $condition = $this->expression(); - - return $this->withChildren($child, $start, function (array $children, FileSpan $span) use ($condition, $wasInControlDirective) { - $this->inControlDirective = $wasInControlDirective; - - return new WhileRule($condition, $children, $span); - }); - } - - /** - * Consumes an at-rule that's not explicitly supported by Sass. - * - * $start should point before the `@`. $name is the name of the at-rule. - */ - protected function unknownAtRule(int $start, Interpolation $name): AtRule - { - $wasInUnknownAtRule = $this->inUnknownAtRule; - $this->inUnknownAtRule = true; - - $value = null; - $next = $this->scanner->peekChar(); - if ($next !== '!' && !$this->atEndOfStatement()) { - $value = $this->almostAnyValue(); - } - - if ($this->lookingAtChildren()) { - $rule = $this->withChildren($this->statementCallable, $start, function (array $children, FileSpan $span) use ($name, $value) { - return new AtRule($name, $span, $value, $children); - }); - } else { - $this->expectStatementSeparator(); - $rule = new AtRule($name, $this->scanner->spanFrom($start), $value); - } - - $this->inUnknownAtRule = $wasInUnknownAtRule; - - return $rule; - } - - /** - * Throws an exception indicating that the at-rule starting at $start is - * not allowed in the current context. - * - * @return never-return - */ - private function disallowedAtRule(int $start) - { - $this->almostAnyValue(); - $this->error('This at-rule is not allowed here.', $this->scanner->spanFrom($start)); - } - - /** - * Consumes an argument declaration. - */ - private function argumentDeclaration(): ArgumentDeclaration - { - $start = $this->scanner->getPosition(); - $this->scanner->expectChar('('); - $this->whitespace(); - - $arguments = []; - $named = []; - $restArgument = null; - - while ($this->scanner->peekChar() === '$') { - $variableStart = $this->scanner->getPosition(); - $name = $this->variableName(); - $this->whitespace(); - - $defaultValue = null; - - if ($this->scanner->scanChar(':')) { - $this->whitespace(); - $defaultValue = $this->expressionUntilComma(); - } elseif ($this->scanner->scanChar('.')) { - $this->scanner->expectChar('.'); - $this->scanner->expectChar('.'); - $this->whitespace(); - $restArgument = $name; - break; - } - - $argument = new Argument($name, $this->scanner->spanFrom($variableStart), $defaultValue); - $arguments[] = $argument; - - if (isset($named[$name])) { - $this->error('Duplicate argument.', $argument->getSpan()); - } - $named[$name] = true; - - if (!$this->scanner->scanChar(',')) { - break; - } - $this->whitespace(); - } - - $this->scanner->expectChar(')'); - - return new ArgumentDeclaration($arguments, $this->scanner->spanFrom($start), $restArgument); - } - - /** - * Consumes an argument invocation. - * - * If $mixin is `true`, this is parsed as a mixin invocation. Mixin - * invocations don't allow the Microsoft-style `=` operator at the top level, - * but function invocations do. - * - * If $allowEmptySecondArg is `true`, this allows the second argument to be - * omitted, in which case an unquoted empty string will be passed in its - * place. - */ - private function argumentInvocation(bool $mixin = false, bool $allowEmptySecondArg = false): ArgumentInvocation - { - $start = $this->scanner->getPosition(); - $this->scanner->expectChar('('); - $this->whitespace(); - - $positional = []; - $named = []; - $rest = null; - $keywordRest = null; - - while ($this->lookingAtExpression()) { - $expression = $this->expressionUntilComma(!$mixin); - $this->whitespace(); - - if ($expression instanceof VariableExpression && $this->scanner->scanChar(':')) { - $this->whitespace(); - - if (isset($named[$expression->getName()])) { - $this->error('Duplicate argument.', $expression->getSpan()); - } - - $named[$expression->getName()] = $this->expressionUntilComma(!$mixin); - } elseif ($this->scanner->scanChar('.')) { - $this->scanner->expectChar('.'); - $this->scanner->expectChar('.'); - - if ($rest === null) { - $rest = $expression; - } else { - $keywordRest = $expression; - $this->whitespace(); - break; - } - } elseif ($named) { - $this->error('Positional arguments must come before keyword arguments.', $expression->getSpan()); - } else { - $positional[] = $expression; - } - - $this->whitespace(); - - if (!$this->scanner->scanChar(',')) { - break; - } - $this->whitespace(); - - if ($allowEmptySecondArg && \count($positional) === 1 && \count($named) === 0 && $rest === null && $this->scanner->peekChar() === ')') { - $positional[] = StringExpression::plain('', $this->scanner->getEmptySpan()); - break; - } - } - - $this->scanner->expectChar(')'); - - return new ArgumentInvocation($positional, $named, $this->scanner->spanFrom($start), $rest, $keywordRest); - } - - /** - * Consumes an expression. - * - * @param (callable(): bool)|null $until - * - * @phpstan-impure - */ - private function expression(?callable $until = null, bool $singleEquals = false, bool $bracketList = false): Expression - { - if ($until !== null && $until()) { - $this->scanner->error('Expected expression.'); - } - - $beforeBracket = null; - - if ($bracketList) { - $beforeBracket = $this->scanner->getPosition(); - $this->scanner->expectChar('['); - $this->whitespace(); - - if ($this->scanner->scanChar(']')) { - return new ListExpression([], ListSeparator::UNDECIDED, $this->scanner->spanFrom($beforeBracket), true); - } - } - - $start = $this->scanner->getPosition(); - $wasInParentheses = $this->inParentheses; - /** - * @var Expression[]|null $commaExpressions - */ - $commaExpressions = null; - /** - * @var Expression[]|null $spaceExpressions - */ - $spaceExpressions = null; - /** - * Operators whose right-hand $operands are not fully parsed yet, in order of - * appearance in the document. Because a low-precedence operator will cause - * parsing to finish for all preceding higher-precedence $operators, this is - * naturally ordered from lowest to highest precedence. - * - * @phpstan-var list|null $operators - */ - $operators = null; - /** - * The left-hand sides of $operators. `$operands[n]` is the left-hand side - * of `$operators[n]`. - * - * @var list|null $operands - */ - $operands = null; - - /** - * Whether the single expression parsed so far may be interpreted as - * slash-separated numbers. - */ - $allowSlash = true; - - /** - * The leftmost expression that's been fully-parsed. This can be null in - * special cases where the expression begins with a sub-expression but has - * a later character that indicates that the outer expression isn't done, - * as here: - * - * foo, bar - * ^ - * - * @var Expression|null $singleExpression - */ - $singleExpression = $this->singleExpression(); - - /** - * Resets the scanner state to the state it was at the beginning of the - * expression, except for {@see $inParentheses}. - */ - $resetState = function () use (&$commaExpressions, &$spaceExpressions, &$operators, &$operands, &$allowSlash, &$singleExpression, $start): void { - $commaExpressions = null; - $spaceExpressions = null; - $operators = null; - $operands = null; - $this->scanner->setPosition($start); - $allowSlash = true; - $singleExpression = $this->singleExpression(); - }; - - $resolveOneOperation = function () use (&$operands, &$operators, &$singleExpression, &$allowSlash): void { - assert($operands !== null); - assert($operators !== null); - $operator = array_pop($operators); - assert($operator !== null, 'The list of operators must not be empty'); - - $left = array_pop($operands); - assert($left !== null, 'The list of operands must not be empty'); - - $right = $singleExpression; - - if ($right === null) { - $this->scanner->error('Expected expression.', $this->scanner->getPosition() - \strlen($operator), \strlen($operator)); - } - - if ($allowSlash && !$this->inParentheses && $operator === BinaryOperator::DIVIDED_BY && self::isSlashOperand($left) && self::isSlashOperand($right)) { - $singleExpression = BinaryOperationExpression::slash($left, $right); - } else { - $singleExpression = new BinaryOperationExpression($operator, $left, $right); - $allowSlash = false; - - if ($operator === BinaryOperator::PLUS || $operator === BinaryOperator::MINUS) { - if ( - $this->scanner->substring($right->getSpan()->getStart()->getOffset() - 1, $right->getSpan()->getStart()->getOffset()) === $operator - && Character::isWhitespace($this->scanner->getString()[$left->getSpan()->getEnd()->getOffset()]) - ) { - $message = <<logger->warn($message, true, $singleExpression->getSpan()); - } - } - } - }; - - $resolveOperations = function () use (&$operators, $resolveOneOperation): void { - if ($operators === null) { - return; - } - - while ($operators) { - $resolveOneOperation(); - } - }; - - $addSingleExpression = function (Expression $expression) use (&$singleExpression, &$allowSlash, &$spaceExpressions, $resetState, $resolveOperations): void { - if ($singleExpression !== null) { - // If we discover we're parsing a list whose first element is a division - // operation, and we're in parentheses, reparse outside of a paren - // context. This ensures that `(1/2 1)` doesn't perform division on its - // first element. - if ($this->inParentheses) { - $this->inParentheses = false; - - if ($allowSlash) { - $resetState(); - return; - } - } - - $spaceExpressions = $spaceExpressions ?? []; - $resolveOperations(); - - $spaceExpressions[] = $singleExpression; - $allowSlash = true; - } - - $singleExpression = $expression; - }; - - $addOperator = - /** - * @param BinaryOperator::* $operator - */ - function (string $operator) use (&$allowSlash, &$operators, &$operands, &$singleExpression, $resolveOneOperation): void { - /** @var BinaryOperator::* $operator */ - if ($this->isPlainCss() && $operator !== BinaryOperator::DIVIDED_BY && $operator !== BinaryOperator::SINGLE_EQUALS) { - $this->scanner->error("Operators aren't allowed in plain CSS.", $this->scanner->getPosition() - \strlen($operator), \strlen($operator)); - } - - $allowSlash = $allowSlash && $operator === BinaryOperator::DIVIDED_BY; - - $operators = $operators ?? []; - $operands = $operands ?? []; - - $precedence = BinaryOperator::getPrecedence($operator); - - while ($operators && BinaryOperator::getPrecedence($operators[\count($operators) - 1]) >= $precedence) { - $resolveOneOperation(); - } - - $operators[] = $operator; - - if ($singleExpression === null) { - $this->scanner->error('Expected expression.', $this->scanner->getPosition() - \strlen($operator), \strlen($operator)); - } - - $operands[] = $singleExpression; - - $this->whitespace(); - $singleExpression = $this->singleExpression(); - }; - - $resolveSpaceExpressions = function () use (&$spaceExpressions, &$singleExpression, $resolveOperations): void { - $resolveOperations(); - - if ($spaceExpressions !== null) { - if ($singleExpression === null) { - $this->scanner->error('Expected expression.'); - } - - $spaceExpressions[] = $singleExpression; - $singleExpression = new ListExpression( - $spaceExpressions, - ListSeparator::SPACE, - $spaceExpressions[0]->getSpan()->expand($spaceExpressions[\count($spaceExpressions) - 1]->getSpan()) - ); - $spaceExpressions = null; - } - }; - - while (true) { - $this->whitespace(); - - if ($until !== null && $until()) { - break; - } - - $first = $this->scanner->peekChar(); - - switch ($first) { - case '(': - // Parenthesized numbers can't be slash-separated. - $addSingleExpression($this->parentheses()); - break; - - case '[': - $addSingleExpression($this->expression(null, false, true)); - break; - - case '$': - $addSingleExpression($this->variable()); - break; - - case '&': - $addSingleExpression($this->selector()); - break; - - case "'": - case '"': - $addSingleExpression($this->interpolatedString()); - break; - - case '#': - $addSingleExpression($this->hashExpression()); - break; - - case '=': - $this->scanner->readChar(); - if ($singleEquals && $this->scanner->peekChar() !== '=') { - $addOperator(BinaryOperator::SINGLE_EQUALS); - } else { - $this->scanner->expectChar('='); - $addOperator(BinaryOperator::EQUALS); - } - break; - - case '!': - $next = $this->scanner->peekChar(1); - - if ($next === '=') { - $this->scanner->readChar(); - $this->scanner->readChar(); - $addOperator(BinaryOperator::NOT_EQUALS); - } elseif ($next === null || $next === 'i' || $next === 'I' || Character::isWhitespace($next)) { - $addSingleExpression($this->importantExpression()); - } else { - break 2; - } - break; - - case '<': - $this->scanner->readChar(); - $addOperator($this->scanner->scanChar('=') ? BinaryOperator::LESS_THAN_OR_EQUALS : BinaryOperator::LESS_THAN); - break; - - case '>': - $this->scanner->readChar(); - $addOperator($this->scanner->scanChar('=') ? BinaryOperator::GREATER_THAN_OR_EQUALS : BinaryOperator::GREATER_THAN); - break; - - case '*': - $this->scanner->readChar(); - $addOperator(BinaryOperator::TIMES); - break; - - case '+': - if ($singleExpression === null) { - $addSingleExpression($this->unaryOperation()); - } else { - $this->scanner->readChar(); - $addOperator(BinaryOperator::PLUS); - } - break; - - case '-': - $next = $this->scanner->peekChar(1); - // Make sure `1-2` parses as `1 - 2`, not `1 (-2)`. - if ((Character::isDigit($next) || $next === '.') && ($singleExpression === null || Character::isWhitespace($this->scanner->peekChar(-1)))) { - $addSingleExpression($this->number()); - } elseif ($this->lookingAtInterpolatedIdentifier()) { - $addSingleExpression($this->identifierLike()); - } elseif ($singleExpression === null) { - $addSingleExpression($this->unaryOperation()); - } else { - $this->scanner->readChar(); - $addOperator(BinaryOperator::MINUS); - } - break; - - case '/': - if ($singleExpression === null) { - $addSingleExpression($this->unaryOperation()); - } else { - $this->scanner->readChar(); - $addOperator(BinaryOperator::DIVIDED_BY); - } - break; - - case '%': - $this->scanner->readChar(); - $addOperator(BinaryOperator::MODULO); - break; - - case '0': - case '1': - case '2': - case '3': - case '4': - case '5': - case '6': - case '7': - case '8': - case '9': - $addSingleExpression($this->number()); - break; - - case '.': - if ($this->scanner->peekChar(1) === '.') { - break 2; - } - - $addSingleExpression($this->number()); - break; - - case 'a': - if (!$this->isPlainCss() && $this->scanIdentifier('and')) { - $addOperator(BinaryOperator::AND); - } else { - $addSingleExpression($this->identifierLike()); - } - break; - - case 'o': - if (!$this->isPlainCss() && $this->scanIdentifier('or')) { - $addOperator(BinaryOperator::OR); - } else { - $addSingleExpression($this->identifierLike()); - } - break; - - case 'u': - case 'U': - if ($this->scanner->peekChar(1) === '+') { - $addSingleExpression($this->unicodeRange()); - } else { - $addSingleExpression($this->identifierLike()); - } - break; - - case 'b': - case 'c': - case 'd': - case 'e': - case 'f': - case 'g': - case 'h': - case 'i': - case 'j': - case 'k': - case 'l': - case 'm': - case 'n': - case 'p': - case 'q': - case 'r': - case 's': - case 't': - case 'v': - case 'w': - case 'x': - case 'y': - case 'z': - case 'A': - case 'B': - case 'C': - case 'D': - case 'E': - case 'F': - case 'G': - case 'H': - case 'I': - case 'J': - case 'K': - case 'L': - case 'M': - case 'N': - case 'O': - case 'P': - case 'Q': - case 'R': - case 'S': - case 'T': - case 'V': - case 'W': - case 'X': - case 'Y': - case 'Z': - case '_': - case '\\': - $addSingleExpression($this->identifierLike()); - break; - - case ',': - // If we discover we're parsing a list whose first element is a - // division operation, and we're in parentheses, reparse outside of a - // paren context. This ensures that `(1/2, 1)` doesn't perform division - // on its first element. - if ($this->inParentheses) { - $this->inParentheses = false; - - if ($allowSlash) { - $resetState(); - break; - } - } - - $commaExpressions = $commaExpressions ?? []; - - if ($singleExpression === null) { - $this->scanner->error('Expected expression.'); - } - $resolveSpaceExpressions(); - - $commaExpressions[] = $singleExpression; - - $this->scanner->readChar(); - $allowSlash = true; - $singleExpression = null; - break; - - default: - if ($first !== null && \ord($first) >= 0x80) { - $addSingleExpression($this->identifierLike()); - break; - } - - break 2; - } - } - - if ($bracketList) { - $this->scanner->expectChar(']'); - } - - if ($commaExpressions !== null) { - $resolveSpaceExpressions(); - $this->inParentheses = $wasInParentheses; - - if ($singleExpression !== null) { - $commaExpressions[] = $singleExpression; - } - - return new ListExpression($commaExpressions, ListSeparator::COMMA, $this->scanner->spanFrom($beforeBracket ?? $start), $bracketList); - } - - if ($bracketList && $spaceExpressions !== null) { - $resolveOperations(); - assert($singleExpression !== null); - $spaceExpressions[] = $singleExpression; - - return new ListExpression($spaceExpressions, ListSeparator::SPACE, $this->scanner->spanFrom($beforeBracket), true); - } - - $resolveSpaceExpressions(); - assert($singleExpression !== null); - - if ($bracketList) { - assert($beforeBracket !== null); - $singleExpression = new ListExpression([$singleExpression], ListSeparator::UNDECIDED, $this->scanner->spanFrom($beforeBracket), true); - } - - return $singleExpression; - } - - /** - * Consumes an expression until it reaches a top-level comma. - * - * If $singleEquals is true, this will allow the Microsoft-style `=` - * operator at the top level. - * - * @phpstan-impure - */ - protected function expressionUntilComma(bool $singleEquals = false): Expression - { - return $this->expression(function () { - return $this->scanner->peekChar() === ','; - }, $singleEquals); - } - - /** - * Whether $expression is allowed as an operand of a `/` expression that - * produces a potentially slash-separated number. - */ - private static function isSlashOperand(Expression $expression): bool - { - return $expression instanceof NumberExpression || $expression instanceof CalculationExpression || ($expression instanceof BinaryOperationExpression && $expression->allowsSlash()); - } - - /** - * Consumes an expression that doesn't contain any top-level whitespace. - */ - private function singleExpression(): Expression - { - $first = $this->scanner->peekChar(); - - switch ($first) { - case '(': - return $this->parentheses(); - case '/': - return $this->unaryOperation(); - case '.': - return $this->number(); - case '[': - return $this->expression(null, false, true); - case '$': - return $this->variable(); - case '&': - return $this->selector(); - - case "'": - case '"': - return $this->interpolatedString(); - - case '#': - return $this->hashExpression(); - - case '+': - return $this->plusExpression(); - - case '-': - return $this->minusExpression(); - - case '!': - return $this->importantExpression(); - - case 'u': - case 'U': - if ($this->scanner->peekChar(1) === '+') { - return $this->unicodeRange(); - } - - return $this->identifierLike(); - - case '0': - case '1': - case '2': - case '3': - case '4': - case '5': - case '6': - case '7': - case '8': - case '9': - return $this->number(); - - case 'a': - case 'b': - case 'c': - case 'd': - case 'e': - case 'f': - case 'g': - case 'h': - case 'i': - case 'j': - case 'k': - case 'l': - case 'm': - case 'n': - case 'o': - case 'p': - case 'q': - case 'r': - case 's': - case 't': - case 'v': - case 'w': - case 'x': - case 'y': - case 'z': - case 'A': - case 'B': - case 'C': - case 'D': - case 'E': - case 'F': - case 'G': - case 'H': - case 'I': - case 'J': - case 'K': - case 'L': - case 'M': - case 'N': - case 'O': - case 'P': - case 'Q': - case 'R': - case 'S': - case 'T': - case 'V': - case 'W': - case 'X': - case 'Y': - case 'Z': - case '_': - case '\\': - return $this->identifierLike(); - - default: - if ($first !== null && \ord($first) >= 0x80) { - return $this->identifierLike(); - } - - $this->scanner->error('Expected expression.'); - } - } - - /** - * Consumes a parenthesized expression. - */ - private function parentheses(): Expression - { - if ($this->isPlainCss()) { - $this->scanner->error("Parentheses aren't allowed in plain CSS."); - } - - $wasInParentheses = $this->inParentheses; - $this->inParentheses = true; - - try { - $start = $this->scanner->getPosition(); - $this->scanner->expectChar('('); - $this->whitespace(); - - if (!$this->lookingAtExpression()) { - $this->scanner->expectChar(')'); - - return new ListExpression([], ListSeparator::UNDECIDED, $this->scanner->spanFrom($start)); - } - - $first = $this->expressionUntilComma(); - - if ($this->scanner->scanChar(':')) { - $this->whitespace(); - - return $this->map($first, $start); - } - - if (!$this->scanner->scanChar(',')) { - $this->scanner->expectChar(')'); - - return new ParenthesizedExpression($first, $this->scanner->spanFrom($start)); - } - - $this->whitespace(); - - $expressions = [$first]; - - while (true) { - if (!$this->lookingAtExpression()) { - break; - } - - $expressions[] = $this->expressionUntilComma(); - - if (!$this->scanner->scanChar(',')) { - break; - } - - $this->whitespace(); - } - - $this->scanner->expectChar(')'); - - return new ListExpression($expressions, ListSeparator::COMMA, $this->scanner->spanFrom($start)); - } finally { - $this->inParentheses = $wasInParentheses; - } - } - - /** - * Consumes a map expression. - * - * This expects to be called after the first colon in the map, with $first - * as the expression before the colon and $start the point before the - * opening parenthesis. - */ - private function map(Expression $first, int $start): MapExpression - { - $pairs = [ - [$first, $this->expressionUntilComma()], - ]; - - while ($this->scanner->scanChar(',')) { - $this->whitespace(); - if (!$this->lookingAtExpression()) { - break; - } - - $key = $this->expressionUntilComma(); - $this->scanner->expectChar(':'); - $this->whitespace(); - $value = $this->expressionUntilComma(); - - $pairs[] = [$key, $value]; - } - - $this->scanner->expectChar(')'); - - return new MapExpression($pairs, $this->scanner->spanFrom($start)); - } - - /** - * Consumes an expression that starts with a `#`. - */ - private function hashExpression(): Expression - { - assert($this->scanner->peekChar() === '#'); - if ($this->scanner->peekChar(1) === '{') { - return $this->identifierLike(); - } - - $start = $this->scanner->getPosition(); - $this->scanner->expectChar('#'); - - $first = $this->scanner->peekChar(); - if ($first !== null && Character::isDigit($first)) { - return new ColorExpression($this->hexColorContents($start), $this->scanner->spanFrom($start)); - } - - $afterHash = $this->scanner->getPosition(); - $identifier = $this->interpolatedIdentifier(); - if ($this->isHexColor($identifier)) { - $this->scanner->setPosition($afterHash); - - return new ColorExpression($this->hexColorContents($start), $this->scanner->spanFrom($start)); - } - - $buffer = new InterpolationBuffer(); - $buffer->write('#'); - $buffer->addInterpolation($identifier); - - return new StringExpression($buffer->buildInterpolation($this->scanner->spanFrom($start))); - } - - /** - * Consumes the contents of a hex color, after the `#`. - */ - private function hexColorContents(int $start): SassColor - { - $digit1 = $this->hexDigit(); - $digit2 = $this->hexDigit(); - $digit3 = $this->hexDigit(); - - $alpha = null; - - if (!Character::isHex($this->scanner->peekChar())) { - // #abc - $red = ($digit1 << 4) + $digit1; - $green = ($digit2 << 4) + $digit2; - $blue = ($digit3 << 4) + $digit3; - } else { - $digit4 = $this->hexDigit(); - - if (!Character::isHex($this->scanner->peekChar())) { - #abcd - $red = ($digit1 << 4) + $digit1; - $green = ($digit2 << 4) + $digit2; - $blue = ($digit3 << 4) + $digit3; - $alpha = (($digit4 << 4) + $digit4) / 0xff; - } else { - $red = ($digit1 << 4) + $digit2; - $green = ($digit3 << 4) + $digit4; - $blue = ($this->hexDigit() << 4) + $this->hexDigit(); - - if (Character::isHex($this->scanner->peekChar())) { - $alpha = (($this->hexDigit() << 4) + $this->hexDigit()) / 0xff; - } - } - } - - // Don't emit four- or eight-digit hex colors as hex, since that's not - // yet well-supported in browsers. - return SassColor::rgbInternal($red, $green, $blue, $alpha, $alpha === null ? new SpanColorFormat($this->scanner->spanFrom($start)) : null); - } - - private function isHexColor(Interpolation $interpolation): bool - { - $plain = $interpolation->getAsPlain(); - - if ($plain === null) { - return false; - } - - $length = \strlen($plain); - - if ($length !== 3 && $length !== 4 && $length !== 6 && $length !== 8) { - return false; - } - - for ($i = 0; $i < $length; $i++) { - if (!Character::isHex($plain[$i])) { - return false; - } - } - - return true; - } - - /** - * Consumes a single hexadecimal digit. - * - * @phpstan-impure - */ - private function hexDigit(): int - { - $char = $this->scanner->peekChar(); - - if ($char === null || !Character::isHex($char)) { - $this->scanner->error('Expected hex digit.'); - } - - return (int) hexdec($this->scanner->readChar()); - } - - /** - * Consumes an expression that starts with a `+`. - */ - private function plusExpression(): Expression - { - assert($this->scanner->peekChar() === '+'); - $next = $this->scanner->peekChar(1); - - if (Character::isDigit($next) || $next === '.') { - return $this->number(); - } - - return $this->unaryOperation(); - } - - /** - * Consumes an expression that starts with a `-`. - */ - private function minusExpression(): Expression - { - assert($this->scanner->peekChar() === '-'); - $next = $this->scanner->peekChar(1); - - if (Character::isDigit($next) || $next === '.') { - return $this->number(); - } - - if ($this->lookingAtInterpolatedIdentifier()) { - return $this->identifierLike(); - } - - return $this->unaryOperation(); - } - - /** - * Consumes an `!important` expression. - */ - private function importantExpression(): Expression - { - assert($this->scanner->peekChar() === '!'); - - $start = $this->scanner->getPosition(); - $this->scanner->readChar(); - $this->whitespace(); - $this->expectIdentifier('important'); - - return StringExpression::plain('!important', $this->scanner->spanFrom($start)); - } - - /** - * Consumes a unary operation expression. - */ - private function unaryOperation(): UnaryOperationExpression - { - $start = $this->scanner->getPosition(); - $operator = $this->unaryOperatorFor($this->scanner->readChar()); - - if ($operator === null) { - $this->scanner->error('Expected unary operator.', $this->scanner->getPosition() - 1); - } - - if ($this->isPlainCss() && $operator !== UnaryOperator::DIVIDE) { - $this->scanner->error("Operators aren't allowed in plain CSS.", $this->scanner->getPosition() - 1, 1); - } - - $this->whitespace(); - $operand = $this->singleExpression(); - - return new UnaryOperationExpression($operator, $operand, $this->scanner->spanFrom($start)); - } - - /** - * Returns the unary operator corresponding to $character, or `null` if - * the character is not a unary operator. - * - * @return UnaryOperator::*|null - */ - private function unaryOperatorFor(string $character): ?string - { - switch ($character) { - case '+': - return UnaryOperator::PLUS; - - case '-': - return UnaryOperator::MINUS; - - case '/': - return UnaryOperator::DIVIDE; - - default: - return null; - } - } - - /** - * Consumes a number expression. - */ - private function number(): NumberExpression - { - $start = $this->scanner->getPosition(); - $first = $this->scanner->peekChar(); - - if ($first === '+' || $first === '-') { - $this->scanner->readChar(); - } - - if ($this->scanner->peekChar() !== '.') { - $this->consumeNaturalNumber(); - } - - // Don't complain about a dot after a number unless the number starts with a - // dot. We don't allow a plain ".", but we need to allow "1." so that - // "1..." will work as a rest argument. - $this->tryDecimal($this->scanner->getPosition() !== $start); - $this->tryExponent(); - - // Use PHP's built-in double parsing so that we don't accumulate - // floating-point errors for numbers with lots of digits. - $number = floatval($this->scanner->substring($start)); - - $unit = null; - if ($this->scanner->scanChar('%')) { - $unit = '%'; - } elseif ($this->lookingAtIdentifier() && ($this->scanner->peekChar() !== '-' || $this->scanner->peekChar(1) !== '-')) { - $unit = $this->identifier(false, true); - } - - return new NumberExpression($number, $this->scanner->spanFrom($start), $unit); - } - - /** - * Consumes a natural number (that is, a non-negative integer). - * - * Doesn't support scientific notation. - */ - private function consumeNaturalNumber(): void - { - if (!Character::isDigit($this->scanner->readChar())) { - $this->scanner->error('Expected digit.', $this->scanner->getPosition() - 1); - } - - while (Character::isDigit($this->scanner->peekChar())) { - $this->scanner->readChar(); - } - } - - /** - * Consumes the decimal component of a number if it exists. - * - * If $allowTrailingDot is `false`, this will throw an error if there's a - * dot without any numbers following it. Otherwise, it will ignore the dot - * without consuming it. - */ - private function tryDecimal(bool $allowTrailingDot = false): void - { - if ($this->scanner->peekChar() !== '.') { - return; - } - - if (!Character::isDigit($this->scanner->peekChar(1))) { - if ($allowTrailingDot) { - return; - } - - $this->scanner->error('Expected digit.', $this->scanner->getPosition() + 1); - } - - $this->scanner->readChar(); - while (Character::isDigit($this->scanner->peekChar())) { - $this->scanner->readChar(); - } - } - - /** - * Consumes the exponent component of a number if it exists. - */ - private function tryExponent(): void - { - $first = $this->scanner->peekChar(); - - if ($first !== 'e' && $first !== 'E') { - return; - } - - $next = $this->scanner->peekChar(1); - - if (!Character::isDigit($next) && $next !== '-' && $next !== '+') { - return; - } - - $this->scanner->readChar(); - if ($next === '+' || $next === '-') { - $this->scanner->readChar(); - } - - if (!Character::isDigit($this->scanner->peekChar())) { - $this->scanner->error('Expected digit.'); - } - - while (Character::isDigit($this->scanner->peekChar())) { - $this->scanner->readChar(); - } - } - - /** - * Consumes a unicode range expression. - */ - private function unicodeRange(): StringExpression - { - $start = $this->scanner->getPosition(); - $this->expectIdentChar('u'); - $this->scanner->expectChar('+'); - - $firstRangeLength = 0; - while ($this->scanCharIf([Character::class, 'isHex'])) { - $firstRangeLength++; - } - - $hasQuestionMark = false; - - while ($this->scanner->scanChar('?')) { - $hasQuestionMark = true; - $firstRangeLength++; - } - - if ($firstRangeLength === 0) { - $this->scanner->error('Expected hex digit or "?".'); - } elseif ($firstRangeLength > 6) { - $this->error('Expected at most 6 digits.', $this->scanner->spanFrom($start)); - } elseif ($hasQuestionMark) { - return StringExpression::plain($this->scanner->substring($start), $this->scanner->spanFrom($start)); - } - - if ($this->scanner->scanChar('-')) { - $secondRangeStart = $this->scanner->getPosition(); - $secondRangeLength = 0; - while ($this->scanCharIf([Character::class, 'isHex'])) { - $secondRangeLength++; - } - - if ($secondRangeLength === 0) { - $this->scanner->error('Expected hex digit.'); - } elseif ($secondRangeLength > 6) { - $this->error('Expected at most 6 digits.', $this->scanner->spanFrom($secondRangeStart)); - } - } - - if ($this->lookingAtInterpolatedIdentifierBody()) { - $this->scanner->error('Expected end of identifier.'); - } - - return StringExpression::plain($this->scanner->substring($start), $this->scanner->spanFrom($start)); - } - - /** - * Consumes a variable expression. - */ - private function variable(): VariableExpression - { - $start = $this->scanner->getPosition(); - $name = $this->variableName(); - - if ($this->isPlainCss()) { - $this->error('Sass variables aren\'t allowed in plain CSS.', $this->scanner->spanFrom($start)); - } - - return new VariableExpression($name, $this->scanner->spanFrom($start)); - } - - /** - * Consumes a selector expression. - */ - private function selector(): SelectorExpression - { - if ($this->isPlainCss()) { - $this->scanner->error("The parent selector isn't allowed in plain CSS.", null, 1); - } - - $start = $this->scanner->getPosition(); - $this->scanner->expectChar('&'); - - if ($this->scanner->scanChar('&')) { - $this->warn('In Sass, "&&" means two copies of the parent selector. You probably want to use "and" instead.', $this->scanner->spanFrom($start)); - $this->scanner->setPosition($this->scanner->getPosition() - 1); - } - - return new SelectorExpression($this->scanner->spanFrom($start)); - } - - /** - * Consumes a quoted string expression. - */ - protected function interpolatedString(): StringExpression - { - $start = $this->scanner->getPosition(); - $quote = $this->scanner->readChar(); - - if ($quote !== "'" && $quote !== '"') { - $this->scanner->error('Expected string.', $start); - } - - $buffer = new InterpolationBuffer(); - - while (true) { - $next = $this->scanner->peekChar(); - - if ($next === $quote) { - $this->scanner->readChar(); - break; - } - - if ($next === null || Character::isNewline($next)) { - $this->scanner->error("Expected $quote."); - } - - if ($next === '\\') { - $second = $this->scanner->peekChar(1); - - if (Character::isNewline($second)) { - $this->scanner->readChar(); - $this->scanner->readChar(); - - if ($second === "\r") { - $this->scanner->scanChar("\n"); - } - } else { - $buffer->write($this->escapeCharacter()); - } - } elseif ($next === '#') { - if ($this->scanner->peekChar(1) === '{') { - $buffer->add($this->singleInterpolation()); - } else { - $buffer->write($this->scanner->readChar()); - } - } else { - $buffer->write($this->scanner->readUtf8Char()); - } - } - - return new StringExpression($buffer->buildInterpolation($this->scanner->spanFrom($start)), true); - } - - /** - * Consumes an expression that starts like an identifier. - */ - protected function identifierLike(): Expression - { - $start = $this->scanner->getPosition(); - $identifier = $this->interpolatedIdentifier(); - $plain = $identifier->getAsPlain(); - - if ($plain !== null) { - if ($plain === 'if' && $this->scanner->peekChar() === '(') { - $invocation = $this->argumentInvocation(); - - return new IfExpression($invocation, $identifier->getSpan()->expand($invocation->getSpan())); - } - - if ($plain === 'not') { - $this->whitespace(); - - return new UnaryOperationExpression(UnaryOperator::NOT, $this->singleExpression(), $identifier->getSpan()); - } - - $lower = strtolower($plain); - - if ($this->scanner->peekChar() !== '(') { - switch ($plain) { - case 'false': - return new BooleanExpression(false, $identifier->getSpan()); - case 'null': - return new NullExpression($identifier->getSpan()); - case 'true': - return new BooleanExpression(true, $identifier->getSpan()); - } - - $color = Colors::colorNameToColor($lower); - - if ($color !== null) { - return new ColorExpression( - SassColor::rgbInternal($color->getRed(), $color->getGreen(), $color->getBlue(), $color->getAlpha(), new SpanColorFormat($identifier->getSpan())), - $identifier->getSpan() - ); - } - } - - $specialFunction = $this->trySpecialFunction($lower, $start); - - if ($specialFunction !== null) { - return $specialFunction; - } - } - - switch ($this->scanner->peekChar()) { - case '.': - if ($this->scanner->peekChar(1) === '.') { - return new StringExpression($identifier); - } - - $this->scanner->readChar(); - - if ($plain !== null) { - return $this->namespacedExpression($plain, $start); - } - - $this->error("Interpolation isn't allowed in namespaces.", $identifier->getSpan()); - - case '(': - if ($plain === null) { - return new InterpolatedFunctionExpression($identifier, $this->argumentInvocation(), $this->scanner->spanFrom($start)); - } - - return new FunctionExpression($plain, $this->argumentInvocation(false, $lower === 'var'), $this->scanner->spanFrom($start)); - - default: - return new StringExpression($identifier); - } - } - - /** - * Consumes an expression after a namespace. - * - * This assumes the scanner is positioned immediately after the `.`. The - * $start should refer to the state at the beginning of the namespace. - */ - protected function namespacedExpression(string $namespace, int $start): Expression - { - if ($this->scanner->peekChar() === '$') { - $name = $this->variableName(); - $this->assertPublic($name, function () use ($start) { - return $this->scanner->spanFrom($start); - }); - - // TODO remove this when implementing modules - $this->error('Sass modules are not implemented yet.', $this->scanner->spanFrom($start)); - // return new VariableExpression($name, $this->scanner->spanFrom($start), $plain); - } - - // TODO remove this when implementing modules - $this->publicIdentifier(); - $this->error('Sass modules are not implemented yet.', $this->scanner->spanFrom($start)); - // return new FunctionExpression($this->publicIdentifier(), $this->argumentInvocation(), $this->scanner->spanFrom($start), $plain); - - } - - /** - * If $name is the name of a function with special syntax, consumes it. - * - * Otherwise, returns `null`. $start is the location before the beginning of $name. - */ - protected function trySpecialFunction(string $name, int $start): ?Expression - { - $calculation = $this->scanner->peekChar() === '(' ? $this->tryCalculation($name, $start) : null; - - if ($calculation !== null) { - return $calculation; - } - - $normalized = Util::unvendor($name); - - switch ($normalized) { - case 'calc': - case 'element': - case 'expression': - if (!$this->scanner->scanChar('(')) { - return null; - } - - $buffer = new InterpolationBuffer(); - $buffer->write($name); - $buffer->write('('); - break; - - case 'progid': - if (!$this->scanner->scanChar(':')) { - return null; - } - - $buffer = new InterpolationBuffer(); - $buffer->write($name); - $buffer->write(':'); - - $next = $this->scanner->peekChar(); - - while ($next !== null && (Character::isAlphabetic($next) || $next === '.')) { - $buffer->write($this->scanner->readChar()); - $next = $this->scanner->peekChar(); - } - - $this->scanner->expectChar('('); - $buffer->write('('); - break; - - case 'url': - $contents = $this->tryUrlContents($start); - - if ($contents === null) { - return null; - } - - return new StringExpression($contents); - - default: - return null; - } - - $buffer->addInterpolation($this->interpolatedDeclarationValue(true)); - $this->scanner->expectChar(')'); - $buffer->write(')'); - - return new StringExpression($buffer->buildInterpolation($this->scanner->spanFrom($start))); - } - - /** - * If $name is the name of a calculation expression, parses the - * corresponding calculation and returns it. - * - * Assumes the scanner is positioned immediately before the opening - * parenthesis of the argument list. - */ - private function tryCalculation(string $name, int $start): ?CalculationExpression - { - assert($this->scanner->peekChar() === '('); - - switch ($name) { - case 'calc': - $arguments = $this->calculationArguments(1); - - return new CalculationExpression($name, $arguments, $this->scanner->spanFrom($start)); - - case 'min': - case 'max': - // min() and max() are parsed as calculations if possible, and otherwise - // are parsed as normal Sass functions. - $beforeArguments = $this->scanner->getPosition(); - - try { - $arguments = $this->calculationArguments(); - } catch (FormatException $e) { - $this->scanner->setPosition($beforeArguments); - - return null; - } - - return new CalculationExpression($name, $arguments, $this->scanner->spanFrom($start)); - - case 'clamp': - $arguments = $this->calculationArguments(3); - - return new CalculationExpression($name, $arguments, $this->scanner->spanFrom($start)); - - default: - return null; - } - } - - /** - * Consumes and returns arguments for a calculation expression, including the - * opening and closing parentheses. - * - * If $maxArgs is passed, at most that many arguments are consumed. - * Otherwise, any number greater than zero are consumed. - * - * @param int|null $maxArgs - * - * @return list - * - * @throws FormatException - */ - private function calculationArguments(?int $maxArgs = null): array - { - $this->scanner->expectChar('('); - $interpolation = $this->tryCalculationInterpolation(); - - if ($interpolation !== null) { - $this->scanner->expectChar(')'); - - return [$interpolation]; - } - - $this->whitespace(); - - $arguments = [$this->calculationSum()]; - - while (($maxArgs === null || \count($arguments) < $maxArgs) && $this->scanner->scanChar(',')) { - $this->whitespace(); - $arguments[] = $this->calculationSum(); - } - - $this->scanner->expectChar(')', \count($arguments) === $maxArgs ? '"+", "-", "*", "/", or ")"' : '"+", "-", "*", "/", ",", or ")"'); - - return $arguments; - } - - /** - * Parses a calculation operation or value expression. - */ - private function calculationSum(): Expression - { - $sum = $this->calculationProduct(); - - while (true) { - $next = $this->scanner->peekChar(); - - if ($next === '+' || $next === '-') { - if (!Character::isWhitespace($this->scanner->peekChar(-1)) || !Character::isWhitespace($this->scanner->peekChar(1))) { - $this->scanner->error('"+" and "-" must be surrounded by whitespace in calculations.'); - } - - $this->scanner->readChar(); - $this->whitespace(); - $sum = new BinaryOperationExpression( - $next === '+' ? BinaryOperator::PLUS : BinaryOperator::MINUS, - $sum, - $this->calculationProduct() - ); - } else { - return $sum; - } - } - } - - /** - * Parses a calculation product or value expression. - */ - private function calculationProduct(): Expression - { - $product = $this->calculationValue(); - - while (true) { - $this->whitespace(); - $next = $this->scanner->peekChar(); - - if ($next === '*' || $next === '/') { - $this->scanner->readChar(); - $this->whitespace(); - $product = new BinaryOperationExpression( - $next === '*' ? BinaryOperator::TIMES : BinaryOperator::DIVIDED_BY, - $product, - $this->calculationValue() - ); - } else { - return $product; - } - } - } - - /** - * Parses a single calculation value. - */ - private function calculationValue(): Expression - { - $next = $this->scanner->peekChar(); - - if ($next === '+' || $next === '-' || $next === '.' || Character::isDigit($next)) { - return $this->number(); - } - - if ($next === '$') { - return $this->variable(); - } - - if ($next === '(') { - $start = $this->scanner->getPosition(); - $this->scanner->readChar(); - - $value = $this->tryCalculationInterpolation(); - - if ($value === null) { - $this->whitespace(); - $value = $this->calculationSum(); - } - - $this->whitespace(); - $this->scanner->expectChar(')'); - - return new ParenthesizedExpression($value, $this->scanner->spanFrom($start)); - } - - if (!$this->lookingAtIdentifier()) { - $this->scanner->error('Expected number, variable, function, or calculation.'); - } - - $start = $this->scanner->getPosition(); - $ident = $this->identifier(); - - if ($this->scanner->scanChar('.')) { - return $this->namespacedExpression($ident, $start); - } - - if ($this->scanner->peekChar() !== '(') { - $this->scanner->error('Expected "(" or ".".'); - } - - $lowercase = strtolower($ident); - $calculation = $this->tryCalculation($lowercase, $start); - - if ($calculation !== null) { - return $calculation; - } - - if ($lowercase === 'if') { - return new IfExpression($this->argumentInvocation(), $this->scanner->spanFrom($start)); - } - - return new FunctionExpression($ident, $this->argumentInvocation(), $this->scanner->spanFrom($start)); - } - - /** - * If the following text up to the next unbalanced `")"`, `"]"`, or `"}"` - * contains interpolation, parses that interpolation as an unquoted - * {@see StringExpression} and returns it. - */ - private function tryCalculationInterpolation(): ?StringExpression - { - return $this->containsCalculationInterpolation() ? new StringExpression($this->interpolatedDeclarationValue()) : null; - } - - /** - * Returns whether the following text up to the next unbalanced `")"`, `"]"`, - * or `"}"` contains interpolation. - */ - private function containsCalculationInterpolation(): bool - { - $parens = 0; - $brackets = []; - - $start = $this->scanner->getPosition(); - while (!$this->scanner->isDone()) { - $next = $this->scanner->peekChar(); - - switch ($next) { - case '\\': - $this->scanner->readChar(); - $this->scanner->readUtf8Char(); - break; - - case '/': - if (!$this->scanComment()) { - $this->scanner->readChar(); - } - break; - - case "'": - case '"': - $this->interpolatedString(); - break; - - case '#': - if ($parens === 0 && $this->scanner->peekChar(1) === '{') { - $this->scanner->setPosition($start); - return true; - } - $this->scanner->readChar(); - break; - - case '(': - $parens++; - // fallthrough - case '{': - case '[': - assert($next !== null); // https://github.com/phpstan/phpstan/issues/5678 - $brackets[] = Character::opposite($next); - $this->scanner->readChar(); - break; - - case ')': - $parens--; - // fallthrough - case '}': - case ']': - if (empty($brackets) || array_pop($brackets) !== $next) { - $this->scanner->setPosition($start); - return false; - } - $this->scanner->readChar(); - break; - - default: - $this->scanner->readUtf8Char(); - } - } - - $this->scanner->setPosition($start); - - return false; - } - - private function tryUrlContents(int $start, ?string $name = null): ?Interpolation - { - $beginningOfContents = $this->scanner->getPosition(); - - if (!$this->scanner->scanChar('(')) { - return null; - } - $this->whitespaceWithoutComments(); - - $buffer = new InterpolationBuffer(); - $buffer->write($name ?? 'url'); - $buffer->write('('); - - while (true) { - $next = $this->scanner->peekChar(); - - if ($next === null) { - break; - } - - if ($next === '\\') { - $buffer->write($this->escape()); - } elseif ($next === '!' || $next === '%' || $next === '&' || (\ord($next) >= \ord('*') && \ord($next) <= \ord('~')) || \ord($next) >= 0x80) { - $buffer->write($this->scanner->readUtf8Char()); - } elseif ($next === '#') { - if ($this->scanner->peekChar(1) === '{') { - $buffer->add($this->singleInterpolation()); - } else { - $buffer->write($this->scanner->readChar()); - } - } elseif (Character::isWhitespace($next)) { - $this->whitespaceWithoutComments(); - - if ($this->scanner->peekChar() !== ')') { - break; - } - } elseif ($next === ')') { - $buffer->write($this->scanner->readChar()); - - return $buffer->buildInterpolation($this->scanner->spanFrom($start)); - } else { - break; - } - } - - $this->scanner->setPosition($beginningOfContents); - - return null; - } - - /** - * Consumes a `url` token that's allowed to contain SassScript. - */ - protected function dynamicUrl(): Expression - { - $start = $this->scanner->getPosition(); - $this->expectIdentifier('url'); - - $contents = $this->tryUrlContents($start); - - if ($contents !== null) { - return new StringExpression($contents); - } - - return new InterpolatedFunctionExpression(new Interpolation(['url'], $this->scanner->spanFrom($start)), $this->argumentInvocation(), $this->scanner->spanFrom($start)); - } - - /** - * Consumes tokens up to "{", "}", ";", or "!". - * - * This respects string and comment boundaries and supports interpolation. - * Once this interpolation is evaluated, it's expected to be re-parsed. - * - * If $omitComments is true, comments will still be consumed, but they will - * not be included in the returned interpolation. - * - * Differences from {@see interpolatedDeclarationValue} include: - * - * - This does not balance brackets. - * - This does not interpret backslashes, since the text is expected to be - * re-parsed. - * - This supports Sass-style single-line comments. - * - This does not compress adjacent whitespace characters. - */ - protected function almostAnyValue(bool $omitComments = false): Interpolation - { - $start = $this->scanner->getPosition(); - $buffer = new InterpolationBuffer(); - - while (true) { - $next = $this->scanner->peekChar(); - - switch ($next) { - case '\\': - // Write a literal backslash because this text will be re-parsed. - $buffer->write($this->scanner->readChar()); - $buffer->write($this->scanner->readUtf8Char()); - break; - - case '"': - case "'": - $buffer->addInterpolation($this->interpolatedString()->asInterpolation()); - break; - - case '/': - $commentStart = $this->scanner->getPosition(); - - if ($this->scanComment()) { - if (!$omitComments) { - $buffer->write($this->scanner->substring($commentStart)); - } - } else { - $buffer->write($this->scanner->readChar()); - } - break; - - case '#': - if ($this->scanner->peekChar(1) === '{') { - // Add a full interpolated identifier to handle cases like - // "#{...}--1", since "--1" isn't a valid identifier on its own. - $buffer->addInterpolation($this->interpolatedIdentifier()); - } else { - $buffer->write($this->scanner->readChar()); - } - break; - - case "\r": - case "\n": - case "\f": - if ($this->isIndented()) { - break 2; - } - $buffer->write($this->scanner->readChar()); - break; - - case '!': - case ';': - case '{': - case '}': - break 2; - - case 'u': - case 'U': - $beforeUrl = $this->scanner->getPosition(); - - if (!$this->scanIdentifier('url')) { - $buffer->write($this->scanner->readChar()); - break; - } - - $contents = $this->tryUrlContents($beforeUrl); - - if ($contents === null) { - $this->scanner->setPosition($beforeUrl); - $buffer->write($this->scanner->readChar()); - } else { - $buffer->addInterpolation($contents); - } - break; - - default: - if ($next === null) { - break 2; - } - - if ($this->lookingAtIdentifier()) { - $buffer->write($this->identifier()); - } else { - $buffer->write($this->scanner->readUtf8Char()); - } - break; - } - } - - return $buffer->buildInterpolation($this->scanner->spanFrom($start)); - } - - /** - * Consumes tokens until it reaches a top-level `";"`, `")"`, `"]"`, - * or `"}"` and returns their contents as a string. - * - * If $allowEmpty is `false` (the default), this requires at least one token. - * - * If $allowSemicolon is `true`, this doesn't stop at semicolons and instead - * includes them in the interpolated output. - * - * If $allowColon is `false`, this stops at top-level colons. - * - * Unlike {@see declarationValue}, this allows interpolation. - */ - private function interpolatedDeclarationValue(bool $allowEmpty = false, bool $allowSemicolon = false, bool $allowColon = true): Interpolation - { - $start = $this->scanner->getPosition(); - $buffer = new InterpolationBuffer(); - $brackets = []; - $wroteNewline = false; - - while (true) { - $next = $this->scanner->peekChar(); - - if ($next === null) { - break; - } - - switch ($next) { - case '\\': - $buffer->write($this->escape(true)); - $wroteNewline = false; - break; - - case '"': - case "'": - $buffer->addInterpolation($this->interpolatedString()->asInterpolation()); - $wroteNewline = false; - break; - - case '/': - if ($this->scanner->peekChar(1) === '*') { - $buffer->write($this->rawText([$this, 'loudComment'])); - } else { - $buffer->write($this->scanner->readChar()); - } - $wroteNewline = false; - break; - - case '#': - if ($this->scanner->peekChar(1) === '{') { - // Add a full interpolated identifier to handle cases like - // "#{...}--1", since "--1" isn't a valid identifier on its own. - $buffer->addInterpolation($this->interpolatedIdentifier()); - } else { - $buffer->write($this->scanner->readChar()); - } - $wroteNewline = false; - break; - - case ' ': - case "\t": - $second = $this->scanner->peekChar(1); - if ($wroteNewline || $second === null || !Character::isWhitespace($second)) { - $buffer->write($this->scanner->readChar()); - } else { - $this->scanner->readChar(); - } - break; - - case "\n": - case "\r": - case "\f": - if ($this->isIndented()) { - break 2; - } - $prev = $this->scanner->peekChar(-1); - if ($prev === null || !Character::isNewline($prev)) { - $buffer->write("\n"); - } - $this->scanner->readChar(); - $wroteNewline = true; - break; - - case '(': - case '{': - case '[': - $buffer->write($next); - $brackets[] = Character::opposite($this->scanner->readChar()); - $wroteNewline = false; - break; - - case ')': - case '}': - case ']': - if (empty($brackets)) { - break 2; - } - - $buffer->write($next); - $this->scanner->expectChar(array_pop($brackets)); - $wroteNewline = false; - break; - - case ';': - if (!$allowSemicolon && empty($brackets)) { - break 2; - } - - $buffer->write($this->scanner->readChar()); - $wroteNewline = false; - break; - - case ':': - if (!$allowColon && empty($brackets)) { - break 2; - } - - $buffer->write($this->scanner->readChar()); - $wroteNewline = false; - break; - - case 'u': - case 'U': - $beforeUrl = $this->scanner->getPosition(); - - if (!$this->scanIdentifier('url')) { - $buffer->write($this->scanner->readChar()); - $wroteNewline = false; - break; - - } - - $contents = $this->tryUrlContents($beforeUrl); - - if ($contents === null) { - $this->scanner->setPosition($beforeUrl); - $buffer->write($this->scanner->readChar()); - } else { - $buffer->addInterpolation($contents); - } - - $wroteNewline = false; - break; - - default: - if ($this->lookingAtIdentifier()) { - $buffer->write($this->identifier()); - } else { - $buffer->write($this->scanner->readUtf8Char()); - } - $wroteNewline = false; - break; - } - } - - if (!empty($brackets)) { - $this->scanner->expectChar(array_pop($brackets)); - } - - if (!$allowEmpty && $buffer->isEmpty()) { - $this->scanner->error('Expected token.'); - } - - return $buffer->buildInterpolation($this->scanner->spanFrom($start)); - } - - /** - * Consumes an identifier that may contain interpolation. - */ - protected function interpolatedIdentifier(): Interpolation - { - $start = $this->scanner->getPosition(); - $buffer = new InterpolationBuffer(); - - if ($this->scanner->scanChar('-')) { - $buffer->write('-'); - - if ($this->scanner->scanChar('-')) { - $buffer->write('-'); - $this->interpolatedIdentifierBody($buffer); - - return $buffer->buildInterpolation($this->scanner->spanFrom($start)); - } - } - - $first = $this->scanner->peekChar(); - - if ($first === null) { - $this->scanner->error('Expected identifier.'); - } - - if (Character::isNameStart($first)) { - $buffer->write($this->scanner->readUtf8Char()); - } elseif ($first === '\\') { - $buffer->write($this->escape(true)); - } elseif ($first === '#' && $this->scanner->peekChar(1) === '{') { - $buffer->add($this->singleInterpolation()); - } else { - $this->scanner->error('Expected identifier.'); - } - - $this->interpolatedIdentifierBody($buffer); - - return $buffer->buildInterpolation($this->scanner->spanFrom($start)); - } - - /** - * Consumes a chunk of a possibly-interpolated CSS identifier after the name - * start, and adds the contents to the $buffer buffer. - */ - private function interpolatedIdentifierBody(InterpolationBuffer $buffer): void - { - while (true) { - $next = $this->scanner->peekChar(); - - if ($next === null) { - break; - } - - if ($next === '_' || $next === '-' || Character::isAlphanumeric($next) || \ord($next) >= 0x80) { - $buffer->write($this->scanner->readUtf8Char()); - } elseif ($next === '\\') { - $buffer->write($this->escape()); - } elseif ($next === '#' && $this->scanner->peekChar(1) === '{') { - $buffer->add($this->singleInterpolation()); - } else { - break; - } - } - } - - /** - * Consumes interpolation. - */ - protected function singleInterpolation(): Expression - { - $start = $this->scanner->getPosition(); - - $this->scanner->expect('#{'); - - $this->whitespace(); - - $contents = $this->expression(); - - $this->scanner->expectChar('}'); - - if ($this->isPlainCss()) { - $this->error('Interpolation isn\'t allowed in plain CSS.', $this->scanner->spanFrom($start)); - } - - return $contents; - } - - /** - * Consumes a list of media queries. - */ - private function mediaQueryList(): Interpolation - { - $start = $this->scanner->getPosition(); - $buffer = new InterpolationBuffer(); - - while (true) { - $this->whitespace(); - $this->mediaQuery($buffer); - $this->whitespace(); - - if (!$this->scanner->scanChar(',')) { - break; - } - - $buffer->write(', '); - } - - return $buffer->buildInterpolation($this->scanner->spanFrom($start)); - } - - /** - * Consumes a single media query. - */ - private function mediaQuery(InterpolationBuffer $buffer): void - { - if ($this->scanner->peekChar() === '(') { - $this->mediaInParens($buffer); - $this->whitespace(); - - if ($this->scanIdentifier('and')) { - $buffer->write(' and '); - $this->expectWhitespace(); - $this->mediaLogicSequence($buffer, 'and'); - } elseif ($this->scanIdentifier('or')) { - $buffer->write(' or '); - $this->expectWhitespace(); - $this->mediaLogicSequence($buffer, 'or'); - } - - return; - } - - $identifier1 = $this->interpolatedIdentifier(); - - if (StringUtil::equalsIgnoreCase($identifier1->getAsPlain(), 'not')) { - // For example, "@media not (...) {" - $this->expectWhitespace(); - - if (!$this->lookingAtInterpolatedIdentifier()) { - $buffer->write('not '); - $this->mediaOrInterp($buffer); - - return; - } - } - - $this->whitespace(); - $buffer->addInterpolation($identifier1); - - if (!$this->lookingAtInterpolatedIdentifier()) { - // For example, "@media screen {". - return; - } - - $buffer->write(' '); - - $identifier2 = $this->interpolatedIdentifier(); - - if (StringUtil::equalsIgnoreCase($identifier2->getAsPlain(), 'and')) { - $this->expectWhitespace(); - // For example, "@media screen and ..." - $buffer->write(' and '); - } else { - $this->whitespace(); - $buffer->addInterpolation($identifier2); - - if ($this->scanIdentifier('and')) { - // For example, "@media only screen and ..." - $this->expectWhitespace(); - $buffer->write(' and '); - } else { - // For example, "@media only screen {" - return; - } - } - - // We've consumed either `IDENTIFIER "and"` or - // `IDENTIFIER IDENTIFIER "and"`. - - if ($this->scanIdentifier('not')) { - // For example, "@media screen and not (...) {" - $this->expectWhitespace(); - $buffer->write('not '); - $this->mediaOrInterp($buffer); - return; - } - - $this->mediaLogicSequence($buffer, 'and'); - } - - /** - * Consumes one or more `MediaOrInterp` expressions separated by $operator - * and writes them to $buffer. - */ - private function mediaLogicSequence(InterpolationBuffer $buffer, string $operator): void - { - while (true) { - $this->mediaOrInterp($buffer); - $this->whitespace(); - - if (!$this->scanIdentifier($operator)) { - return; - } - $this->expectWhitespace(); - - $buffer->write(' '); - $buffer->write($operator); - $buffer->write(' '); - } - } - - /** - * Consumes a `MediaOrInterp` expression and writes it to $buffer. - */ - private function mediaOrInterp(InterpolationBuffer $buffer): void - { - if ($this->scanner->peekChar() === '#') { - $interpolation = $this->singleInterpolation(); - - $buffer->addInterpolation(new Interpolation([$interpolation], $interpolation->getSpan())); - } else { - $this->mediaInParens($buffer); - } - } - - /** - * Consumes a `MediaInParens` expression and writes it to $buffer. - */ - private function mediaInParens(InterpolationBuffer $buffer): void - { - $this->scanner->expectChar('(', 'media condition in parentheses'); - $buffer->write('('); - $this->whitespace(); - - $needsParenDeprecation = $this->scanner->peekChar() === '('; - $needsNotDeprecation = $this->matchesIdentifier('not'); - $expression = $this->expressionUntilComparison(); - - if ($needsParenDeprecation || $needsNotDeprecation) { - $this->logger->warn(sprintf( - "Starting a @media query with \"%s\" is deprecated because it conflicts with official CSS syntax.\n\nTo preserve existing behavior: #{%s}\nTo migrate to new behavior: #{\"%s\"}\n\nFor details, see https://sass-lang.com/d/media-logic", - $needsParenDeprecation ? '(' : 'not', - $expression, - $expression - ), true, $expression->getSpan()); - } - - $buffer->add($expression); - - if ($this->scanner->scanChar(':')) { - $this->whitespace(); - $buffer->write(': '); - $buffer->add($this->expression()); - } else { - $next = $this->scanner->peekChar(); - - if ($next === '<' || $next === '>' || $next === '=') { - $buffer->write(' '); - $buffer->write($this->scanner->readChar()); - if (($next === '<' || $next === '>') && $this->scanner->scanChar('=')) { - $buffer->write('='); - } - $buffer->write(' '); - - $this->whitespace(); - $buffer->add($this->expressionUntilComparison()); - - if (($next === '<' || $next === '>') && $this->scanner->scanChar($next)) { - $buffer->write(' '); - $buffer->write($next); - if ($this->scanner->scanChar('=')) { - $buffer->write('='); - } - $buffer->write(' '); - - $this->whitespace(); - $buffer->add($this->expressionUntilComparison()); - } - } - } - - $this->scanner->expectChar(')'); - $this->whitespace(); - $buffer->write(')'); - } - - /** - * Consumes an expression until it reaches a top-level `<`, `>`, or a `=` - * that's not `==`. - */ - private function expressionUntilComparison(): Expression - { - return $this->expression(function () { - $next = $this->scanner->peekChar(); - - if ($next === '=') { - return $this->scanner->peekChar(1) !== '='; - } - - return $next === '<' || $next === '>'; - }); - } - - /** - * Consumes a `@supports` condition. - */ - private function supportsCondition(): SupportsCondition - { - $start = $this->scanner->getPosition(); - - if ($this->scanIdentifier('not')) { - $this->whitespace(); - - return new SupportsNegation($this->supportsConditionInParens(), $this->scanner->spanFrom($start)); - } - - $condition = $this->supportsConditionInParens(); - $this->whitespace(); - $operator = null; - - while ($this->lookingAtIdentifier()) { - if ($operator !== null) { - $this->expectIdentifier($operator); - } elseif ($this->scanIdentifier('or')) { - $operator = 'or'; - } else { - $this->expectIdentifier('and'); - $operator = 'and'; - } - - $this->whitespace(); - $right = $this->supportsConditionInParens(); - - $condition = new SupportsOperation($condition, $right, $operator, $this->scanner->spanFrom($start)); - $this->whitespace(); - } - - return $condition; - } - - /** - * Consumes a parenthesized supports condition, or an interpolation. - */ - private function supportsConditionInParens(): SupportsCondition - { - $start = $this->scanner->getPosition(); - - if ($this->lookingAtInterpolatedIdentifier()) { - $identifier = $this->interpolatedIdentifier(); - - if ($identifier->getAsPlain() !== null && strtolower($identifier->getAsPlain()) === 'not') { - $this->error('"not" is not a valid identifier here.', $identifier->getSpan()); - } - - if ($this->scanner->scanChar('(')) { - $arguments = $this->interpolatedDeclarationValue(true, true); - $this->scanner->expectChar(')'); - - return new SupportsFunction($identifier, $arguments, $this->scanner->spanFrom($start)); - } - - if (\count($identifier->getContents()) !== 1 || !$identifier->getContents()[0] instanceof Expression) { - $this->error('Expected @supports condition.', $identifier->getSpan()); - } else { - return new SupportsInterpolation($identifier->getContents()[0], $identifier->getSpan()); - } - } - - $this->scanner->expectChar('('); - $this->whitespace(); - - if ($this->scanIdentifier('not')) { - $this->whitespace(); - $condition = $this->supportsConditionInParens(); - $this->scanner->expectChar(')'); - - return new SupportsNegation($condition, $this->scanner->spanFrom($start)); - } - - if ($this->scanner->peekChar() === '(') { - $condition = $this->supportsCondition(); - $this->scanner->expectChar(')'); - - return $condition; - } - - // Unfortunately, we may have to backtrack here. The grammar is: - // - // Expression ":" Expression - // | InterpolatedIdentifier InterpolatedAnyValue? - // - // These aren't ambiguous because this `InterpolatedAnyValue` is forbidden - // from containing a top-level colon, but we still have to parse the full - // expression to figure out if there's a colon after it. - // - // We could avoid the overhead of a full expression parse by looking ahead - // for a colon (outside of balanced brackets), but in practice we expect the - // vast majority of real uses to be `Expression ":" Expression`, so it makes - // sense to parse that case faster in exchange for less code complexity and - // a slower backtracking case. - - $nameStart = $this->scanner->getPosition(); - $wasInParentheses = $this->inParentheses; - - try { - $name = $this->expression(); - $this->scanner->expectChar(':'); - } catch (FormatException $e) { - $this->scanner->setPosition($nameStart); - $this->inParentheses = $wasInParentheses; - - $identifier = $this->interpolatedIdentifier(); - $operation = $this->trySupportsOperation($identifier, $nameStart); - - if ($operation !== null) { - $this->scanner->expectChar(')'); - - return $operation; - } - - // If parsing an expression fails, try to parse an - // `InterpolatedAnyValue` instead. But if that value runs into a - // top-level colon, then this is probably intended to be a declaration - // after all, so we rethrow the declaration-parsing error. - $buffer = new InterpolationBuffer(); - $buffer->addInterpolation($identifier); - $buffer->addInterpolation($this->interpolatedDeclarationValue(true, true, false)); - - $contents = $buffer->buildInterpolation($this->scanner->spanFrom($nameStart)); - - if ($this->scanner->peekChar() === ':') { - throw $e; - } - - $this->scanner->expectChar(')'); - - return new SupportsAnything($contents, $this->scanner->spanFrom($start)); - } - - $declaration = $this->supportsDeclarationValue($name, $start); - $this->scanner->expectChar(')'); - - return $declaration; - } - - private function supportsDeclarationValue(Expression $name, int $start): SupportsDeclaration - { - if ($name instanceof StringExpression && !$name->hasQuotes() && StringUtil::startsWith($name->getText()->getInitialPlain(), '--')) { - $value = new StringExpression($this->interpolatedDeclarationValue()); - } else { - $this->whitespace(); - $value = $this->expression(); - } - - return new SupportsDeclaration($name, $value, $this->scanner->spanFrom($start)); - } - - /** - * If $interpolation is followed by `"and"` or `"or"`, parse it as a supports operation. - * - * Otherwise, return `null` without moving the scanner position. - */ - private function trySupportsOperation(Interpolation $interpolation, int $start): ?SupportsOperation - { - if (\count($interpolation->getContents()) !== 1) { - return null; - } - - $expression = $interpolation->getContents()[0]; - - if (!$expression instanceof Expression) { - return null; - } - - $beforeWhitespace = $this->scanner->getPosition(); - $this->whitespace(); - - $operation = null; - $operator = null; - - while ($this->lookingAtIdentifier()) { - if ($operator !== null) { - $this->expectIdentifier($operator); - } elseif ($this->scanIdentifier('and')) { - $operator = 'and'; - } elseif ($this->scanIdentifier('or')) { - $operator = 'or'; - } else { - $this->scanner->setPosition($beforeWhitespace); - - return null; - } - - $this->whitespace(); - $right = $this->supportsConditionInParens(); - - $operation = new SupportsOperation($operation ?? new SupportsInterpolation($expression, $interpolation->getSpan()), $right, $operator, $this->scanner->spanFrom($start)); - $this->whitespace(); - } - - return $operation; - } - - /** - * Returns whether the scanner is immediately before an identifier that may - * contain interpolation. - * - * This is based on [the CSS algorithm][], but it assumes all backslashes - * start escapes and it considers interpolation to be valid in an identifier. - * - * [the CSS algorithm]: https://drafts.csswg.org/css-syntax-3/#would-start-an-identifier - */ - private function lookingAtInterpolatedIdentifier(): bool - { - $first = $this->scanner->peekChar(); - - if ($first === null) { - return false; - } - - if ($first === '\\' || Character::isNameStart($first)) { - return true; - } - - if ($first === '#' && $this->scanner->peekChar(1) === '{') { - return true; - } - - if ($first !== '-') { - return false; - } - - $second = $this->scanner->peekChar(1); - - if ($second === null) { - return false; - } - - if ($second === '#') { - return $this->scanner->peekChar(2) === '{'; - } - - return $second === '\\' || $second === '-' || Character::isNameStart($second); - } - - /** - * Returns whether the scanner is immediately before a sequence of characters - * that could be part of an CSS identifier body. - * - * The identifier body may include interpolation. - */ - private function lookingAtInterpolatedIdentifierBody(): bool - { - $first = $this->scanner->peekChar(); - - if ($first === null) { - return false; - } - - if ($first === '\\' || Character::isName($first)) { - return true; - } - - return $first === '#' && $this->scanner->peekChar(1) === '{'; - } - - /** - * Returns whether the scanner is immediately before a SassScript expression. - */ - private function lookingAtExpression(): bool - { - $character = $this->scanner->peekChar(); - - if ($character === null) { - return false; - } - - if ($character === '.') { - return $this->scanner->peekChar(1) !== '.'; - } - - if ($character === '!') { - $next = $this->scanner->peekChar(1); - - return $next === null || $next === 'i' || $next === 'I' || Character::isWhitespace($next); - } - - return $character === '(' || - $character === '/' || - $character === '[' || - $character === "'" || - $character === '"' || - $character === '#' || - $character === '+' || - $character === '-' || - $character === '\\' || - $character === '$' || - $character === '&' || - Character::isNameStart($character) || - Character::isDigit($character); - } - - /** - * Consumes a block of $child statements and passes them, as well as the - * span from $start to the end of the child block, to $create. - * - * @template T - * @param callable(): Statement $child - * @param callable(Statement[], FileSpan): T $create - * @return T - */ - private function withChildren(callable $child, int $start, callable $create) - { - $children = $this->children($child); - $result = $create($children, $this->scanner->spanFrom($start)); - $this->whitespaceWithoutComments(); - - return $result; - } - - /** - * Like {@see identifier}, but rejects identifiers that begin with `_` or `-`. - */ - private function publicIdentifier(): string - { - $start = $this->scanner->getPosition(); - $result = $this->identifier(true); - $this->assertPublic($result, function () use ($start) { - return $this->scanner->spanFrom($start); - }); - - return $result; - } - - /** - * Throws an error if $identifier isn't public. - * - * Calls $span to provide the span for an error if one occurs. - * - * @param callable(): FileSpan $span - */ - private function assertPublic(string $identifier, callable $span): void - { - if (!Character::isPrivate($identifier)) { - return; - } - - $this->error("Private members can't be accessed from outside their modules.", $span()); - } - - /** - * Whether this is parsing the indented syntax. - */ - abstract protected function isIndented(): bool; - - /** - * Whether this is a plain CSS stylesheet. - */ - protected function isPlainCss(): bool - { - return false; - } - - /** - * The indentation level at the current scanner position. - * - * This value isn't used directly by StylesheetParser; it's just passed to - * {@see scanElse}. - */ - abstract protected function getCurrentIndentation(): int; - - /** - * Parses and returns a selector used in a style rule. - */ - abstract protected function styleRuleSelector(): Interpolation; - - /** - * Asserts that the scanner is positioned before a statement separator, or at - * the end of a list of statements. - * - * If the name of the parent rule is passed, it's used for error reporting. - * - * This consumes whitespace, but nothing else, including comments. - * - * @throws FormatException - */ - abstract protected function expectStatementSeparator(?string $name = null): void; - - /** - * Whether the scanner is positioned at the end of a statement. - */ - abstract protected function atEndOfStatement(): bool; - - /** - * Whether the scanner is positioned before a block of children that can be - * parsed with {@see children}. - */ - abstract protected function lookingAtChildren(): bool; - - /** - * Tries to scan an `@else` rule after an `@if` block, and returns whether that succeeded. - * - * This should just scan the rule name, not anything afterwards. - * $ifIndentation is the result of {@see getCurrentIndentation} from before the - * corresponding `@if` was parsed. - */ - abstract protected function scanElse(int $ifIndentation): bool; - - /** - * Consumes a block of child statements. - * - * Unlike most production consumers, this does *not* consume trailing - * whitespace. This is necessary to ensure that the source span for the - * parent rule doesn't cover whitespace after the rule. - * - * @param callable(): Statement $child - * - * @return Statement[] - */ - abstract protected function children(callable $child): array; - - /** - * Consumes top-level statements. - * - * The $statement callback may return `null`, indicating that a statement - * was consumed that shouldn't be added to the AST. - * - * @param callable(): ?Statement $statement - * - * @return Statement[] - */ - abstract protected function statements(callable $statement): array; -} diff --git a/scssphp/src/Serializer/SerializeResult.php b/scssphp/src/Serializer/SerializeResult.php deleted file mode 100644 index 3d570de..0000000 --- a/scssphp/src/Serializer/SerializeResult.php +++ /dev/null @@ -1,37 +0,0 @@ -css = $css; - } - - public function getCss(): string - { - return $this->css; - } -} diff --git a/scssphp/src/Serializer/SerializeVisitor.php b/scssphp/src/Serializer/SerializeVisitor.php deleted file mode 100644 index 8365ea7..0000000 --- a/scssphp/src/Serializer/SerializeVisitor.php +++ /dev/null @@ -1,1591 +0,0 @@ - - * @template-implements ValueVisitor - * @template-implements SelectorVisitor - */ -final class SerializeVisitor implements CssVisitor, ValueVisitor, SelectorVisitor -{ - /** - * @var StringBuffer - */ - private $buffer; - - /** - * The current indentation of the CSS output. - * - * @var int - */ - private $indentation = 0; - - /** - * Whether we're emitting an unambiguous representation of the source - * structure, as opposed to valid CSS. - * - * @var bool - */ - private $inspect; - - /** - * Whether quoted strings should be emitted with quotes. - * - * @var bool - */ - private $quote; - - /** - * @var bool - */ - private $compressed; - - /** - * @phpstan-param OutputStyle::* $style - */ - public function __construct(bool $inspect = false, bool $quote = true, string $style = OutputStyle::EXPANDED) - { - $this->buffer = new SimpleStringBuffer(); - $this->inspect = $inspect; - $this->quote = $quote; - $this->compressed = $style === OutputStyle::COMPRESSED; - } - - /** - * @return StringBuffer - */ - public function getBuffer(): StringBuffer - { - return $this->buffer; - } - - public function visitCssStylesheet($node): void - { - $previous = null; - - foreach ($node->getChildren() as $child) { - if ($this->isInvisible($child)) { - continue; - } - - if ($previous !== null) { - if ($this->requiresSemicolon($previous)) { - $this->buffer->writeChar(';'); - } - - if ($this->isTrailingComment($child, $previous)) { - $this->writeOptionalSpace(); - } else { - $this->writeLineFeed(); - - if ($previous->isGroupEnd()) { - $this->writeLineFeed(); - } - } - } - - $previous = $child; - $child->accept($this); - } - - if ($previous !== null && $this->requiresSemicolon($previous) && !$this->compressed) { - $this->buffer->writeChar(';'); - } - } - - public function visitCssComment($node): void - { - $this->for($node, function () use ($node) { - // Preserve comments that start with `/*!`. - if ($this->compressed && !$node->isPreserved()) { - return; - } - - $minimumIndentation = $this->minimumIndentation($node->getText()); - assert($minimumIndentation !== -1); - - if ($minimumIndentation === null) { - $this->writeIndentation(); - $this->buffer->write($node->getText()); - return; - } - - $minimumIndentation = min($minimumIndentation, $node->getSpan()->getStart()->getColumn()); - $this->writeIndentation(); - $this->writeWithIndent($node->getText(), $minimumIndentation); - }); - } - - public function visitCssAtRule($node): void - { - $this->writeIndentation(); - - $this->for($node, function () use ($node) { - $this->buffer->writeChar('@'); - $this->write($node->getName()); - - $value = $node->getValue(); - - if ($value !== null) { - $this->buffer->writeChar(' '); - $this->write($value); - } - - if (!$node->isChildless()) { - $this->writeOptionalSpace(); - $this->visitChildren($node); - } - }); - } - - public function visitCssMediaRule($node): void - { - $this->writeIndentation(); - - $this->for($node, function () use ($node) { - $this->buffer->write('@media'); - - $firstQuery = $node->getQueries()[0]; - - if (!$this->compressed || $firstQuery->getModifier() !== null || $firstQuery->getType() !== null || (\count($firstQuery->getConditions()) === 1) && StringUtil::startsWith($firstQuery->getConditions()[0], '(not ')) { - $this->buffer->writeChar(' '); - } - - $this->writeBetween($node->getQueries(), $this->getCommaSeparator(), [$this, 'visitMediaQuery']); - }); - - $this->writeOptionalSpace(); - $this->visitChildren($node); - } - - public function visitCssImport($node): void - { - $this->writeIndentation(); - - $this->for($node, function () use ($node) { - $this->buffer->write('@import'); - $this->writeOptionalSpace(); - $this->for($node->getUrl(), function () use ($node) { - $this->writeImportUrl($node->getUrl()->getValue()); - }); - - if ($node->getModifiers() !== null) { - $this->writeOptionalSpace(); - $this->write($node->getModifiers()); - } - }); - } - - /** - * Writes $url, which is an import's URL, to the buffer. - */ - private function writeImportUrl(string $url): void - { - if (!$this->compressed || $url[0] !== 'u') { - $this->buffer->write($url); - return; - } - - // If this is url(...), remove the surrounding function. This is terser and - // it allows us to remove whitespace between `@import` and the URL. - $urlContents = substr($url, 4, \strlen($url) - 5); - - $maybeQuote = $urlContents[0]; - if ($maybeQuote === "'" || $maybeQuote === '"') { - $this->buffer->write($urlContents); - } else { - // If the URL didn't contain quotes, write them manually. - $this->visitQuotedString($urlContents); - } - } - - public function visitCssKeyframeBlock($node): void - { - $this->writeIndentation(); - - $this->for($node->getSelector(), function () use ($node) { - $this->writeBetween($node->getSelector()->getValue(), $this->getCommaSeparator(), [$this->buffer, 'write']); - }); - $this->writeOptionalSpace(); - $this->visitChildren($node); - } - - private function visitMediaQuery(CssMediaQuery $query): void - { - if ($query->getModifier() !== null) { - $this->buffer->write($query->getModifier()); - $this->buffer->writeChar(' '); - } - - if ($query->getType() !== null) { - $this->buffer->write($query->getType()); - - if (\count($query->getConditions())) { - $this->buffer->write(' and '); - } - } - - if (\count($query->getConditions()) === 1 && StringUtil::startsWith($query->getConditions()[0], '(not ')) { - $this->buffer->write('not '); - $condition = $query->getConditions()[0]; - $this->buffer->write(substr($condition, \strlen('(not '), \strlen($condition) - (\strlen('(not ') + 1))); - } else { - $operator = $query->isConjunction() ? 'and' : 'or'; - - $this->writeBetween($query->getConditions(), $this->compressed ? "$operator " : " $operator ", [$this->buffer, 'write']); - } - } - - public function visitCssStyleRule($node): void - { - $this->writeIndentation(); - - $this->for($node->getSelector(), function () use ($node) { - $node->getSelector()->getValue()->accept($this); - }); - $this->writeOptionalSpace(); - $this->visitChildren($node); - } - - public function visitCssSupportsRule($node): void - { - $this->writeIndentation(); - - $this->for($node, function () use ($node) { - $this->buffer->write('@supports'); - - if (!($this->compressed && $node->getCondition()->getValue()[0] === '(')) { - $this->buffer->writeChar(' '); - } - - $this->write($node->getCondition()); - }); - $this->writeOptionalSpace(); - $this->visitChildren($node); - } - - public function visitCssDeclaration($node): void - { - $this->writeIndentation(); - $this->write($node->getName()); - $this->buffer->writeChar(':'); - - // If `node` is a custom property that was parsed as a normal Sass-syntax - // property (such as `#{--foo}: ...`), we serialize its value using the - // normal Sass property logic as well. - if ($node->isCustomProperty() && $node->isParsedAsCustomProperty()) { - $this->for($node->getValue(), function () use ($node) { - if ($this->compressed) { - $this->writeFoldedValue($node); - } else { - $this->writeReindentedValue($node); - } - }); - } else { - $this->writeOptionalSpace(); - - try { - // TODO implement source map tracking - $node->getValue()->getValue()->accept($this); - } catch (SassScriptException $error) { - throw new SassRuntimeException($error->getMessage(), $node->getValue()->getSpan(), $error); - } - } - } - - /** - * Emits the value of $node, with all newlines followed by whitespace - */ - private function writeFoldedValue(CssDeclaration $node): void - { - $value = $node->getValue()->getValue(); - assert($value instanceof SassString); - $scannner = new StringScanner($value->getText()); - - while (!$scannner->isDone()) { - $next = $scannner->readUtf8Char(); - if ($next !== "\n") { - $this->buffer->writeChar($next); - continue; - } - - $this->buffer->writeChar(' '); - while (Character::isWhitespace($scannner->peekChar())) { - $scannner->readChar(); - } - } - } - - /** - * Emits the value of $node, re-indented relative to the current indentation. - */ - private function writeReindentedValue(CssDeclaration $node): void - { - $nodeValue = $node->getValue()->getValue(); - assert($nodeValue instanceof SassString); - $value = $nodeValue->getText(); - - $minimumIndentation = $this->minimumIndentation($value); - if ($minimumIndentation === null) { - $this->buffer->write($value); - return; - } - - if ($minimumIndentation === -1) { - $this->buffer->write(StringUtil::trimAsciiRight($value, true)); - $this->buffer->writeChar(' '); - return; - } - - $minimumIndentation = min($minimumIndentation, $node->getName()->getSpan()->getStart()->getColumn()); - $this->writeWithIndent($value, $minimumIndentation); - } - - /** - * Returns the indentation level of the least-indented non-empty line in - * $text after the first. - * - * Returns `null` if $text contains no newlines, and -1 if it contains - * newlines but no lines are indented. - */ - private function minimumIndentation(string $text): ?int - { - $scanner = new LineScanner($text); - while (!$scanner->isDone() && $scanner->readChar() !== "\n") { - } - - if ($scanner->isDone()) { - return $scanner->peekChar(-1) === "\n" ? -1 : null; - } - - $min = null; - while (!$scanner->isDone()) { - while (!$scanner->isDone()) { - $next = $scanner->peekChar(); - if ($next !== ' ' && $next !== "\t") { - break; - } - $scanner->readChar(); - } - - if ($scanner->isDone() || $scanner->scanChar("\n")) { - continue; - } - - $min = $min === null ? $scanner->getColumn() : min($min, $scanner->getColumn()); - - while (!$scanner->isDone() && $scanner->readChar() !== "\n") { - } - } - - return $min ?? -1; - } - - /** - * Writes $text to {@see buffer}, replacing $minimumIndentation with - * {@see indentation} for each non-empty line after the first. - * - * Compresses trailing empty lines of $text into a single trailing space. - */ - private function writeWithIndent(string $text, int $minimumIndentation): void - { - $scanner = new LineScanner($text); - - while (!$scanner->isDone()) { - $next = $scanner->readChar(); - - if ($next === "\n") { - break; - } - $this->buffer->writeChar($next); - } - - while (true) { - assert(Character::isWhitespace($scanner->peekChar(-1))); - // Scan forward until we hit non-whitespace or the end of [text]. - $lineStart = $scanner->getPosition(); - $newlines = 1; - - while (true) { - // If we hit the end of $text, we still need to preserve the fact that - // whitespace exists because it could matter for custom properties. - if ($scanner->isDone()) { - $this->buffer->writeChar(' '); - return; - } - - $next = $scanner->readChar(); - - if ($next === ' ' || $next === "\t") { - continue; - } - - if ($next !== "\n") { - break; - } - - $lineStart = $scanner->getPosition(); - $newlines++; - } - - $this->writeTimes("\n", $newlines); - $this->writeIndentation(); - $this->buffer->write($scanner->substring($lineStart + $minimumIndentation)); - - // Scan and write until we hit a newline or the end of $text. - while (true) { - if ($scanner->isDone()) { - return; - } - $next = $scanner->readChar(); - if ($next === "\n") { - break; - } - $this->buffer->writeChar($next); - } - } - } - - // ## Values - - public function visitBoolean(SassBoolean $value) - { - $this->buffer->write($value->getValue() ? 'true': 'false'); - } - - public function visitCalculation(SassCalculation $value) - { - $this->buffer->write($value->getName()); - $this->buffer->writeChar('('); - - $isFirst = true; - - foreach ($value->getArguments() as $argument) { - if ($isFirst) { - $isFirst = false; - } else { - $this->buffer->write($this->getCommaSeparator()); - } - - $this->writeCalculationValue($argument); - } - $this->buffer->writeChar(')'); - } - - private function writeCalculationValue(object $value): void - { - if ($value instanceof Value) { - $value->accept($this); - } elseif ($value instanceof CalculationInterpolation) { - $this->buffer->write($value->getValue()); - } elseif ($value instanceof CalculationOperation) { - $left = $value->getLeft(); - $parenthesizeLeft = $left instanceof CalculationInterpolation || ($left instanceof CalculationOperation && CalculationOperator::getPrecedence($left->getOperator()) < CalculationOperator::getPrecedence($value->getOperator())); - - if ($parenthesizeLeft) { - $this->buffer->writeChar('('); - } - $this->writeCalculationValue($left); - if ($parenthesizeLeft) { - $this->buffer->writeChar(')'); - } - - $operatorWhitespace = !$this->compressed || CalculationOperator::getPrecedence($value->getOperator()) === 1; - if ($operatorWhitespace) { - $this->buffer->writeChar(' '); - } - $this->buffer->write($value->getOperator()); - if ($operatorWhitespace) { - $this->buffer->writeChar(' '); - } - - $right = $value->getRight(); - $parenthesizeRight = $right instanceof CalculationInterpolation || ($right instanceof CalculationOperation && $this->parenthesizeCalculationRhs($value->getOperator(), $right->getOperator())); - - if ($parenthesizeRight) { - $this->buffer->writeChar('('); - } - $this->writeCalculationValue($right); - if ($parenthesizeRight) { - $this->buffer->writeChar(')'); - } - } - } - - /** - * Returns whether the right-hand operation of a calculation should be - * parenthesized. - * - * In `a ? (b # c)`, `outer` is `?` and `right` is `#`. - * - * @phpstan-param CalculationOperator::* $outer - * @phpstan-param CalculationOperator::* $right - */ - private function parenthesizeCalculationRhs(string $outer, string $right): bool - { - if ($outer === CalculationOperator::DIVIDED_BY) { - return true; - } - - if ($outer === CalculationOperator::PLUS) { - return false; - } - - return $right === CalculationOperator::PLUS || $right === CalculationOperator::MINUS; - } - - public function visitColor(SassColor $value) - { - $name = Colors::RGBaToColorName($value->getRed(), $value->getGreen(), $value->getBlue(), $value->getAlpha()); - - // In compressed mode, emit colors in the shortest representation possible. - if ($this->compressed) { - if (!NumberUtil::fuzzyEquals($value->getAlpha(), 1)) { - $this->writeRgb($value); - } else { - $canUseShortHex = $this->canUseShortHex($value); - $hexLength = $canUseShortHex ? 4 : 7; - - if ($name !== null && \strlen($name) <= $hexLength) { - $this->buffer->write($name); - } elseif ($canUseShortHex) { - $this->buffer->writeChar('#'); - $this->buffer->writeChar(dechex($value->getRed() & 0xF)); - $this->buffer->writeChar(dechex($value->getGreen() & 0xF)); - $this->buffer->writeChar(dechex($value->getBlue() & 0xF)); - } else { - $this->buffer->writeChar('#'); - $this->writeHexComponent($value->getRed()); - $this->writeHexComponent($value->getGreen()); - $this->writeHexComponent($value->getBlue()); - } - } - - return; - } - - $format = $value->getFormat(); - - if ($format !== null) { - if ($format === ColorFormat::RGB_FUNCTION) { - $this->writeRgb($value); - } elseif ($format === ColorFormat::HSL_FUNCTION) { - $this->writeHsl($value); - } else { - $this->buffer->write($format->getOriginal()); - } - } elseif ($name !== null && - // Always emit generated transparent colors in rgba format. This works - // around an IE bug. See https://github.com/sass/sass/issues/1782. - !NumberUtil::fuzzyEquals($value->getAlpha(), 0) - ) { - $this->buffer->write($name); - } elseif (NumberUtil::fuzzyEquals($value->getAlpha(), 1)) { - $this->buffer->writeChar('#'); - $this->writeHexComponent($value->getRed()); - $this->writeHexComponent($value->getGreen()); - $this->writeHexComponent($value->getBlue()); - } else { - $this->writeRgb($value); - } - } - - /** - * Writes $value as an `rgb` or `rgba` function. - */ - private function writeRgb(SassColor $value): void - { - $opaque = NumberUtil::fuzzyEquals($value->getAlpha(), 1); - $this->buffer->write($opaque ? 'rgb(' : 'rgba('); - $this->buffer->write((string) $value->getRed()); - $this->buffer->write($this->getCommaSeparator()); - $this->buffer->write((string) $value->getGreen()); - $this->buffer->write($this->getCommaSeparator()); - $this->buffer->write((string) $value->getBlue()); - - if (!$opaque) { - $this->buffer->write($this->getCommaSeparator()); - $this->writeNumber($value->getAlpha()); - } - - $this->buffer->writeChar(')'); - } - - /** - * Writes $value as an `hsl` or `hsla` function. - */ - private function writeHsl(SassColor $value): void - { - $opaque = NumberUtil::fuzzyEquals($value->getAlpha(), 1); - $this->buffer->write($opaque ? 'hsl(' : 'hsla('); - $this->writeNumber($value->getHue()); - $this->buffer->write('deg'); - $this->buffer->write($this->getCommaSeparator()); - $this->writeNumber($value->getSaturation()); - $this->buffer->writeChar('%'); - $this->buffer->write($this->getCommaSeparator()); - $this->writeNumber($value->getLightness()); - $this->buffer->writeChar('%'); - - if (!$opaque) { - $this->buffer->write($this->getCommaSeparator()); - $this->writeNumber($value->getAlpha()); - } - - $this->buffer->writeChar(')'); - } - - /** - * Returns whether $color's hex pair representation is symmetrical (e.g. `FF`). - */ - private function isSymmetricalHex(int $color): bool - { - return ($color & 0xF) === $color >> 4; - } - - /** - * Returns whether $color can be represented as a short hexadecimal color - * (e.g. `#fff`). - */ - private function canUseShortHex(SassColor $color): bool - { - return $this->isSymmetricalHex($color->getRed()) && $this->isSymmetricalHex($color->getGreen()) && $this->isSymmetricalHex($color->getBlue()); - } - - /** - * Emits $color as a hex character pair. - */ - private function writeHexComponent(int $color): void - { - $this->buffer->write(str_pad(dechex($color), 2, '0', STR_PAD_LEFT)); - } - - public function visitFunction(SassFunction $value) - { - if (!$this->inspect) { - throw new SassScriptException("$value is not a valid CSS value."); - } - - $this->buffer->write('get-function('); - $this->visitQuotedString($value->getName()); - $this->buffer->writeChar(')'); - } - - public function visitList(SassList $value) - { - if ($value->hasBrackets()) { - $this->buffer->writeChar('['); - } elseif (\count($value->asList()) === 0) { - if (!$this->inspect) { - throw new SassScriptException("() is not a valid CSS value."); - } - - $this->buffer->write('()'); - return; - } - - $singleton = $this->inspect && \count($value->asList()) === 1 && ($value->getSeparator() === ListSeparator::COMMA || $value->getSeparator() === ListSeparator::SLASH); - - if ($singleton && !$value->hasBrackets()) { - $this->buffer->writeChar('('); - } - - $separator = $this->separatorString($value->getSeparator()); - - $isFirst = true; - - foreach ($value->asList() as $element) { - if (!$this->inspect && $element->isBlank()) { - continue; - } - - if ($isFirst) { - $isFirst = false; - } else { - $this->buffer->write($separator); - } - - $needsParens = $this->inspect && self::elementNeedsParens($value->getSeparator(), $element); - - if ($needsParens) { - $this->buffer->writeChar('('); - } - - $element->accept($this); - - if ($needsParens) { - $this->buffer->writeChar(')'); - } - } - - if ($singleton) { - $this->buffer->write($value->getSeparator()); - - if (!$value->hasBrackets()) { - $this->buffer->writeChar(')'); - } - } - - if ($value->hasBrackets()) { - $this->buffer->writeChar(']'); - } - } - - /** - * @phpstan-param ListSeparator::* $separator - */ - private function separatorString(string $separator): string - { - switch ($separator) { - case ListSeparator::COMMA: - return $this->getCommaSeparator(); - - case ListSeparator::SLASH: - return $this->compressed ? '/' : ' / '; - - case ListSeparator::SPACE: - return ' '; - - default: - /** - * This should never be used, but it may still be returned since - * {@see separatorString} is invoked eagerly by {@see writeList} even for lists - * with only one element. - */ - return ''; - } - } - - /** - * Returns whether the value needs parentheses as an element in a list with the given separator. - * - * @param string $separator - * @param Value $value - * - * @return bool - * - * @phpstan-param ListSeparator::* $separator - */ - private static function elementNeedsParens(string $separator, Value $value): bool - { - if (!$value instanceof SassList) { - return false; - } - - if (count($value->asList()) < 2) { - return false; - } - - if ($value->hasBrackets()) { - return false; - } - - switch ($separator) { - case ListSeparator::COMMA: - return $value->getSeparator() === ListSeparator::COMMA; - - case ListSeparator::SLASH: - return $value->getSeparator() === ListSeparator::COMMA || $value->getSeparator() === ListSeparator::SLASH; - - default: - return $value->getSeparator() !== ListSeparator::UNDECIDED; - } - } - - public function visitMap(SassMap $value) - { - if (!$this->inspect) { - throw new SassScriptException("$value is not a valid CSS value."); - } - - $this->buffer->writeChar('('); - - $isFirst = true; - - foreach ($value->getContents() as $key => $element) { - if ($isFirst) { - $isFirst = false; - } else { - $this->buffer->write(', '); - } - - $this->writeMapElement($key); - $this->buffer->write(': '); - $this->writeMapElement($element); - } - $this->buffer->writeChar(')'); - } - - private function writeMapElement(Value $value): void - { - $needsParens = $value instanceof SassList - && ListSeparator::COMMA === $value->getSeparator() - && !$value->hasBrackets(); - - if ($needsParens) { - $this->buffer->writeChar('('); - } - - $value->accept($this); - - if ($needsParens) { - $this->buffer->writeChar(')'); - } - } - - public function visitNull() - { - if ($this->inspect) { - $this->buffer->write('null'); - } - } - - public function visitNumber(SassNumber $value) - { - $asSlash = $value->getAsSlash(); - - if ($asSlash !== null) { - $this->visitNumber($asSlash[0]); - $this->buffer->writeChar('/'); - $this->visitNumber($asSlash[1]); - - return; - } - - $this->writeNumber($value->getValue()); - - if (!$this->inspect) { - if (\count($value->getNumeratorUnits()) > 1 || \count($value->getDenominatorUnits()) > 0) { - throw new SassScriptException("$value is not a valid CSS value."); - } - - if (\count($value->getNumeratorUnits()) > 0) { - $this->buffer->write($value->getNumeratorUnits()[0]); - } - } else { - $this->buffer->write($value->getUnitString()); - } - } - - /** - * Writes $number without exponent notation and with at most - * {@see SassNumber::PRECISION} digits after the decimal point. - * - * @param float $number - */ - private function writeNumber(float $number): void - { - if (is_nan($number)) { - $this->buffer->write('NaN'); - return; - } - - if ($number === INF) { - $this->buffer->write('Infinity'); - return; - } - - if ($number === -INF) { - $this->buffer->write('-Infinity'); - return; - } - - $int = NumberUtil::fuzzyAsInt($number); - - if ($int !== null) { - $this->buffer->write((string) $int); - return; - } - - $output = number_format($number, SassNumber::PRECISION, '.', ''); - - $this->buffer->write(rtrim(rtrim($output, '0'), '.')); - } - - public function visitString(SassString $value) - { - if ($this->quote && $value->hasQuotes()) { - $this->visitQuotedString($value->getText()); - } else { - $this->visitUnquotedString($value->getText()); - } - } - - private function visitQuotedString(string $string): void - { - $includesDoubleQuote = false !== strpos($string, '"'); - $includesSingleQuote = false !== strpos($string, '\''); - $forceDoubleQuotes = $includesSingleQuote && $includesDoubleQuote; - $quote = $forceDoubleQuotes || !$includesDoubleQuote ? '"' : "'"; - - $this->buffer->writeChar($quote); - - $length = \strlen($string); - - for ($i = 0; $i < $length; $i++) { - $char = $string[$i]; - - switch ($char) { - case "'": - $this->buffer->writeChar("'"); // such string is always rendered double-quoted - break; - - case '"': - if ($forceDoubleQuotes) { - $this->buffer->writeChar('\\'); - } - $this->buffer->writeChar('"'); - break; - - case "\0": - case "\x1": - case "\x2": - case "\x3": - case "\x4": - case "\x5": - case "\x6": - case "\x7": - case "\x8": - case "\xA": - case "\xB": - case "\xC": - case "\xD": - case "\xE": - case "\xF": - case "\x11": - case "\x12": - case "\x13": - case "\x14": - case "\x15": - case "\x16": - case "\x17": - case "\x18": - case "\x19": - case "\x1A": - case "\x1B": - case "\x1C": - case "\x1D": - case "\x1E": - case "\x1F": - $this->writeEscape($this->buffer, $char, $string, $i); - break; - - case '\\': - $this->buffer->writeChar('\\'); - $this->buffer->writeChar('\\'); - break; - - default: - $newIndex = $this->tryPrivateUseCharacter($this->buffer, $char, $string, $i); - - if ($newIndex !== null) { - $i = $newIndex; - break; - } - - $this->buffer->writeChar($char); - break; - } - } - - $this->buffer->writeChar($quote); - } - - private function visitUnquotedString(string $string): void - { - $afterNewline = false; - $length = \strlen($string); - - for ($i = 0; $i < $length; ++$i) { - $char = $string[$i]; - - switch ($char) { - case "\n": - $this->buffer->writeChar(' '); - $afterNewline = true; - break; - - case ' ': - if (!$afterNewline) { - $this->buffer->writeChar(' '); - } - break; - - default: - $afterNewline = false; - $newIndex = $this->tryPrivateUseCharacter($this->buffer, $char, $string, $i); - - if ($newIndex !== null) { - $i = $newIndex; - break; - } - - $this->buffer->writeChar($char); - break; - } - } - } - - /** - * If $char is the beginning of a private-use character and Sass isn't - * emitting compressed CSS, writes that character as an escape to $buffer. - * - * The $string is the string from which $char was read, and $i is the - * index it was read from. If this successfully writes the character, returns - * the index of the *last* byte that was consumed for it. Otherwise, - * returns `null`. - * - * In expanded mode, we print all characters in Private Use Areas as escape - * codes since there's no useful way to render them directly. These - * characters are often used for glyph fonts, where it's useful for readers - * to be able to distinguish between them in the rendered stylesheet. - */ - private function tryPrivateUseCharacter(StringBuffer $buffer, string $char, string $string, int $i): ?int - { - if ($this->compressed) { - return null; - } - - $firstByteCode = \ord($char); - if ($firstByteCode >= 0xF0) { - $extraBytes = 3; // 4-bytes chars - } elseif ($firstByteCode >= 0xE0) { - $extraBytes = 2; // 3-bytes chars - } elseif ($firstByteCode >= 0xC2) { - $extraBytes = 1; // 2-bytes chars - } elseif ($firstByteCode >= 0x80 && $firstByteCode <= 0x8F) { - return null; // Continuation of a UTF-8 char started in a previous byte - } else { - $extraBytes = 0; - } - - if (\strlen($string) <= $i + $extraBytes) { - return null; // Invalid UTF-8 chars - } - - if ($extraBytes) { - $fullChar = substr($string, $i, $extraBytes + 1); - $charCode = Util::mbOrd($fullChar); - } else { - $fullChar = $char; - $charCode = $firstByteCode; - } - - if ($charCode >= 0xE000 && $charCode <= 0xF8FF || // PUA of the BMP - $charCode >= 0xF0000 && $charCode <= 0x10FFFF // Supplementary PUAs of the planes 15 and 16 - ) { - $this->writeEscape($buffer, $fullChar, $string, $i + $extraBytes); - - return $i + $extraBytes; - } - - return null; - } - - /** - * Writes $character as a hexadecimal escape sequence to $buffer. - * - * The $string is the string from which the escape is being written, and $i - * is the index of the last byte of $character in that string. These - * are used to write a trailing space after the escape if necessary to - * disambiguate it from the next character. - */ - private function writeEscape(StringBuffer $buffer, string $character, string $string, int $i): void - { - $buffer->writeChar('\\'); - $buffer->write(dechex(Util::mbOrd($character))); - - if (\strlen($string) === $i + 1) { - return; - } - - $next = $string[$i + 1]; - - if ($next === ' ' || $next === "\t" || Character::isHex($next)) { - $buffer->writeChar(' '); - } - } - - // ## Selectors - - public function visitAttributeSelector(AttributeSelector $attribute) - { - $this->buffer->writeChar('['); - $this->buffer->write($attribute->getName()); - - $value = $attribute->getValue(); - - if ($value !== null) { - assert($attribute->getOp() !== null); - $this->buffer->write($attribute->getOp()); - - // Emit identifiers that start with `--` with quotes, because IE11 - // doesn't consider them to be valid identifiers. - if (Parser::isIdentifier($value) && 0 !== strpos($value, '--')) { - $this->buffer->write($value); - - if ($attribute->getModifier() !== null) { - $this->buffer->writeChar(' '); - } - } else { - $this->visitQuotedString($value); - - if ($attribute->getModifier() !== null) { - $this->writeOptionalSpace(); - } - } - - if ($attribute->getModifier() !== null) { - $this->buffer->write($attribute->getModifier()); - } - } - - $this->buffer->writeChar(']'); - } - - public function visitClassSelector(ClassSelector $klass) - { - $this->buffer->writeChar('.'); - $this->buffer->write($klass->getName()); - } - - public function visitComplexSelector(ComplexSelector $complex) - { - $this->writeCombinators($complex->getLeadingCombinators()); - - if (\count($complex->getLeadingCombinators()) !== 0 && \count($complex->getComponents()) !== 0) { - $this->writeOptionalSpace(); - } - - foreach ($complex->getComponents() as $i => $component) { - $this->visitCompoundSelector($component->getSelector()); - - if (\count($component->getCombinators()) !== 0) { - $this->writeOptionalSpace(); - } - - $this->writeCombinators($component->getCombinators()); - - if ($i !== \count($complex->getComponents()) - 1 && (!$this->compressed || \count($component->getCombinators()) === 0)) { - $this->buffer->writeChar(' '); - } - } - } - - /** - * Writes $combinators to {@see buffer}, with spaces in between in expanded - * mode. - * - * @param string[] $combinators - * - * @return void - */ - private function writeCombinators(array $combinators): void - { - $this->writeBetween($combinators, $this->compressed ? '' : ' ', function ($text) { - $this->buffer->write($text); - }); - } - - public function visitCompoundSelector(CompoundSelector $compound) - { - $start = $this->buffer->getLength(); - - foreach ($compound->getComponents() as $simple) { - $simple->accept($this); - } - - // If we emit an empty compound, it's because all of the components got - // optimized out because they match all selectors, so we just emit the - // universal selector. - if ($this->buffer->getLength() === $start) { - $this->buffer->writeChar('*'); - } - } - - public function visitIDSelector(IDSelector $id) - { - $this->buffer->writeChar('#'); - $this->buffer->write($id->getName()); - } - - public function visitSelectorList(SelectorList $list) - { - $first = true; - - foreach ($list->getComponents() as $complex) { - if (!$this->inspect && $complex->isInvisible()) { - continue; - } - - if ($first) { - $first = false; - } else { - $this->buffer->writeChar(','); - - if ($complex->getLineBreak()) { - $this->writeLineFeed(); - } else { - $this->writeOptionalSpace(); - } - } - - $this->visitComplexSelector($complex); - } - } - - public function visitParentSelector(ParentSelector $parent) - { - $this->buffer->writeChar('&'); - - if ($parent->getSuffix() !== null) { - $this->buffer->write($parent->getSuffix()); - } - } - - public function visitPlaceholderSelector(PlaceholderSelector $placeholder) - { - $this->buffer->writeChar('%'); - $this->buffer->write($placeholder->getName()); - } - - public function visitPseudoSelector(PseudoSelector $pseudo) - { - $innerSelector = $pseudo->getSelector(); - - // `:not(%a)` is semantically identical to `*`. - if ($innerSelector !== null && $pseudo->getName() === 'not' && $innerSelector->isInvisible()) { - return; - } - - $this->buffer->writeChar(':'); - if ($pseudo->isSyntacticElement()) { - $this->buffer->writeChar(':'); - } - $this->buffer->write($pseudo->getName()); - - if ($pseudo->getArgument() === null && $pseudo->getSelector() === null) { - return; - } - - $this->buffer->writeChar('('); - - if ($pseudo->getArgument() !== null) { - $this->buffer->write($pseudo->getArgument()); - - if ($pseudo->getSelector() !== null) { - $this->buffer->writeChar(' '); - } - } - - if ($innerSelector !== null) { - $this->visitSelectorList($innerSelector); - } - - $this->buffer->writeChar(')'); - } - - public function visitTypeSelector(TypeSelector $type) - { - $this->buffer->write($type->getName()); - } - - public function visitUniversalSelector(UniversalSelector $universal) - { - if ($universal->getNamespace() !== null) { - $this->buffer->write($universal->getNamespace()); - $this->buffer->writeChar('|'); - } - $this->buffer->writeChar('*'); - } - - // ## Utilities - - /** - * Runs $callback and associates all text written within it with the span of $node - * - * @template T - * - * @param AstNode $node - * @param callable(): T $callback - * - * @return T - */ - private function for(AstNode $node, callable $callback) - { - // TODO implement sourcemap tracking - return $callback(); - } - - /** - * @param CssValue $value - */ - private function write(CssValue $value): void - { - $this->for($value, function () use ($value) { - $this->buffer->write($value->getValue()); - }); - } - - /** - * Emits `$parent->getChildren()` in a block - */ - private function visitChildren(CssParentNode $parent): void - { - $this->buffer->writeChar('{'); - - $prePrevious = null; - $previous = null; - - foreach ($parent->getChildren() as $child) { - if ($this->isInvisible($child)) { - continue; - } - - if ($previous !== null && $this->requiresSemicolon($previous)) { - $this->buffer->writeChar(';'); - } - - if ($this->isTrailingComment($child, $previous ?? $parent)) { - $this->writeOptionalSpace(); - $this->withoutIndendation(function () use ($child) { - $child->accept($this); - }); - } else { - $this->writeLineFeed(); - $this->indent(function () use ($child) { - $child->accept($this); - }); - } - - $prePrevious = $previous; - $previous = $child; - } - - if ($previous !== null) { - if ($this->requiresSemicolon($previous) && !$this->compressed) { - $this->buffer->writeChar(';'); - } - - if ($prePrevious !== null && $this->isTrailingComment($previous, $parent)) { - $this->writeOptionalSpace(); - } else { - $this->writeLineFeed(); - $this->writeIndentation(); - } - } - - $this->buffer->writeChar('}'); - } - - /** - * Whether $node requires a semicolon to be written after it. - */ - private function requiresSemicolon(CssNode $node): bool - { - if ($node instanceof CssParentNode) { - return $node->isChildless(); - } - - return !$node instanceof CssComment; - } - - private function isTrailingComment(CssNode $node, CssNode $previous): bool - { - // Short-circuit in compressed mode to avoid expensive span shenanigans - // (shespanigans?), since we're compressing all whitespace anyway. - if ($this->compressed) { - return false; - } - - if (!$node instanceof CssComment) { - return false; - } - - if (!SpanUtil::contains($previous->getSpan(), $node->getSpan())) { - return $node->getSpan()->getStart()->getLine() === $previous->getSpan()->getEnd()->getLine(); - } - - // Walk back from just before the current node starts looking for the - // parent's left brace (to open the child block). This is safer than a - // simple forward search of the previous.span.text as that might contain - // other left braces. - $searchFrom = $node->getSpan()->getStart()->getOffset() - $previous->getSpan()->getStart()->getOffset() - 1; - - // Imports can cause a node to be "contained" by another node when they are - // actually the same node twice in a row. - if ($searchFrom < 0) { - return false; - } - - $endOffset = strrpos($previous->getSpan()->getText(), '{', $searchFrom); - if ($endOffset === false) { - $endOffset = 0; - } - - $span = $previous->getSpan()->getFile()->span($previous->getSpan()->getStart()->getOffset(), $previous->getSpan()->getStart()->getOffset() + $endOffset); - - return $node->getSpan()->getStart()->getLine() === $span->getEnd()->getLine(); - } - - /** - * Writes a line feed, unless this emitting compressed CSS. - */ - private function writeLineFeed(): void - { - if (!$this->compressed) { - $this->buffer->writeChar("\n"); - } - } - - private function writeOptionalSpace(): void - { - if (!$this->compressed) { - $this->buffer->writeChar(' '); - } - } - - private function writeIndentation(): void - { - if (!$this->compressed) { - $this->writeTimes(' ', $this->indentation * 2); - } - } - - /** - * Writes $char to {@see buffer} with $times repetitions. - */ - private function writeTimes(string $char, int $times): void - { - for ($i = 0; $i < $times; $i++) { - $this->buffer->writeChar($char); - } - } - - /** - * Calls $callback to write each value in $iterable, and writes $text - * between each one. - * - * @template T - * - * @param iterable $iterable - * @param string $text - * @param callable(T): void $callback - */ - private function writeBetween(iterable $iterable, string $text, callable $callback): void - { - $first = true; - - foreach ($iterable as $value) { - if ($first) { - $first = false; - } else { - $this->buffer->write($text); - } - - $callback($value); - } - } - - /** - * Returns a comma used to separate values in lists. - */ - private function getCommaSeparator(): string - { - return $this->compressed ? ',': ', '; - } - - /** - * Runs $callback with indentation increased one level. - * - * @param callable(): void $callback - */ - private function indent(callable $callback): void - { - $this->indentation++; - $callback(); - $this->indentation--; - } - - /** - * Runs $callback without any indentation. - * - * @param callable(): void $callback - */ - private function withoutIndendation(callable $callback): void - { - $savedIndentation = $this->indentation; - $this->indentation = 0; - $callback(); - $this->indentation = $savedIndentation; - } - - /** - * Returns whether $node is invisible. - */ - private function isInvisible(CssNode $node): bool - { - return !$this->inspect && ($this->compressed ? $node->isInvisibleHidingComments() : $node->isInvisible()); - } -} diff --git a/scssphp/src/Serializer/Serializer.php b/scssphp/src/Serializer/Serializer.php deleted file mode 100644 index 6624b5f..0000000 --- a/scssphp/src/Serializer/Serializer.php +++ /dev/null @@ -1,84 +0,0 @@ -accept($visitor); - $css = (string) $visitor->getBuffer(); - - $prefix = ''; - - if ($charset && strlen($css) !== Util::mbStrlen($css)) { - if ($style === OutputStyle::COMPRESSED) { - $prefix = "\u{FEFF}"; - } else { - $prefix = '@charset "UTF-8";' . "\n"; - } - } - - // TODO build the source map - - return new SerializeResult($prefix . $css); - } - - /** - * Converts $value to a CSS string. - * - * If $inspect is `true`, this will emit an unambiguous representation of the - * source structure. Note however that, although this will be valid SCSS, it - * may not be valid CSS. If $inspect is `false` and $value can't be - * represented in plain CSS, throws a {@see SassScriptException}. - * - * If $quote is `false`, quoted strings are emitted without quotes. - */ - public static function serializeValue(Value $value, bool $inspect = false, bool $quote = true): string - { - $visitor = new SerializeVisitor($inspect, $quote); - $value->accept($visitor); - - return (string) $visitor->getBuffer(); - } - - /** - * Converts $selector to a CSS string. - * - * If $inspect is `true`, this will emit an unambiguous representation of the - * source structure. Note however that, although this will be valid SCSS, it - * may not be valid CSS. If $inspect is `false` and $selector can't be - * represented in plain CSS, throws a {@see SassScriptException}. - */ - public static function serializeSelector(Selector $selector, bool $inspect = false): string - { - $visitor = new SerializeVisitor($inspect); - $selector->accept($visitor); - - return (string) $visitor->getBuffer(); - } -} diff --git a/scssphp/src/Serializer/SimpleStringBuffer.php b/scssphp/src/Serializer/SimpleStringBuffer.php deleted file mode 100644 index 546f344..0000000 --- a/scssphp/src/Serializer/SimpleStringBuffer.php +++ /dev/null @@ -1,44 +0,0 @@ -text); - } - - public function write(string $string): void - { - $this->text .= $string; - } - - public function writeChar(string $char): void - { - $this->text .= $char; - } - - public function __toString(): string - { - return $this->text; - } -} diff --git a/scssphp/src/Serializer/StringBuffer.php b/scssphp/src/Serializer/StringBuffer.php deleted file mode 100644 index cf01531..0000000 --- a/scssphp/src/Serializer/StringBuffer.php +++ /dev/null @@ -1,33 +0,0 @@ -file = $file; - $this->start = $start; - $this->end = $end; - } - - public function getFile(): SourceFile - { - return $this->file; - } - - public function getLength(): int - { - return $this->end - $this->start; - } - - public function getStart(): SourceLocation - { - return new SourceLocation($this->file, $this->start); - } - - public function getEnd(): SourceLocation - { - return new SourceLocation($this->file, $this->end); - } - - public function getText(): string - { - return $this->file->getText($this->start, $this->end); - } - - public function expand(FileSpan $other): FileSpan - { - if ($this->file->getSourceUrl() !== $other->file->getSourceUrl()) { - throw new \InvalidArgumentException('Source map URLs don\'t match.'); - } - - $start = min($this->start, $other->start); - $end = max($this->end, $other->end); - - return new FileSpan($this->file, $start, $end); - } - - /** - * Formats $message in a human-friendly way associated with this span. - * - * @param string $message - * - * @return string - */ - public function message(string $message): string - { - $startLine = $this->getStart()->getLine() + 1; - $startColumn = $this->getStart()->getColumn() + 1; - $sourceUrl = $this->file->getSourceUrl(); - - $buffer = "line $startLine, column $startColumn"; - - if ($sourceUrl !== null) { - $prettyUri = Path::prettyUri($sourceUrl); - $buffer .= " of $prettyUri"; - } - - $buffer .= ": $message"; - - // TODO implement the highlighting of a code snippet - - return $buffer; - } - - /** - * Return a span from $start bytes (inclusive) to $end bytes - * (exclusive) after the beginning of this span - */ - public function subspan(int $start, ?int $end = null): FileSpan - { - ErrorUtil::checkValidRange($start, $end, $this->getLength()); - - if ($start === 0 && ($end === null || $end === $this->getLength())) { - return $this; - } - - return $this->file->span($this->start + $start, $end === null ? $this->end : $this->start + $end); - } -} diff --git a/scssphp/src/SourceSpan/SourceFile.php b/scssphp/src/SourceSpan/SourceFile.php deleted file mode 100644 index 1234947..0000000 --- a/scssphp/src/SourceSpan/SourceFile.php +++ /dev/null @@ -1,209 +0,0 @@ -string = $content; - $this->sourceUrl = $sourceUrl; - - // Extract line starts - $this->lineStarts = [0]; - - if ($content === '') { - return; - } - - $prev = 0; - - while (($pos = strpos($content, "\n", $prev)) !== false) { - $this->lineStarts[] = $pos; - $prev = $pos + 1; - } - - $this->lineStarts[] = \strlen($content); - - if (substr($content, -1) !== "\n") { - $this->lineStarts[] = \strlen($content) + 1; - } - } - - public function span(int $start, ?int $end = null): FileSpan - { - if ($end === null) { - $end = \strlen($this->string); - } - - return new FileSpan($this, $start, $end); - } - - public function getSourceUrl(): ?string - { - return $this->sourceUrl; - } - - /** - * The 0-based line - * - * @param int $position - * - * @return int - */ - public function getLine(int $position): int - { - if ($position < 0) { - throw new \RangeException('Position cannot be negative'); - } - - if ($position > \strlen($this->string)) { - throw new \RangeException('Position cannot be greater than the number of characters in the string.'); - } - - if ($this->isNearCacheLine($position)) { - assert($this->cachedLine !== null); - - return $this->cachedLine; - } - - $low = 0; - $high = \count($this->lineStarts); - - while ($low < $high) { - $mid = (int) (($high + $low) / 2); - - if ($position < $this->lineStarts[$mid]) { - $high = $mid - 1; - continue; - } - - if ($position >= $this->lineStarts[$mid + 1]) { - $low = $mid + 1; - continue; - } - - $this->cachedLine = $mid; - - return $this->cachedLine; - } - - $this->cachedLine = $low; - - return $this->cachedLine; - } - - /** - * Returns `true` if $position is near {@see $cachedLine}. - * - * Checks on {@see $cachedLine} and the next line. If it's on the next line, it - * updates {@see $cachedLine} to point to that. - * - * @param int $position - * - * @return bool - */ - private function isNearCacheLine(int $position): bool - { - if ($this->cachedLine === null) { - return false; - } - - if ($position < $this->lineStarts[$this->cachedLine]) { - return false; - } - - if ($this->cachedLine >= \count($this->lineStarts) - 1 || - $position < $this->lineStarts[$this->cachedLine + 1] - ) { - return true; - } - - if ($this->cachedLine >= \count($this->lineStarts) - 2 || - $position < $this->lineStarts[$this->cachedLine + 2] - ) { - ++$this->cachedLine; - - return true; - } - - return false; - } - - /** - * The 0-based column of that position - * - * @param int $position - * - * @return int - */ - public function getColumn(int $position): int - { - $line = $this->getLine($position); - - return $position - $this->lineStarts[$line]; - } - - /** - * Returns the text of the file from $start to $end (exclusive). - * - * If $end isn't passed, it defaults to the end of the file. - */ - public function getText(int $start, ?int $end = null): string - { - if ($end === null) { - return substr($this->string, $start); - } - - if ($end < $start) { - $length = 0; - } else { - $length = $end - $start; - } - - return substr($this->string, $start, $length); - } -} diff --git a/scssphp/src/SourceSpan/SourceLocation.php b/scssphp/src/SourceSpan/SourceLocation.php deleted file mode 100644 index 6a129cf..0000000 --- a/scssphp/src/SourceSpan/SourceLocation.php +++ /dev/null @@ -1,68 +0,0 @@ -file = $file; - $this->offset = $offset; - } - - public function getFile(): SourceFile - { - return $this->file; - } - - public function getOffset(): int - { - return $this->offset; - } - - /** - * The 0-based line of that location - */ - public function getLine(): int - { - return $this->file->getLine($this->offset); - } - - /** - * The 0-based column of that location - */ - public function getColumn(): int - { - return $this->file->getColumn($this->offset); - } - - public function getSourceUrl(): ?string - { - return $this->file->getSourceUrl(); - } -} diff --git a/scssphp/src/StackTrace/Frame.php b/scssphp/src/StackTrace/Frame.php deleted file mode 100644 index 8dbf179..0000000 --- a/scssphp/src/StackTrace/Frame.php +++ /dev/null @@ -1,123 +0,0 @@ -url = $url; - $this->line = $line; - $this->column = $column; - $this->member = $member; - } - - /** - * The URI of the file in which the code is located. - */ - public function getUrl(): string - { - return $this->url; - } - - /** - * The line number on which the code location is located. - * - * This can be null, indicating that the line number is unknown or - * unimportant. - */ - public function getLine(): ?int - { - return $this->line; - } - - /** - * The column number of the code location. - * - * This can be null, indicating that the column number is unknown or - * unimportant. - */ - public function getColumn(): ?int - { - return $this->column; - } - - /** - * The name of the member in which the code location occurs. - */ - public function getMember(): ?string - { - return $this->member; - } - - /** - * A human-friendly description of the code location. - */ - public function getLocation(): string - { - $library = Path::prettyUri($this->url); - - if ($this->line === null) { - return $library; - } - - if ($this->column === null) { - return $library . ' ' . $this->line; - } - - return $library . ' ' . $this->line . ':' . $this->column; - } -} diff --git a/scssphp/src/StackTrace/Trace.php b/scssphp/src/StackTrace/Trace.php deleted file mode 100644 index 48e74ac..0000000 --- a/scssphp/src/StackTrace/Trace.php +++ /dev/null @@ -1,55 +0,0 @@ - - * @readonly - */ - private $frames; - - /** - * @param list $frames - */ - public function __construct(array $frames) - { - $this->frames = $frames; - } - - /** - * @return list - */ - public function getFrames(): array - { - return $this->frames; - } - - public function getFormattedTrace(): string - { - $longest = 0; - - foreach ($this->frames as $frame) { - $length = \strlen($frame->getLocation()); - $longest = max($longest, $length); - } - - return implode(array_map(function (Frame $frame) use ($longest) { - return str_pad($frame->getLocation(), $longest) . ' ' . $frame->getMember() . "\n"; - }, $this->frames)); - } -} diff --git a/scssphp/src/Syntax.php b/scssphp/src/Syntax.php deleted file mode 100644 index 065ece5..0000000 --- a/scssphp/src/Syntax.php +++ /dev/null @@ -1,47 +0,0 @@ -= \ord('a') && $charCode <= \ord('z')) || ($charCode >= \ord('A') && $charCode <= \ord('Z')); - } - - /** - * Returns whether $character is a digit. - */ - public static function isDigit(?string $character): bool - { - if ($character === null) { - return false; - } - - $charCode = \ord($character); - - return $charCode >= \ord('0') && $charCode <= \ord('9'); - } - - /** - * Returns whether $character is legal as the start of a Sass identifier. - */ - public static function isNameStart(string $character): bool - { - return $character === '_' || self::isAlphabetic($character) || \ord($character) >= 0x80; - } - - /** - * Returns whether $character is legal in the body of a Sass identifier. - */ - public static function isName(string $character): bool - { - return self::isNameStart($character) || self::isDigit($character) || $character === '-'; - } - - /** - * Returns whether $character is a hexadecimal digit. - */ - public static function isHex(?string $character): bool - { - if ($character === null) { - return false; - } - - if (self::isDigit($character)) { - return true; - } - - $charCode = \ord($character); - - if ($charCode >= \ord('a') && $charCode <= \ord('f')) { - return true; - } - - if ($charCode >= \ord('A') && $charCode <= \ord('F')) { - return true; - } - - return false; - } - - /** - * Returns whether $character can start a simple selector other than a type - * selector. - */ - public static function isSimpleSelectorStart(?string $character): bool - { - return $character === '*' || $character === '[' || $character === '.' || $character === '#' || $character === '%' || $character === ':'; - } - - /** - * Returns whether $identifier is module-private. - * - * Assumes $identifier is a valid Sass identifier. - */ - public static function isPrivate(string $identifier): bool - { - $first = $identifier[0]; - - return $first === '-' || $first === '_'; - } - - /** - * Assumes that $character is a left-hand brace-like character, and returns - * the right-hand version. - */ - public static function opposite(string $character): string - { - switch ($character) { - case '(': - return ')'; - - case '{': - return '}'; - - case '[': - return ']'; - - default: - throw new \InvalidArgumentException(sprintf('Expected a brace character. Got "%s"', $character)); - } - } -} diff --git a/scssphp/src/Util/Equatable.php b/scssphp/src/Util/Equatable.php deleted file mode 100644 index 739c8b1..0000000 --- a/scssphp/src/Util/Equatable.php +++ /dev/null @@ -1,21 +0,0 @@ - $list - */ - public static function listContains(array $list, Equatable $item): bool - { - foreach ($list as $listItem) { - if (!\is_object($listItem)) { - continue; - } - - if ($item === $listItem) { - return true; - } - - if ($item->equals($listItem)) { - return true; - } - } - - return false; - } - - /** - * Checks whether 2 values are equals, using the Equatable semantic to compare objects if possible. - * - * When compared values don't implement {@see Equatable}, they are compared - * using `===`. - * Values implementing {@see Equatable} are still compared with `===` first to - * optimize comparisons to the same object, as an object is always expected to - * be equal to itself. - * - * @param mixed $item1 - * @param mixed $item2 - * - * @return bool - */ - public static function equals($item1, $item2): bool - { - if ($item1 === $item2) { - return true; - } - - if ($item1 instanceof Equatable && $item2 instanceof Equatable) { - return $item1->equals($item2); - } - - return false; - } - - /** - * Checks whether 2 lists are equals, using the Equatable semantic to compare objects if possible. - * - * @param list $list1 - * @param list $list2 - */ - public static function listEquals(array $list1, array $list2): bool - { - if (\count($list1) !== \count($list2)) { - return false; - } - - foreach ($list1 as $i => $item1) { - $item2 = $list2[$i]; - - if (self::equals($item1, $item2)) { - continue; - } - - return false; - } - - return true; - } -} diff --git a/scssphp/src/Util/ErrorUtil.php b/scssphp/src/Util/ErrorUtil.php deleted file mode 100644 index cbccafe..0000000 --- a/scssphp/src/Util/ErrorUtil.php +++ /dev/null @@ -1,62 +0,0 @@ - $maxValue) { - $nameDisplay = $name ? " $name" : ''; - - throw new \OutOfRangeException("Invalid value:$nameDisplay must be between $minValue and $maxValue: $value."); - } - } - - /** - * Check that a range represents a slice of an indexable object. - * - * Throws if the range is not valid for an indexable object with - * the given length. - * A range is valid for an indexable object with a given $length - * if `0 <= $start <= $end <= $length`. - * An `end` of `null` is considered equivalent to `length`. - * - * @throws \OutOfRangeException - */ - public static function checkValidRange(int $start, ?int $end, int $length, ?string $startName = null, ?string $endName = null): void - { - if ($start < 0 || $start > $length) { - $startName = $startName ?? 'start'; - $startNameDisplay = $startName ? " $startName" : ''; - - throw new \OutOfRangeException("Invalid value:$startNameDisplay must be between 0 and $length: $start."); - } - - if ($end !== null) { - - if ($end < $start || $end > $length) { - $endName = $endName ?? 'end'; - $endNameDisplay = $endName ? " $endName" : ''; - - throw new \OutOfRangeException("Invalid value:$endNameDisplay must be between $start and $length: $end."); - } - } - } -} diff --git a/scssphp/src/Util/ListUtil.php b/scssphp/src/Util/ListUtil.php deleted file mode 100644 index 7d78b94..0000000 --- a/scssphp/src/Util/ListUtil.php +++ /dev/null @@ -1,154 +0,0 @@ -> $queues - * - * @return list - */ - public static function flattenVertically(array $queues): array - { - if (\count($queues) === 1) { - return $queues[0]; - } - - $result = []; - - while (!empty($queues)) { - foreach ($queues as $i => &$queue) { - $item = array_shift($queue); - - if ($item === null) { - unset($queues[$i]); - } else { - $result[] = $item; - } - } - unset($queue); - } - - return $result; - } - - /** - * Returns the longest common subsequence between $list1 and $list2. - * - * If there are more than one equally long common subsequence, returns the one - * which starts first in $list1. - * - * If $select is passed, it's used to check equality between elements in each - * list. If it returns `null`, the elements are considered unequal; otherwise, - * it should return the element to include in the return value. - * - * @template T - * - * @param list $list1 - * @param list $list2 - * @param (callable(T, T): (T|null))|null $select - * - * @return list - */ - public static function longestCommonSubsequence(array $list1, array $list2, ?callable $select = null): array - { - if ($select === null) { - $select = function ($element1, $element2) { - return EquatableUtil::equals($element1, $element2) ? $element1 : null; - }; - } - - $lengths = array_fill(0, \count($list1) + 1, array_fill(0, \count($list2) + 1, 0)); - $selections = array_fill(0, \count($list1) + 1, array_fill(0, \count($list2) + 1, null)); - - for ($i = 0; $i < \count($list1); $i++) { - for ($j = 0; $j < \count($list2); $j++) { - $selection = $select($list1[$i], $list2[$j]); - $selections[$i][$j] = $selection; - $lengths[$i + 1][$j + 1] = $selection === null - ? max($lengths[$i + 1][$j], $lengths[$i][$j + 1]) - : $lengths[$i][$j] + 1; - } - } - - $backtrack = function (int $i, int $j) use ($selections, $lengths, &$backtrack): array { - if ($i === -1 || $j === -1) { - return []; - } - - $selection = $selections[$i][$j]; - - if ($selection !== null) { - $selected = $backtrack($i - 1, $j - 1); - $selected[] = $selection; - - return $selected; - } - - return $lengths[$i + 1][$j] > $lengths[$i][$j + 1] - ? $backtrack($i, $j - 1) - : $backtrack($i - 1, $j); - }; - - return $backtrack(\count($list1) - 1, \count($list2) - 1); - } - - /** - * @template T - * - * @param list $list - * - * @return T - */ - public static function last(array $list) - { - $count = count($list); - - if ($count === 0) { - throw new \LogicException('The list may not be empty.'); - } - - return $list[$count - 1]; - } - - /** - * @template T - * - * @param list $list - * - * @return list - */ - public static function exceptLast(array $list): array - { - $count = count($list); - - if ($count === 0) { - throw new \LogicException('The list may not be empty.'); - } - - return array_slice($list, 0, $count - 1); - } -} diff --git a/scssphp/src/Util/NumberUtil.php b/scssphp/src/Util/NumberUtil.php deleted file mode 100644 index 499d642..0000000 --- a/scssphp/src/Util/NumberUtil.php +++ /dev/null @@ -1,187 +0,0 @@ - EPSILON` implies that - * `a` isn't fuzzy-equal to `b`. Note that the inverse implication is not - * necessarily true! For example, if `a = 5.1e-11` and `b = 4.4e-11`, then - * `a - b < 1e-11` but `a` fuzzy-equals 5e-11 and b fuzzy-equals 4e-11. - * - * @see https://github.com/sass/sass/blob/main/spec/types/number.md#fuzzy-equality - */ - private const EPSILON = 10 ** (-SassNumber::PRECISION - 1); - private const INVERSE_EPSILON = 10 ** (SassNumber::PRECISION + 1); - - public static function fuzzyEquals(float $number1, float $number2): bool - { - if ($number1 == $number2) { - return true; - } - return abs($number1 - $number2) <= self::EPSILON && round($number1 * self::INVERSE_EPSILON) === round($number2 * self::INVERSE_EPSILON); - } - - public static function fuzzyLessThan(float $number1, float $number2): bool - { - return $number1 < $number2 && !self::fuzzyEquals($number1, $number2); - } - - public static function fuzzyLessThanOrEquals(float $number1, float $number2): bool - { - return $number1 <= $number2 || self::fuzzyEquals($number1, $number2); - } - - public static function fuzzyGreaterThan(float $number1, float $number2): bool - { - return $number1 > $number2 && !self::fuzzyEquals($number1, $number2); - } - - public static function fuzzyGreaterThanOrEquals(float $number1, float $number2): bool - { - return $number1 >= $number2 || self::fuzzyEquals($number1, $number2); - } - - public static function fuzzyIsInt(float $number): bool - { - if (is_infinite($number) || is_nan($number)) { - return false; - } - - return self::fuzzyEquals($number, round($number)); - } - - public static function fuzzyAsInt(float $number): ?int - { - if (is_infinite($number) || is_nan($number)) { - return null; - } - - $rounded = (int) round($number); - - return self::fuzzyEquals($number, $rounded) ? $rounded : null; - } - - public static function fuzzyRound(float $number): int - { - if ($number > 0) { - return intval(self::fuzzyLessThan(fmod($number, 1), 0.5) ? floor($number) : ceil($number)); - } - - return intval(self::fuzzyLessThanOrEquals(fmod($number, 1), 0.5) ? floor($number) : ceil($number)); - } - - public static function fuzzyCheckRange(float $number, float $min, float $max): ?float - { - if (self::fuzzyEquals($number, $min)) { - return $min; - } - - if (self::fuzzyEquals($number, $max)) { - return $max; - } - - if ($number > $min && $number < $max) { - return $number; - } - - return null; - } - - /** - * @param float $number - * @param float $min - * @param float $max - * @param string|null $name - * - * @return float - * - * @throws \OutOfRangeException - */ - public static function fuzzyAssertRange(float $number, float $min, float $max, ?string $name = null): float - { - $result = self::fuzzyCheckRange($number, $min, $max); - - if (!\is_null($result)) { - return $result; - } - - $nameDisplay = $name ? " $name" : ''; - - throw new \OutOfRangeException("Invalid value:$nameDisplay must be between $min and $max: $number."); - } - - /** - * Returns $num1 / $num2, using Sass's division semantic. - * - * Sass allows dividing by 0. - * - * @param float $num1 - * @param float $num2 - * - * @return float - */ - public static function divideLikeSass(float $num1, float $num2): float - { - if ($num2 == 0) { - if ($num1 == 0) { - return NAN; - } - - if ($num1 > 0) { - return INF; - } - - return -INF; - } - - return $num1 / $num2; - } - - /** - * Return $num1 modulo $num2, using Sass's [floored division] modulo - * semantics, which it inherited from Ruby and which differ from Dart's. - * - * [floored division]: https://en.wikipedia.org/wiki/Modulo_operation#Variants_of_the_definition - */ - public static function moduloLikeSass(float $num1, float $num2): float - { - if ($num2 == 0) { - return NAN; - } - - $result = fmod($num1, $num2); - - if ($result == 0) { - return 0; - } - - // PHP's fdiv has a different semantic when the 2 numbers have a different sign. - if ($num2 < 0 xor $num1 < 0) { - $result += $num2; - } - - return $result; - } -} diff --git a/scssphp/src/Util/ParserUtil.php b/scssphp/src/Util/ParserUtil.php deleted file mode 100644 index d5d0d67..0000000 --- a/scssphp/src/Util/ParserUtil.php +++ /dev/null @@ -1,69 +0,0 @@ -expectChar('\\'); - - $first = $scanner->peekChar(); - - if ($first === null) { - return "\u{FFFD}"; - } - - if (Character::isNewline($first)) { - $scanner->error('Expected escape sequence.'); - } - - if (Character::isHex($first)) { - $value = 0; - for ($i = 0; $i < 6; $i++) { - $next = $scanner->peekChar(); - - if ($next === null || !Character::isHex($next)) { - break; - } - - $value *= 16; - $value += hexdec($scanner->readChar()); - assert(\is_int($value)); - } - - if (Character::isWhitespace($scanner->peekChar())) { - $scanner->readChar(); - } - - if ($value === 0 || ($value >= 0xD800 && $value <= 0xDFFF) || $value >= 0x10FFFF) { - return "\u{FFFD}"; - } - - return Util::mbChr($value); - } - - return $scanner->readUtf8Char(); - } -} diff --git a/scssphp/src/Util/SpanUtil.php b/scssphp/src/Util/SpanUtil.php deleted file mode 100644 index 7d47184..0000000 --- a/scssphp/src/Util/SpanUtil.php +++ /dev/null @@ -1,142 +0,0 @@ -getText(); - $textLength = \strlen($text); - - while ($start < $textLength && Character::isWhitespace($text[$start])) { - $start++; - } - - return $span->subspan($start); - } - - /** - * Returns this span with all trailing whitespace trimmed. - */ - public static function trimRight(FileSpan $span): FileSpan - { - $text = $span->getText(); - $end = \strlen($text) - 1; - - while ($end >= 0 && Character::isWhitespace($text[$end])) { - $end--; - } - - return $span->subspan(0, $end + 1); - } - - /** - * Returns the span of the identifier at the start of this span. - * - * If $includeLeading is greater than 0, that many additional characters - * will be included from the start of this span before looking for an - * identifier. - */ - public static function initialIdentifier(FileSpan $span, int $includeLeading = 0): FileSpan - { - $scanner = new StringScanner($span->getText()); - - for ($i = 0; $i < $includeLeading; $i++) { - $scanner->readUtf8Char(); - } - - self::scanIdentifier($scanner); - - return $span->subspan(0, $scanner->getPosition()); - } - - /** - * Returns a subspan excluding the identifier at the start of this span. - */ - public static function withoutInitialIdentifier(FileSpan $span): FileSpan - { - $scanner = new StringScanner($span->getText()); - self::scanIdentifier($scanner); - - return $span->subspan($scanner->getPosition()); - } - - /** - * Returns a subspan excluding a namespace and `.` at the start of this span. - */ - public static function withoutNamespace(FileSpan $span): FileSpan - { - return self::withoutInitialIdentifier($span)->subspan(1); - } - - /** - * Returns a subspan excluding an initial at-rule and any whitespace after - * it. - */ - public static function withoutInitialAtRule(FileSpan $span): FileSpan - { - $scanner = new StringScanner($span->getText()); - $scanner->expectChar('@'); - self::scanIdentifier($scanner); - - return self::trimLeft($span->subspan($scanner->getPosition())); - } - - /** - * Whether $span contains the $target FileSpan. - * - * Validates the FileSpans to be in the same file and for the $target to be - * within $span FileSpan inclusive range [start,end]. - */ - public static function contains(FileSpan $span, FileSpan $target): bool - { - return $span->getFile() === $target->getFile() && $span->getStart()->getOffset() <= $target->getStart()->getOffset() && $span->getEnd()->getOffset() >= $target->getEnd()->getOffset(); - } - - /** - * Consumes an identifier from $scanner. - */ - private static function scanIdentifier(StringScanner $scanner): void - { - while (!$scanner->isDone()) { - $char = $scanner->peekChar(); - - if ($char === '\\') { - ParserUtil::consumeEscapedCharacter($scanner); - } elseif ($char !== null && Character::isName($char)) { - $scanner->readUtf8Char(); - } else { - break; - } - } - } -} diff --git a/scssphp/src/Util/StringUtil.php b/scssphp/src/Util/StringUtil.php deleted file mode 100644 index ab49bea..0000000 --- a/scssphp/src/Util/StringUtil.php +++ /dev/null @@ -1,130 +0,0 @@ -= 80000) { - return str_starts_with($haystack, $needle); - } - - return '' === $needle || ('' !== $haystack && 0 === substr_compare($haystack, $needle, 0, \strlen($needle))); - } - - /** - * Checks whether $haystack ends with $needle. - * - * This is a userland implementation of `str_ends_with` of PHP 8+. - * - * @param string $haystack - * @param string $needle - * - * @return bool - */ - public static function endsWith(string $haystack, string $needle): bool - { - if (\PHP_VERSION_ID >= 80000) { - return str_ends_with($haystack, $needle); - } - - return '' === $needle || ('' !== $haystack && 0 === substr_compare($haystack, $needle, -\strlen($needle))); - } - - public static function trimAsciiRight(string $string, bool $excludeEscape = false): string - { - $end = self::lastNonWhitespace($string, $excludeEscape); - - if ($end === null) { - return ''; - } - - return substr($string, 0, $end + 1); - } - - /** - * Returns the index of the last character in $string that's not ASCII - * whitespace, or `null` if $string is entirely spaces. - * - * If $excludeEscape is `true`, this doesn't move past whitespace that's - * included in a CSS escape. - */ - private static function lastNonWhitespace(string $string, bool $excludeEscape = false): ?int - { - for ($i = \strlen($string) - 1; $i >= 0; $i--) { - $char = $string[$i]; - - if (!Character::isWhitespace($char)) { - if ($excludeEscape && $i !== 0 && $i !== \strlen($string) && $char === '\\') { - return $i + 1; - } - - return $i; - } - } - - return null; - } - - /** - * Returns whether $string1 and $string2 are equal, ignoring ASCII case. - * - * @param string|null $string1 - * @param string $string2 - * - * @return bool - */ - public static function equalsIgnoreCase(?string $string1, string $string2): bool - { - if ($string1 === $string2) { - return true; - } - - if ($string1 === null) { - return false; - } - - return self::toAsciiLowerCase($string1) === self::toAsciiLowerCase($string2); - } - - /** - * Converts all ASCII chars to lowercase in the input string. - * - * This does not uses `strtolower` because `strtolower` is locale-dependant - * rather than operating on ASCII. - * Passing an input string in an encoding that it is not ASCII compatible is - * unsupported, and will probably generate garbage. - * - * @param string $string - * - * @return string - */ - public static function toAsciiLowerCase(string $string): string - { - return strtr($string, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz'); - } -} diff --git a/scssphp/src/Value/CalculationInterpolation.php b/scssphp/src/Value/CalculationInterpolation.php deleted file mode 100644 index ef58ddf..0000000 --- a/scssphp/src/Value/CalculationInterpolation.php +++ /dev/null @@ -1,45 +0,0 @@ -value = $value; - } - - public function getValue(): string - { - return $this->value; - } - - public function equals(object $other): bool - { - return $other instanceof CalculationInterpolation && $this->value === $other->value; - } -} diff --git a/scssphp/src/Value/CalculationOperation.php b/scssphp/src/Value/CalculationOperation.php deleted file mode 100644 index 496cfd3..0000000 --- a/scssphp/src/Value/CalculationOperation.php +++ /dev/null @@ -1,89 +0,0 @@ -operator = $operator; - $this->left = $left; - $this->right = $right; - } - - /** - * @phpstan-return CalculationOperator::* - */ - public function getOperator(): string - { - return $this->operator; - } - - public function getLeft(): object - { - return $this->left; - } - - public function getRight(): object - { - return $this->right; - } - - public function equals(object $other): bool - { - assert($this->left instanceof Equatable); - assert($this->right instanceof Equatable); - - return $other instanceof CalculationOperation && $this->operator === $other->operator && $this->left->equals($other->left) && $this->right->equals($other->right); - } -} diff --git a/scssphp/src/Value/CalculationOperator.php b/scssphp/src/Value/CalculationOperator.php deleted file mode 100644 index d6c0555..0000000 --- a/scssphp/src/Value/CalculationOperator.php +++ /dev/null @@ -1,48 +0,0 @@ - - * @readonly - */ - private $numeratorUnits; - - /** - * @var list - * @readonly - */ - private $denominatorUnits; - - /** - * @param float $value - * @param list $numeratorUnits - * @param list $denominatorUnits - * @param array{SassNumber, SassNumber}|null $asSlash - */ - public function __construct(float $value, array $numeratorUnits, array $denominatorUnits, array $asSlash = null) - { - assert(\count($numeratorUnits) > 1 || \count($denominatorUnits) > 0); - - parent::__construct($value, $asSlash); - $this->numeratorUnits = $numeratorUnits; - $this->denominatorUnits = $denominatorUnits; - } - - public function getNumeratorUnits(): array - { - return $this->numeratorUnits; - } - - public function getDenominatorUnits(): array - { - return $this->denominatorUnits; - } - - public function hasUnits(): bool - { - return true; - } - - public function hasUnit(string $unit): bool - { - return false; - } - - public function compatibleWithUnit(string $unit): bool - { - return false; - } - - public function hasPossiblyCompatibleUnits(SassNumber $other): bool - { - // This logic is well-defined, and we could implement it in principle. - // However, it would be fairly complex and there's no clear need for it yet. - throw new \BadMethodCallException(__METHOD__ . 'is not implemented.'); - } - - protected function withValue(float $value): SassNumber - { - return new self($value, $this->numeratorUnits, $this->denominatorUnits); - } - - public function withSlash(SassNumber $numerator, SassNumber $denominator): SassNumber - { - return new self($this->getValue(), $this->numeratorUnits, $this->denominatorUnits, array($numerator, $denominator)); - } -} diff --git a/scssphp/src/Value/ListSeparator.php b/scssphp/src/Value/ListSeparator.php deleted file mode 100644 index ea5c301..0000000 --- a/scssphp/src/Value/ListSeparator.php +++ /dev/null @@ -1,24 +0,0 @@ - - * @readonly - */ - private $keywords; - - /** - * @var bool - */ - private $keywordAccessed = false; - - /** - * SassArgumentList constructor. - * - * @param list $contents - * @param array $keywords - * @param string $separator - * - * @phpstan-param ListSeparator::* $separator - */ - public function __construct(array $contents, array $keywords, string $separator) - { - parent::__construct($contents, $separator); - $this->keywords = $keywords; - } - - /** - * @return array - */ - public function getKeywords(): array - { - $this->keywordAccessed = true; - - return $this->keywords; - } - - /** - * @return bool - * - * @internal - */ - public function wereKeywordAccessed(): bool - { - return $this->keywordAccessed; - } -} diff --git a/scssphp/src/Value/SassBoolean.php b/scssphp/src/Value/SassBoolean.php deleted file mode 100644 index b52d685..0000000 --- a/scssphp/src/Value/SassBoolean.php +++ /dev/null @@ -1,93 +0,0 @@ -value = $value; - } - - public function getValue(): bool - { - return $this->value; - } - - public function isTruthy(): bool - { - return $this->value; - } - - public function accept(ValueVisitor $visitor) - { - return $visitor->visitBoolean($this); - } - - public function assertBoolean(?string $name = null): SassBoolean - { - return $this; - } - - public function unaryNot(): Value - { - return self::create(!$this->value); - } - - public function equals(object $other): bool - { - if (!$other instanceof SassBoolean) { - return false; - } - - return $this->value === $other->value; - } -} diff --git a/scssphp/src/Value/SassCalculation.php b/scssphp/src/Value/SassCalculation.php deleted file mode 100644 index f4aae33..0000000 --- a/scssphp/src/Value/SassCalculation.php +++ /dev/null @@ -1,527 +0,0 @@ - - * @readonly - */ - private $arguments; - - /** - * Creates a new calculation with the given [name] and [arguments] - * that will not be simplified. - * - * @param string $name - * @param list $arguments - * - * @return Value - * - * @internal - */ - public static function unsimplified(string $name, array $arguments): Value - { - return new SassCalculation($name, $arguments); - } - - /** - * Creates a `calc()` calculation with the given $argument. - * - * The $argument must be either a {@see SassNumber}, a {@see SassCalculation}, an - * unquoted {@see SassString}, a {@see CalculationOperation}, or a - * {@see CalculationInterpolation}. - * - * This automatically simplifies the calculation, so it may return a - * {@see SassNumber} rather than a {@see SassCalculation}. It throws an exception if it - * can determine that the calculation will definitely produce invalid CSS. - * - * @param object $argument - * - * @return Value - * - * @throws SassScriptException - */ - public static function calc(object $argument): Value - { - $argument = self::simplify($argument); - - if ($argument instanceof SassNumber) { - return $argument; - } - - if ($argument instanceof SassCalculation) { - return $argument; - } - - return new SassCalculation('calc', [$argument]); - } - - /** - * Creates a `min()` calculation with the given $arguments. - * - * Each argument must be either a {@see SassNumber}, a {@see SassCalculation}, an - * unquoted {@see SassString}, a {@see CalculationOperation}, or a - * {@see CalculationInterpolation}. It must be passed at least one argument. - * - * This automatically simplifies the calculation, so it may return a - * {@see SassNumber} rather than a {@see SassCalculation}. It throws an exception if it - * can determine that the calculation will definitely produce invalid CSS. - * - * @param list $arguments - * - * @return Value - * - * @throws SassScriptException - */ - public static function min(array $arguments): Value - { - $args = self::simplifyArguments($arguments); - - if (!$args) { - throw new \InvalidArgumentException('min() must have at least one argument.'); - } - - /** @var SassNumber|null $minimum */ - $minimum = null; - - foreach ($args as $arg) { - if (!$arg instanceof SassNumber || $minimum !== null && !$minimum->isComparableTo($arg)) { - $minimum = null; - break; - } - - if ($minimum === null || $minimum->greaterThan($arg)->isTruthy()) { - $minimum = $arg; - } - } - - if ($minimum !== null) { - return $minimum; - } - - self::verifyCompatibleNumbers($args); - - return new SassCalculation('min', $args); - } - - /** - * Creates a `max()` calculation with the given $arguments. - * - * Each argument must be either a {@see SassNumber}, a {@see SassCalculation}, an - * unquoted {@see SassString}, a {@see CalculationOperation}, or a - * {@see CalculationInterpolation}. It must be passed at least one argument. - * - * This automatically simplifies the calculation, so it may return a - * {@see SassNumber} rather than a {@see SassCalculation}. It throws an exception if it - * can determine that the calculation will definitely produce invalid CSS. - * - * @param list $arguments - * - * @return Value - * - * @throws SassScriptException - */ - public static function max(array $arguments): Value - { - $args = self::simplifyArguments($arguments); - - if (!$args) { - throw new \InvalidArgumentException('max() must have at least one argument.'); - } - - /** @var SassNumber|null $maximum */ - $maximum = null; - - foreach ($args as $arg) { - if (!$arg instanceof SassNumber || $maximum !== null && !$maximum->isComparableTo($arg)) { - $maximum = null; - break; - } - - if ($maximum === null || $maximum->lessThan($arg)->isTruthy()) { - $maximum = $arg; - } - } - - if ($maximum !== null) { - return $maximum; - } - - self::verifyCompatibleNumbers($args); - - return new SassCalculation('max', $args); - } - - /** - * Creates a `clamp()` calculation with the given $min, $value, and $max. - * - * Each argument must be either a {@see SassNumber}, a {@see SassCalculation}, an - * unquoted {@see SassString}, a {@see CalculationOperation}, or a - * {@see CalculationInterpolation}. - * - * This automatically simplifies the calculation, so it may return a - * {@see SassNumber} rather than a {@see SassCalculation}. It throws an exception if it - * can determine that the calculation will definitely produce invalid CSS. - * - * This may be passed fewer than three arguments, but only if one of the - * arguments is an unquoted `var()` string. - * - * @param object $min - * @param object|null $value - * @param object|null $max - * - * @return Value - * - * @throws SassScriptException - */ - public static function clamp(object $min, object $value = null, object $max = null): Value - { - if ($value === null && $max !== null) { - throw new \InvalidArgumentException('If value is null, max must also be null.'); - } - - $min = self::simplify($min); - - if ($value !== null) { - $value = self::simplify($value); - } - - if ($max !== null) { - $max = self::simplify($max); - } - - if ($min instanceof SassNumber && $value instanceof SassNumber && $max instanceof SassNumber && $min->hasCompatibleUnits($value) && $min->hasCompatibleUnits($max)) { - if ($value->lessThanOrEquals($min)->isTruthy()) { - return $min; - } - - if ($value->greaterThanOrEquals($max)->isTruthy()) { - return $max; - } - - return $value; - } - - $args = array_filter([$min, $value, $max]); - self::verifyCompatibleNumbers($args); - self::verifyLength($args, 3); - - return new SassCalculation('clamp', $args); - } - - /** - * Creates and simplifies a {@see CalculationOperation} with the given $operator, - * $left, and $right. - * - * This automatically simplifies the operation, so it may return a - * {@see SassNumber} rather than a {@see CalculationOperation}. - * - * Each of $left and $right must be either a {@see SassNumber}, a - * {@see SassCalculation}, an unquoted {@see SassString}, a {@see CalculationOperation}, or - * a {@see CalculationInterpolation}. - * - * @param string $operator - * @param object $left - * @param object $right - * - * @return object - * - * @phpstan-param CalculationOperator::* $operator - * - * @throws SassScriptException - */ - public static function operate(string $operator, object $left, object $right): object - { - return self::operateInternal($operator, $left, $right, false, true); - } - - /** - * Like {@see operate}, but with the internal-only $inMinMax parameter. - * - * If $inMinMax is `true`, this allows unitless numbers to be added and - * subtracted with numbers with units, for backwards-compatibility with the - * old global `min()` and `max()` functions. - * - * If $simplify is `false`, no simplification will be done. - * - * @param string $operator - * @param object $left - * @param object $right - * @param bool $inMinMax - * @param bool $simplify - * - * @return object - * - * @throws SassScriptException - * - * @phpstan-param CalculationOperator::* $operator - * - * @internal - */ - public static function operateInternal(string $operator, object $left, object $right, bool $inMinMax, bool $simplify): object - { - if (!$simplify) { - return new CalculationOperation($operator, $left, $right); - } - - $left = self::simplify($left); - $right = self::simplify($right); - - if ($operator === CalculationOperator::PLUS || $operator === CalculationOperator::MINUS) { - if ($left instanceof SassNumber && $right instanceof SassNumber && ($inMinMax ? $left->isComparableTo($right) : $left->hasCompatibleUnits($right))) { - return $operator === CalculationOperator::PLUS ? $left->plus($right) : $left->minus($right); - } - - self::verifyCompatibleNumbers([$left, $right]); - - if ($right instanceof SassNumber && NumberUtil::fuzzyLessThan($right->getValue(), 0)) { - $right = $right->times(SassNumber::create(-1)); - $operator = $operator === CalculationOperator::PLUS ? CalculationOperator::MINUS : CalculationOperator::PLUS; - } - - return new CalculationOperation($operator, $left, $right); - } - - if ($left instanceof SassNumber && $right instanceof SassNumber) { - return $operator === CalculationOperator::TIMES ? $left->times($right) : $left->dividedBy($right); - } - - return new CalculationOperation($operator, $left, $right); - } - - /** - * An internal constructor that doesn't perform any validation or - * simplification. - * - * @param string $name - * @param list $arguments - */ - private function __construct(string $name, array $arguments) - { - $this->name = $name; - $this->arguments = $arguments; - } - - public function getName(): string - { - return $this->name; - } - - public function isSpecialNumber(): bool - { - return true; - } - - /** - * @return list - */ - public function getArguments(): array - { - return $this->arguments; - } - - public function accept(ValueVisitor $visitor) - { - return $visitor->visitCalculation($this); - } - - public function assertCalculation(?string $name = null): SassCalculation - { - return $this; - } - - public function plus(Value $other): Value - { - if ($other instanceof SassString) { - return parent::plus($other); - } - - throw new SassScriptException("Undefined operation \"$this + $other\"."); - } - - public function minus(Value $other): Value - { - throw new SassScriptException("Undefined operation \"$this - $other\"."); - } - - public function unaryPlus(): Value - { - throw new SassScriptException("Undefined operation \"+$this\"."); - } - - public function unaryMinus(): Value - { - throw new SassScriptException("Undefined operation \"-$this\"."); - } - - public function equals(object $other): bool - { - if (!$other instanceof SassCalculation || $this->name !== $other->name) { - return false; - } - - if (\count($this->arguments) !== \count($other->arguments)) { - return false; - } - - foreach ($this->arguments as $i => $argument) { - assert($argument instanceof Equatable); - $otherArgument = $other->arguments[$i]; - - if (!$argument->equals($otherArgument)) { - return false; - } - } - - return true; - } - - /** - * @param list $args - * - * @return list - * - * @throws SassScriptException - */ - private static function simplifyArguments(array $args): array - { - return array_map([self::class, 'simplify'], $args); - } - - /** - * @throws SassScriptException - */ - private static function simplify(object $arg): object - { - if ($arg instanceof SassNumber || $arg instanceof CalculationInterpolation || $arg instanceof CalculationOperation) { - return $arg; - } - - if ($arg instanceof SassString) { - if (!$arg->hasQuotes()) { - return $arg; - } - - throw new SassScriptException("Quoted string $arg can't be used in a calculation."); - } - - if ($arg instanceof SassCalculation) { - return $arg->getName() === 'calc' ? $arg->getArguments()[0] : $arg; - } - - if ($arg instanceof Value) { - throw new SassScriptException("Value $arg can't be used in a calculation."); - } - - throw new \InvalidArgumentException(sprintf('Unexpected calculation argument %s.', get_class($arg))); - } - - /** - * Verifies that all the numbers in $args aren't known to be incompatible - * with one another, and that they don't have units that are too complex for - * calculations. - * - * @param list $args - * - * @throws SassScriptException - */ - private static function verifyCompatibleNumbers(array $args): void - { - foreach ($args as $arg) { - if (!$arg instanceof SassNumber) { - continue; - } - - if (\count($arg->getNumeratorUnits()) > 1 || \count($arg->getDenominatorUnits())) { - throw new SassScriptException("Number $arg isn't compatible with CSS calculations."); - } - } - - for ($i = 0; $i < \count($args); $i++) { - $number1 = $args[$i]; - - if (!$number1 instanceof SassNumber) { - continue; - } - - for ($j = $i + 1; $j < \count($args); $j++) { - $number2 = $args[$j]; - - if (!$number2 instanceof SassNumber) { - continue; - } - - if ($number1->hasPossiblyCompatibleUnits($number2)) { - continue; - } - - throw new SassScriptException("$number1 and $number2 are incompatible."); - } - } - } - - /** - * Throws a {@see SassScriptException} if $args isn't $expectedLength *and* - * doesn't contain either a {@see SassString} or a {@see CalculationInterpolation}. - * - * @param list $args - * @param int $expectedLength - * - * @throws SassScriptException - */ - private static function verifyLength(array $args, int $expectedLength): void - { - if (\count($args) === $expectedLength) { - return; - } - - foreach ($args as $arg) { - if ($arg instanceof SassString || $arg instanceof CalculationInterpolation) { - return; - } - } - - $length = \count($args); - $verb = $length === 1 ? 'was' : 'were'; - - throw new SassScriptException("$expectedLength arguments required, but only $length $verb passed."); - } -} diff --git a/scssphp/src/Value/SassColor.php b/scssphp/src/Value/SassColor.php deleted file mode 100644 index 27324c6..0000000 --- a/scssphp/src/Value/SassColor.php +++ /dev/null @@ -1,515 +0,0 @@ - 1) { - $scaledWhiteness /= $sum; - $scaledBlackness /= $sum; - } - - $factor = 1 - $scaledWhiteness - $scaledBlackness; - - $toRgb = function (float $hue) use ($factor, $scaledWhiteness) { - $channel = self::hueToRgb(0, 1, $hue) * $factor + $scaledWhiteness; - - return NumberUtil::fuzzyRound($channel * 255); - }; - - return self::rgb($toRgb($scaledHue + 1/3), $toRgb($scaledHue), $toRgb($scaledHue - 1/3), $alpha); - } - - /** - * This must always provide non-null values for either RGB or HSL values. - * If they are all provided, they are expected to be in sync and this not - * revalidated. This constructor does not revalidate ranges either. - * Use named factories when this cannot be guaranteed. - * - * @param int|null $red - * @param int|null $green - * @param int|null $blue - * @param float|null $hue - * @param float|null $saturation - * @param float|null $lightness - * @param float $alpha - * @param SpanColorFormat|string|null $format - * - * @phpstan-param SpanColorFormat|ColorFormat::*|null $format - */ - private function __construct(?int $red, ?int $green, ?int $blue, ?float $hue, ?float $saturation, ?float $lightness, float $alpha, $format = null) - { - $this->red = $red; - $this->green = $green; - $this->blue = $blue; - $this->hue = $hue; - $this->saturation = $saturation; - $this->lightness = $lightness; - $this->alpha = $alpha; - $this->format = $format; - } - - public function getRed(): int - { - if (\is_null($this->red)) { - $this->hslToRgb(); - assert(!\is_null($this->red)); - } - - return $this->red; - } - - public function getGreen(): int - { - if (\is_null($this->green)) { - $this->hslToRgb(); - assert(!\is_null($this->green)); - } - - return $this->green; - } - - public function getBlue(): int - { - if (\is_null($this->blue)) { - $this->hslToRgb(); - assert(!\is_null($this->blue)); - } - - return $this->blue; - } - - public function getHue(): float - { - if (\is_null($this->hue)) { - $this->rgbToHsl(); - assert(!\is_null($this->hue)); - } - - return $this->hue; - } - - public function getSaturation(): float - { - if (\is_null($this->saturation)) { - $this->rgbToHsl(); - assert(!\is_null($this->saturation)); - } - - return $this->saturation; - } - - public function getLightness(): float - { - if (\is_null($this->lightness)) { - $this->rgbToHsl(); - assert(!\is_null($this->lightness)); - } - - return $this->lightness; - } - - public function getWhiteness(): float - { - return min($this->getRed(), $this->getGreen(), $this->getBlue()) / 255 * 100; - } - - public function getBlackness(): float - { - return 100 - max($this->getRed(), $this->getGreen(), $this->getBlue()) / 255 * 100; - } - - public function getAlpha(): float - { - return $this->alpha; - } - - /** - * The format in which this color was originally written and should be - * serialized in expanded mode, or `null` if the color wasn't written in a - * supported format. - * - * @internal - * - * @return SpanColorFormat|string|null - * @phpstan-return SpanColorFormat|ColorFormat::*|null - */ - public function getFormat() - { - return $this->format; - } - - public function accept(ValueVisitor $visitor) - { - return $visitor->visitColor($this); - } - - public function assertColor(?string $name = null): SassColor - { - return $this; - } - - /** - * @param int|null $red - * @param int|null $green - * @param int|null $blue - * @param float|null $alpha - * - * @return SassColor - */ - public function changeRgb(?int $red = null, ?int $green = null, ?int $blue = null, ?float $alpha = null): SassColor - { - return self::rgb($red ?? $this->getRed(), $green ?? $this->getGreen(), $blue ?? $this->getBlue(), $alpha ?? $this->alpha); - } - - /** - * @param float|null $hue - * @param float|null $saturation - * @param float|null $lightness - * @param float|null $alpha - * - * @return SassColor - */ - public function changeHsl(?float $hue = null, ?float $saturation = null, ?float $lightness = null, ?float $alpha = null): SassColor - { - return self::hsl($hue ?? $this->getHue(), $saturation ?? $this->getSaturation(), $lightness ?? $this->getLightness(), $alpha ?? $this->alpha); - } - - /** - * @param float|null $hue - * @param float|null $whiteness - * @param float|null $blackness - * @param float|null $alpha - * - * @return SassColor - */ - public function changeHwb(?float $hue = null, ?float $whiteness = null, ?float $blackness = null, ?float $alpha = null): SassColor - { - return self::hwb($hue ?? $this->getHue(), $whiteness ?? $this->getWhiteness(), $blackness ?? $this->getBlackness(), $alpha ?? $this->alpha); - } - - /** - * @param float $alpha - * - * @return SassColor - */ - public function changeAlpha(float $alpha): SassColor - { - return new self( - $this->red, - $this->green, - $this->blue, - $this->hue, - $this->saturation, - $this->lightness, - NumberUtil::fuzzyAssertRange($alpha, 0, 1, 'alpha') - ); - } - - public function plus(Value $other): Value - { - if (!$other instanceof SassColor && !$other instanceof SassNumber) { - return parent::plus($other); - } - - throw new SassScriptException("Undefined operation \"$this + $other\"."); - } - - public function minus(Value $other): Value - { - if (!$other instanceof SassColor && !$other instanceof SassNumber) { - return parent::minus($other); - } - - throw new SassScriptException("Undefined operation \"$this - $other\"."); - } - - public function dividedBy(Value $other): Value - { - if (!$other instanceof SassColor && !$other instanceof SassNumber) { - return parent::dividedBy($other); - } - - throw new SassScriptException("Undefined operation \"$this / $other\"."); - } - - public function modulo(Value $other): Value - { - if (!$other instanceof SassColor && !$other instanceof SassNumber) { - return parent::modulo($other); - } - - throw new SassScriptException("Undefined operation \"$this % $other\"."); - } - - public function equals(object $other): bool - { - return $other instanceof SassColor && $this->getRed() === $other->getRed() && $this->getGreen() === $other->getGreen() && $this->getBlue() === $other->getBlue() && $this->alpha === $other->alpha; - } - - /** - * @return void - */ - private function rgbToHsl(): void - { - $scaledRed = $this->getRed() / 255; - $scaledGreen = $this->getGreen() / 255; - $scaledBlue = $this->getBlue() / 255; - - $min = min($scaledRed, $scaledGreen, $scaledBlue); - $max = max($scaledRed, $scaledGreen, $scaledBlue); - $delta = $max - $min; - - if ($delta == 0) { - $this->hue = 0; - } elseif ($max == $scaledRed) { - $this->hue = fmod(60 * ($scaledGreen - $scaledBlue) / $delta, 360); - } elseif ($max == $scaledGreen) { - $this->hue = fmod(120 + 60 * ($scaledBlue - $scaledRed) / $delta, 360); - } else { - $this->hue = fmod(240 + 60 * ($scaledRed - $scaledGreen) / $delta, 360); - } - - $this->lightness = 50 * ($max + $min); - - if ($max == $min) { - $this->saturation = 50; - } elseif ($this->lightness < 50) { - $this->saturation = 100 * $delta / ($max + $min); - } else { - $this->saturation = 100 * $delta / (2 - $max - $min); - } - } - - /** - * @return void - */ - private function hslToRgb(): void - { - $scaledHue = $this->getHue() / 360; - $scaledSaturation = $this->getSaturation() / 100; - $scaledLightness = $this->getLightness() / 100; - - if ($scaledLightness <= 0.5) { - $m2 = $scaledLightness * ($scaledSaturation + 1); - } else { - $m2 = $scaledLightness + $scaledSaturation - $scaledLightness * $scaledSaturation; - } - - $m1 = $scaledLightness * 2 - $m2; - - $this->red = NumberUtil::fuzzyRound(self::hueToRgb($m1, $m2, $scaledHue + 1 / 3) * 255); - $this->green = NumberUtil::fuzzyRound(self::hueToRgb($m1, $m2, $scaledHue) * 255); - $this->blue = NumberUtil::fuzzyRound(self::hueToRgb($m1, $m2, $scaledHue - 1 / 3) * 255); - } - - private static function hueToRgb(float $m1, float $m2, float $hue): float - { - if ($hue < 0) { - $hue += 1; - } elseif ($hue > 1) { - $hue -= 1; - } - - if ($hue < 1 / 6) { - return $m1 + ($m2 - $m1) * $hue * 6; - } - - if ($hue < 1 / 2) { - return $m2; - } - - if ($hue < 2 / 3) { - return $m1 + ($m2 - $m1) * (2 / 3 - $hue) * 6; - } - - return $m1; - } -} diff --git a/scssphp/src/Value/SassFunction.php b/scssphp/src/Value/SassFunction.php deleted file mode 100644 index 88ab353..0000000 --- a/scssphp/src/Value/SassFunction.php +++ /dev/null @@ -1,58 +0,0 @@ -name = $name; - } - - /** - * @internal - */ - public function getName(): string - { - return $this->name; - } - - public function accept(ValueVisitor $visitor) - { - return $visitor->visitFunction($this); - } - - public function assertFunction(?string $name = null): SassFunction - { - return $this; - } - - public function equals(object $other): bool - { - return $other instanceof SassFunction && $this->name === $other->name; - } -} diff --git a/scssphp/src/Value/SassList.php b/scssphp/src/Value/SassList.php deleted file mode 100644 index 74c05b6..0000000 --- a/scssphp/src/Value/SassList.php +++ /dev/null @@ -1,159 +0,0 @@ - - * @readonly - */ - private $contents; - - /** - * @var string - * @phpstan-var ListSeparator::* - * @readonly - */ - private $separator; - - /** - * @var bool - * @readonly - */ - private $brackets; - - /** - * @param string $separator - * @param bool $brackets - * - * @return SassList - * - * @phpstan-param ListSeparator::* $separator - */ - public static function createEmpty(string $separator = ListSeparator::UNDECIDED, bool $brackets = false): SassList - { - return new self(array(), $separator, $brackets); - } - - /** - * @param list $contents - * @param string $separator - * @param bool $brackets - * - * @phpstan-param ListSeparator::* $separator - */ - public function __construct(array $contents, string $separator, bool $brackets = false) - { - if ($separator === ListSeparator::UNDECIDED && count($contents) > 1) { - throw new \InvalidArgumentException('A list with more than one element must have an explicit separator.'); - } - - $this->contents = $contents; - $this->separator = $separator; - $this->brackets = $brackets; - } - - public function getSeparator(): string - { - return $this->separator; - } - - public function hasBrackets(): bool - { - return $this->brackets; - } - - public function isBlank(): bool - { - if ($this->brackets) { - return false; - } - - foreach ($this->contents as $element) { - if (!$element->isBlank()) { - return false; - } - } - - return true; - } - - public function asList(): array - { - return $this->contents; - } - - protected function getLengthAsList(): int - { - return \count($this->contents); - } - - public function accept(ValueVisitor $visitor) - { - return $visitor->visitList($this); - } - - public function assertMap(?string $name = null): SassMap - { - if (\count($this->contents) === 0) { - return SassMap::createEmpty(); - } - - return parent::assertMap($name); - } - - public function tryMap(): ?SassMap - { - if (\count($this->contents) === 0) { - return SassMap::createEmpty(); - } - - return null; - } - - public function equals(object $other): bool - { - if ($other instanceof SassMap) { - return \count($this->contents) === 0 && \count($other->asList()) === 0; - } - - if (!$other instanceof SassList) { - return false; - } - - if ($this->separator !== $other->separator || $this->brackets !== $other->brackets) { - return false; - } - - $otherContent = $other->contents; - $length = \count($this->contents); - - if ($length !== \count($otherContent)) { - return false; - } - - for ($i = 0; $i < $length; ++$i) { - if (!$this->contents[$i]->equals($otherContent[$i])) { - return false; - } - } - - return true; - } -} diff --git a/scssphp/src/Value/SassMap.php b/scssphp/src/Value/SassMap.php deleted file mode 100644 index bcf04df..0000000 --- a/scssphp/src/Value/SassMap.php +++ /dev/null @@ -1,130 +0,0 @@ - - * @readonly - */ - private $contents; - - /** - * @param Map $contents - */ - private function __construct(Map $contents) - { - $this->contents = Map::unmodifiable($contents); - } - - public static function createEmpty(): SassMap - { - return new self(new Map()); - } - - /** - * @param Map $contents - * - * @return SassMap - */ - public static function create(Map $contents): SassMap - { - return new self($contents); - } - - /** - * The returned Map is unmodifiable. - * - * @return Map - */ - public function getContents(): Map - { - return $this->contents; - } - - public function getSeparator(): string - { - return \count($this->contents) === 0 ? ListSeparator::UNDECIDED : ListSeparator::COMMA; - } - - public function asList(): array - { - $result = []; - - foreach ($this->contents as $key => $value) { - $result[] = new SassList([$key, $value], ListSeparator::SPACE); - } - - return $result; - } - - protected function getLengthAsList(): int - { - return \count($this->contents); - } - - public function accept(ValueVisitor $visitor) - { - return $visitor->visitMap($this); - } - - public function assertMap(?string $name = null): SassMap - { - return $this; - } - - public function tryMap(): ?SassMap - { - return $this; - } - - public function equals(object $other): bool - { - if ($other instanceof SassList) { - return \count($this->contents) === 0 && \count($other->asList()) === 0; - } - - if (!$other instanceof SassMap) { - return false; - } - - if ($this->contents === $other->contents) { - return true; - } - - if (\count($this->contents) !== \count($other->contents)) { - return false; - } - - foreach ($this->contents as $key => $value) { - $otherValue = $other->contents->get($key); - - if ($otherValue === null) { - return false; - } - - if (!$value->equals($otherValue)) { - return false; - } - } - - return true; - } -} diff --git a/scssphp/src/Value/SassNull.php b/scssphp/src/Value/SassNull.php deleted file mode 100644 index 9e44f5a..0000000 --- a/scssphp/src/Value/SassNull.php +++ /dev/null @@ -1,64 +0,0 @@ -visitNull(); - } - - public function equals(object $other): bool - { - return $other instanceof SassNull; - } - - public function unaryNot(): Value - { - return SassBoolean::create(true); - } -} diff --git a/scssphp/src/Value/SassNumber.php b/scssphp/src/Value/SassNumber.php deleted file mode 100644 index 1d9ac7a..0000000 --- a/scssphp/src/Value/SassNumber.php +++ /dev/null @@ -1,1107 +0,0 @@ - [ - 'in' => 1.0, - 'pc' => 6.0, - 'pt' => 72.0, - 'px' => 96.0, - 'cm' => 2.54, - 'mm' => 25.4, - 'q' => 101.6, - ], - 'deg' => [ - 'deg' => 360.0, - 'grad' => 400.0, - 'rad' => 2 * M_PI, - 'turn' => 1.0, - ], - 's' => [ - 's' => 1.0, - 'ms' => 1000.0, - ], - 'Hz' => [ - 'Hz' => 1.0, - 'kHz' => 0.001, - ], - 'dpi' => [ - 'dpi' => 1.0, - 'dpcm' => 1 / 2.54, - 'dppx' => 1 / 96, - ], - ]; - - /** - * A map from human-readable names of unit types to the convertable units that - * fall into those types. - */ - private const UNITS_BY_TYPE = [ - 'length' => ['in', 'cm', 'pc', 'mm', 'q', 'pt', 'px'], - 'angle' => ['deg', 'grad', 'rad', 'turn'], - 'time' => ['s', 'ms'], - 'frequency' => ['Hz', 'kHz'], - 'pixel density' => ['dpi', 'dpcm', 'dppx'] - ]; - - /** - * A map from units to the human-readable names of those unit types. - */ - private const TYPES_BY_UNIT = [ - 'in' => 'length', - 'cm' => 'length', - 'pc' => 'length', - 'mm' => 'length', - 'q' => 'length', - 'pt' => 'length', - 'px' => 'length', - 'deg' => 'angle', - 'grad' => 'angle', - 'rad' => 'angle', - 'turn' => 'angle', - 's' => 'time', - 'ms' => 'time', - 'Hz' => 'frequency', - 'kHz' => 'frequency', - 'dpi' => 'pixel density', - 'dpcm' => 'pixel density', - 'dppx' => 'pixel density', - ]; - - /** - * @var float - * @readonly - */ - private $value; - - /** - * The representation of this number as two slash-separated numbers, if it has one. - * - * @var array{SassNumber, SassNumber}|null - * @readonly - * @internal - */ - private $asSlash; - - /** - * @param float $value - * @param array{SassNumber, SassNumber}|null $asSlash - */ - protected function __construct(float $value, array $asSlash = null) - { - $this->value = $value; - $this->asSlash = $asSlash; - } - - /** - * Creates a number, optionally with a single numerator unit. - * - * This matches the numbers that can be written as literals. - * {@see SassNumber::withUnits} can be used to construct more complex units. - * - * @param float $value - * @param string|null $unit - * - * @return self - */ - final public static function create(float $value, ?string $unit = null): SassNumber - { - if ($unit === null) { - return new UnitlessSassNumber($value); - } - - return new SingleUnitSassNumber($value, $unit); - } - - /** - * Creates a number with full $numeratorUnits and $denominatorUnits. - * - * @param float $value - * @param list $numeratorUnits - * @param list $denominatorUnits - * - * @return self - */ - final public static function withUnits(float $value, array $numeratorUnits = [], array $denominatorUnits = []): SassNumber - { - if (empty($numeratorUnits) && empty($denominatorUnits)) { - return new UnitlessSassNumber($value); - } - - if (empty($denominatorUnits) && \count($numeratorUnits) === 1) { - return new SingleUnitSassNumber($value, $numeratorUnits[0]); - } - - if (empty($numeratorUnits)) { - return new ComplexSassNumber($value, $numeratorUnits, $denominatorUnits); - } - - $numerators = $numeratorUnits; - $unsimplifiedDenominators = $denominatorUnits; - $denominators = []; - - foreach ($unsimplifiedDenominators as $denominator) { - $simplifiedAway = false; - - foreach ($numerators as $i => $numerator) { - $factor = self::getConversionFactor($denominator, $numerator); - - if ($factor === null) { - continue; - } - - $value *= $factor; - unset($numerators[$i]); - $simplifiedAway = true; - break; - } - - if (!$simplifiedAway) { - $denominators[] = $denominator; - } - } - - $numerators = array_values($numerators); - - if (empty($denominators)) { - if (empty($numerators)) { - return new UnitlessSassNumber($value); - } - - if (\count($numerators) === 1) { - return new SingleUnitSassNumber($value, $numerators[0]); - } - } - - return new ComplexSassNumber($value, $numerators, $denominators); - } - - /** - * The value of this number. - * - * Note that due to details of floating-point arithmetic, this may be a - * float even if $this represents an int from Sass's perspective. Use - * {@see isInt} to determine whether this is an integer, {@see asInt} to get its - * integer value, or {@see assertInt} to do both at once. - */ - public function getValue(): float - { - return $this->value; - } - - /** - * @return list - */ - abstract public function getNumeratorUnits(): array; - - /** - * @return list - */ - abstract public function getDenominatorUnits(): array; - - /** - * @return array{SassNumber, SassNumber}|null - * - * @internal - */ - final public function getAsSlash(): ?array - { - return $this->asSlash; - } - - public function accept(ValueVisitor $visitor) - { - return $visitor->visitNumber($this); - } - - /** - * Returns a SassNumber with this value and the same units. - * - * @param float $value - * - * @return self - */ - abstract protected function withValue(float $value): SassNumber; - - /** - * @param SassNumber $numerator - * @param SassNumber $denominator - * - * @return SassNumber - * - * @internal - */ - abstract public function withSlash(SassNumber $numerator, SassNumber $denominator): SassNumber; - - public function withoutSlash(): Value - { - if ($this->asSlash === null) { - return $this; - } - - return $this->withValue($this->value); - } - - public function assertNumber(?string $name = null): SassNumber - { - return $this; - } - - /** - * Returns a human-readable string representation of this number's units. - */ - public function getUnitString(): string - { - return $this->hasUnits() ? self::buildUnitString($this->getNumeratorUnits(), $this->getDenominatorUnits()): ''; - } - - /** - * Whether $this is an integer, according to {@see NumberUtil::fuzzyEquals}. - * - * The int value can be accessed using {@see asInt} or {@see assertInt}. Note that - * this may return `false` for very large doubles even though they may be - * mathematically integers, because not all platforms have a valid - * representation for integers that large. - */ - public function isInt(): bool - { - return NumberUtil::fuzzyIsInt($this->value); - } - - /** - * If $this is an integer according to {@see isInt}, returns {@see value} as an int. - * - * Otherwise, returns `null`. - */ - public function asInt(): ?int - { - return NumberUtil::fuzzyAsInt($this->value); - } - - /** - * Returns the value as an int, if it's an integer value according to - * {@see isInt}. - * - * @throws SassScriptException if the value isn't an integer. If this came - * from a function argument, $name is the argument name (without the `$`). - * It's used for error reporting. - */ - public function assertInt(?string $name = null): int - { - $integer = NumberUtil::fuzzyAsInt($this->value); - - if ($integer !== null) { - return $integer; - } - - throw SassScriptException::forArgument("$this is not an int.", $name); - } - - /** - * If {@see value} is between $min and $max, returns it. - * - * If {@see value} is {@see NumberUtil::fuzzyEquals} to $min or $max, it's clamped to the - * appropriate value. Otherwise, this throws a {@see SassScriptException}. If this - * came from a function argument, $name is the argument name (without the - * `$`). It's used for error reporting. - * - * @param float $min - * @param float $max - * @param string|null $name - * - * @return float - * - * @throws SassScriptException if the value is outside the range - */ - public function valueInRange(float $min, float $max, ?string $name = null): float - { - $result = NumberUtil::fuzzyCheckRange($this->value, $min, $max); - - if ($result !== null) { - return $result; - } - - $unitString = $this->getUnitString(); - - throw SassScriptException::forArgument("Expected $this to be within $min$unitString and $max$unitString.", $name); - } - - /** - * Like {@see valueInRange}, but with an explicit unit for the expected upper and - * lower bounds. - * - * This exists to solve the confusing error message in https://github.com/sass/dart-sass/issues/1745, - * and should be removed once https://github.com/sass/sass/issues/3374 fully lands and unitless values - * are required in these positions. - * - * @param float $min - * @param float $max - * @param string $name - * @param string $unit - * - * @return float - * - * @throws SassScriptException if the value is outside the range - * - * @internal - */ - public function valueInRangeWithUnit(float $min, float $max, string $name, string $unit): float - { - $result = NumberUtil::fuzzyCheckRange($this->value, $min, $max); - - if ($result !== null) { - return $result; - } - - throw SassScriptException::forArgument("Expected $this to be within $min$unit and $max$unit.", $name); - } - - /** - * Returns true if the number has units. - * - * @return boolean - */ - abstract public function hasUnits(): bool; - - /** - * Returns whether $this has $unit as its only unit (and as a numerator). - * - * @param string $unit - * - * @return bool - */ - abstract public function hasUnit(string $unit): bool; - - /** - * Returns whether $this has units that are compatible with $other. - * - * Unlike {@see isComparableTo}, unitless numbers are only considered compatible - * with other unitless numbers. - */ - public function hasCompatibleUnits(SassNumber $other): bool - { - if (\count($this->getNumeratorUnits()) !== \count($other->getNumeratorUnits())) { - return false; - } - - if (\count($this->getDenominatorUnits()) !== \count($other->getDenominatorUnits())) { - return false; - } - - return $this->isComparableTo($other); - } - - /** - * Returns whether $this has units that are possibly-compatible with - * $other, as defined by the Sass spec. - * - * @internal - */ - abstract public function hasPossiblyCompatibleUnits(SassNumber $other): bool; - - /** - * Returns whether $this can be coerced to the given unit. - * - * This always returns `true` for a unitless number. - * - * @param string $unit - * - * @return bool - */ - abstract public function compatibleWithUnit(string $unit): bool; - - /** - * Throws a SassScriptException unless $this has $unit as its only unit - * (and as a numerator). - * - * If this came from a function argument, $name is the argument name - * (without the `$`). It's used for error reporting. - * - * @throws SassScriptException - */ - public function assertUnit(string $unit, ?string $varName = null): void - { - if ($this->hasUnit($unit)) { - return; - } - - throw SassScriptException::forArgument(sprintf('Expected %s to have unit "%s".', $this, $unit), $varName); - } - - /** - * Throws a SassScriptException unless $this has no units. - * - * If this came from a function argument, $name is the argument name - * (without the `$`). It's used for error reporting. - * - * @throws SassScriptException - */ - public function assertNoUnits(?string $varName = null): void - { - if (!$this->hasUnits()) { - return; - } - - throw SassScriptException::forArgument(sprintf('Expected %s to have no units.', $this), $varName); - } - - /** - * Returns a copy of this number, converted to the units represented by $newNumeratorUnits and $newDenominatorUnits. - * - * Note that {@see convertValue} is generally more efficient if the value - * is going to be accessed directly. - * - * @param list $newNumeratorUnits - * @param list $newDenominatorUnits - * @param string|null $name The argument name if this is a function argument - * - * @return SassNumber - * - * @throws SassScriptException if this number's units are not compatible with $newNumeratorUnits and $newDenominatorUnits, or if either number is unitless but the other is not. - */ - public function convert(array $newNumeratorUnits, array $newDenominatorUnits, ?string $name = null): SassNumber - { - return self::withUnits($this->convertValue($newNumeratorUnits, $newDenominatorUnits, $name), $newNumeratorUnits, $newDenominatorUnits); - } - - /** - * Returns {@see value}, converted to the units represented by $newNumeratorUnits and $newDenominatorUnits. - * - * @param list $newNumeratorUnits - * @param list $newDenominatorUnits - * @param string|null $name The argument name if this is a function argument - * - * @return float - * - * @throws SassScriptException if this number's units are not compatible with $newNumeratorUnits and $newDenominatorUnits, or if either number is unitless but the other is not. - */ - public function convertValue(array $newNumeratorUnits, array $newDenominatorUnits, ?string $name = null): float - { - return $this->convertOrCoerceValue($newNumeratorUnits, $newDenominatorUnits, false, $name); - } - - /** - * Returns a copy of this number, converted to the same units as $other. - * - * Note that {@see convertValueToMatch} is generally more efficient if the value - * is going to be accessed directly. - * - * @param SassNumber $other - * @param string|null $name The argument name if this is a function argument - * @param string|null $otherName The argument name for $other if this is a function argument - * - * @return SassNumber - * - * @throws SassScriptException if the units are not compatible or if either number is unitless but the other is not. - */ - public function convertToMatch(SassNumber $other, ?string $name = null, ?string $otherName = null): SassNumber - { - return self::withUnits($this->convertValueToMatch($other, $name, $otherName), $other->getNumeratorUnits(), $other->getDenominatorUnits()); - } - - /** - * Returns {@see value}, converted to the same units as $other. - * - * @param SassNumber $other - * @param string|null $name The argument name if this is a function argument - * @param string|null $otherName The argument name for $other if this is a function argument - * - * @return float - * - * @throws SassScriptException if the units are not compatible or if either number is unitless but the other is not. - */ - public function convertValueToMatch(SassNumber $other, ?string $name = null, ?string $otherName = null): float - { - return $this->convertOrCoerceValue($other->getNumeratorUnits(), $other->getDenominatorUnits(), false, $name, $other, $otherName); - } - - /** - * 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. - * - * Note that {@see coerceValue} is generally more efficient if the value - * is going to be accessed directly. - * - * @param list $newNumeratorUnits - * @param list $newDenominatorUnits - * @param string|null $name The argument name if this is a function argument - * - * @return SassNumber - * - * @throws SassScriptException if this number's units are not compatible with $newNumeratorUnits and $newDenominatorUnits - */ - public function coerce(array $newNumeratorUnits, array $newDenominatorUnits, ?string $name = null): SassNumber - { - return self::withUnits($this->coerceValue($newNumeratorUnits, $newDenominatorUnits, $name), $newNumeratorUnits, $newDenominatorUnits); - } - - /** - * Returns {@see value}, 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 list $newNumeratorUnits - * @param list $newDenominatorUnits - * @param string|null $name The argument name if this is a function argument - * - * @return float - * - * @throws SassScriptException if this number's units are not compatible with $newNumeratorUnits and $newDenominatorUnits - */ - public function coerceValue(array $newNumeratorUnits, array $newDenominatorUnits, ?string $name = null): float - { - return $this->convertOrCoerceValue($newNumeratorUnits, $newDenominatorUnits, true, $name); - } - - /** - * A shorthand for {@see coerceValue} with a single unit - * - * @param string $unit - * @param string|null $name The argument name if this is a function argument - * - * @return float - */ - public function coerceValueToUnit(string $unit, ?string $name = null): float - { - return $this->coerceValue([$unit], [], $name); - } - - /** - * Returns a copy of this number, converted to the same units as $other. - * - * Unlike {@see convertToMatch}, this does not throw an error if this number is - * unitless and $other is not, or vice versa. Instead, it treats all unitless - * numbers as convertible to and from all units without changing the value. - * - * Note that {@see coerceValueToMatch} is generally more efficient if the value - * is going to be accessed directly. - * - * @param SassNumber $other - * @param string|null $name The argument name if this is a function argument - * @param string|null $otherName The argument name for $other if this is a function argument - * - * @return SassNumber - * - * @throws SassScriptException if the units are not compatible - */ - public function coerceToMatch(SassNumber $other, ?string $name = null, ?string $otherName = null): SassNumber - { - return self::withUnits($this->coerceValueToMatch($other, $name, $otherName), $other->getNumeratorUnits(), $other->getDenominatorUnits()); - } - - /** - * Returns {@see value}, converted to the same units as $other. - * - * Unlike {@see convertValueToMatch}, this does not throw an error if this number - * is unitless and $other is not, or vice versa. Instead, it treats all unitless - * numbers as convertible to and from all units without changing the value. - * - * @param SassNumber $other - * @param string|null $name The argument name if this is a function argument - * @param string|null $otherName The argument name for $other if this is a function argument - * - * @return float - * - * @throws SassScriptException if the units are not compatible - */ - public function coerceValueToMatch(SassNumber $other, ?string $name = null, ?string $otherName = null): float - { - return $this->convertOrCoerceValue($other->getNumeratorUnits(), $other->getDenominatorUnits(), true, $name, $other, $otherName); - } - - /** - * Returns whether this number can be compared to $other. - * - * Two numbers can be compared if they have compatible units, or if either - * number has no units. - * - * @param SassNumber $other - * - * @return bool - * - * @internal - */ - public function isComparableTo(SassNumber $other): bool - { - if (!$this->hasUnits() || !$other->hasUnits()) { - return true; - } - - try { - $this->greaterThan($other); - return true; - } catch (SassScriptException $e) { - return false; - } - } - - public function greaterThan(Value $other): SassBoolean - { - if ($other instanceof SassNumber) { - return SassBoolean::create($this->coerceUnits($other, [NumberUtil::class, 'fuzzyGreaterThan'])); - } - - throw new SassScriptException("Undefined operation \"$this > $other\"."); - } - - public function greaterThanOrEquals(Value $other): SassBoolean - { - if ($other instanceof SassNumber) { - return SassBoolean::create($this->coerceUnits($other, [NumberUtil::class, 'fuzzyGreaterThanOrEquals'])); - } - - throw new SassScriptException("Undefined operation \"$this >= $other\"."); - } - - public function lessThan(Value $other): SassBoolean - { - if ($other instanceof SassNumber) { - return SassBoolean::create($this->coerceUnits($other, [NumberUtil::class, 'fuzzyLessThan'])); - } - - throw new SassScriptException("Undefined operation \"$this < $other\"."); - } - - public function lessThanOrEquals(Value $other): SassBoolean - { - if ($other instanceof SassNumber) { - return SassBoolean::create($this->coerceUnits($other, [NumberUtil::class, 'fuzzyLessThanOrEquals'])); - } - - throw new SassScriptException("Undefined operation \"$this > $other\"."); - } - - public function modulo(Value $other): Value - { - if ($other instanceof SassNumber) { - return $this->withValue($this->coerceUnits($other, [NumberUtil::class, 'moduloLikeSass'])); - } - - throw new SassScriptException("Undefined operation \"$this % $other\"."); - } - - public function plus(Value $other): Value - { - if ($other instanceof SassNumber) { - return $this->withValue($this->coerceUnits($other, function ($num1, $num2) { - return $num1 + $num2; - })); - } - - if (!$other instanceof SassColor) { - return parent::plus($other); - } - - throw new SassScriptException("Undefined operation \"$this + $other\"."); - } - - public function minus(Value $other): Value - { - if ($other instanceof SassNumber) { - return $this->withValue($this->coerceUnits($other, function ($num1, $num2) { - return $num1 - $num2; - })); - } - - if (!$other instanceof SassColor) { - return parent::plus($other); - } - - throw new SassScriptException("Undefined operation \"$this - $other\"."); - } - - public function times(Value $other): Value - { - if ($other instanceof SassNumber) { - if (!$other->hasUnits()) { - return $this->withValue($this->value * $other->value); - } - - return $this->multiplyUnits($this->value * $other->value, $other->getNumeratorUnits(), $other->getDenominatorUnits()); - } - - throw new SassScriptException("Undefined operation \"$this * $other\"."); - } - - public function dividedBy(Value $other): Value - { - if ($other instanceof SassNumber) { - $value = NumberUtil::divideLikeSass($this->value, $other->value); - - if (!$other->hasUnits()) { - return $this->withValue($value); - } - - return $this->multiplyUnits($value, $other->getDenominatorUnits(), $other->getNumeratorUnits()); - } - - return parent::dividedBy($other); - } - - public function unaryPlus(): Value - { - return $this; - } - - public function equals(object $other): bool - { - if (!$other instanceof SassNumber) { - return false; - } - - if (\count($this->getNumeratorUnits()) !== \count($other->getNumeratorUnits()) || \count($this->getDenominatorUnits()) !== \count($other->getDenominatorUnits())) { - return false; - } - - // In Sass, neither NaN nor Infinity are equal to themselves, while PHP defines INF==INF - if (is_nan($this->value) || is_nan($other->value) || !is_finite($this->value) || !is_finite($other->value)) { - return false; - } - - if (!$this->hasUnits()) { - return NumberUtil::fuzzyEquals($this->value, $other->value); - } - - if (self::canonicalizeUnitList($this->getNumeratorUnits()) !== self::canonicalizeUnitList($other->getNumeratorUnits()) || - self::canonicalizeUnitList($this->getDenominatorUnits()) !== self::canonicalizeUnitList($other->getDenominatorUnits()) - ) { - return false; - } - - return NumberUtil::fuzzyEquals( - $this->value * self::getCanonicalMultiplier($this->getNumeratorUnits()) / self::getCanonicalMultiplier($this->getDenominatorUnits()), - $other->value * self::getCanonicalMultiplier($other->getNumeratorUnits()) / self::getCanonicalMultiplier($other->getDenominatorUnits()) - ); - } - - /** - * @param list $units - */ - private static function getCanonicalMultiplier(array $units): float - { - return array_reduce($units, function ($multiplier, $unit) { - return $multiplier * self::getCanonicalMultiplierForUnit($unit); - }, 1.0); - } - - private static function getCanonicalMultiplierForUnit(string $unit): float - { - foreach (self::CONVERSIONS as $canonicalUnit => $conversions) { - if (isset($conversions[$unit])) { - return $conversions[$canonicalUnit] / $conversions[$unit]; - } - } - - return 1.0; - } - - /** - * @param list $units - * - * @return list - */ - private static function canonicalizeUnitList(array $units): array - { - if (\count($units) === 0) { - return $units; - } - - if (\count($units) === 1) { - if (isset(self::TYPES_BY_UNIT[$units[0]])) { - $type = self::TYPES_BY_UNIT[$units[0]]; - - return [self::UNITS_BY_TYPE[$type][0]]; - } - - return $units; - } - - $canonicalUnits = []; - - foreach ($units as $unit) { - if (isset(self::TYPES_BY_UNIT[$unit])) { - $type = self::TYPES_BY_UNIT[$unit]; - - $canonicalUnits[] = self::UNITS_BY_TYPE[$type][0]; - } else { - $canonicalUnits[] = $unit; - } - } - - sort($canonicalUnits); - - return $canonicalUnits; - } - - /** - * @template T - * - * @param SassNumber $other - * @param callable(float, float): T $operation - * - * @return T - */ - private function coerceUnits(SassNumber $other, callable $operation) - { - try { - return \call_user_func($operation, $this->value, $other->coerceValueToMatch($this)); - } catch (SassScriptException $e) { - // If the conversion fails, re-run it in the other direction. This will - // generate an error message that prints $this before $other, which is - // more readable. - $this->coerceValueToMatch($other); - - throw $e; // Should be unreadable as the coercion should throw. - } - } - - /** - * @param list $newNumeratorUnits - * @param list $newDenominatorUnits - * @param bool $coerceUnitless - * @param string|null $name The argument name if this is a function argument - * @param SassNumber|null $other - * @param string|null $otherName The argument name for $other if this is a function argument - * - * @return float - * - * @throws SassScriptException if this number's units are not compatible with $newNumeratorUnits and $newDenominatorUnits - */ - private function convertOrCoerceValue(array $newNumeratorUnits, array $newDenominatorUnits, bool $coerceUnitless, ?string $name = null, ?SassNumber $other = null, ?string $otherName = null): float - { - assert($other === null || ($other->getNumeratorUnits() === $newNumeratorUnits && $other->getDenominatorUnits() === $newDenominatorUnits), sprintf("Expected %s to have units %s.", $other, self::buildUnitString($newNumeratorUnits, $newDenominatorUnits))); - - if ($this->getNumeratorUnits() === $newNumeratorUnits && $this->getDenominatorUnits() === $newDenominatorUnits) { - return $this->value; - } - - $otherHasUnits = !empty($newNumeratorUnits) || !empty($newDenominatorUnits); - - if ($coerceUnitless && (!$otherHasUnits || !$this->hasUnits())) { - return $this->value; - } - - $value = $this->value; - $oldNumerators = $this->getNumeratorUnits(); - - foreach ($newNumeratorUnits as $newNumerator) { - foreach ($oldNumerators as $key => $oldNumerator) { - $conversionFactor = self::getConversionFactor($newNumerator, $oldNumerator); - - if (\is_null($conversionFactor)) { - continue; - } - - $value *= $conversionFactor; - unset($oldNumerators[$key]); - continue 2; - } - - throw $this->compatibilityException($otherHasUnits, $newNumeratorUnits, $newDenominatorUnits, $name, $other, $otherName); - } - - $oldDenominators = $this->getDenominatorUnits(); - - foreach ($newDenominatorUnits as $newDenominator) { - foreach ($oldDenominators as $key => $oldDenominator) { - $conversionFactor = self::getConversionFactor($newDenominator, $oldDenominator); - - if (\is_null($conversionFactor)) { - continue; - } - - $value /= $conversionFactor; - unset($oldDenominators[$key]); - continue 2; - } - - throw $this->compatibilityException($otherHasUnits, $newNumeratorUnits, $newDenominatorUnits, $name, $other, $otherName); - } - - if (\count($oldNumerators) || \count($oldDenominators)) { - throw $this->compatibilityException($otherHasUnits, $newNumeratorUnits, $newDenominatorUnits, $name, $other, $otherName); - } - - return $value; - } - - /** - * @param bool $otherHasUnits - * @param list $newNumeratorUnits - * @param list $newDenominatorUnits - * @param string|null $name - * @param SassNumber|null $other - * @param string|null $otherName - * - * @return SassScriptException - */ - private function compatibilityException(bool $otherHasUnits, array $newNumeratorUnits, array $newDenominatorUnits, ?string $name, ?SassNumber $other = null, ?string $otherName = null): SassScriptException - { - if ($other !== null) { - $message = "$this and"; - - if ($otherName !== null) { - $message .= " \$$otherName:"; - } - - $message .= "$other have incompatible units"; - - if (!$this->hasUnits() || !$otherHasUnits) { - $message .= " (one has units and the other doesn't)"; - } - - return SassScriptException::forArgument("$message.", $name); - } - - if (!$otherHasUnits) { - return SassScriptException::forArgument("Expected $this to have no units.", $name); - } - - if (\count($newNumeratorUnits) === 1 && \count($newDenominatorUnits) === 0 && isset(self::TYPES_BY_UNIT[$newNumeratorUnits[0]])) { - $type = self::TYPES_BY_UNIT[$newNumeratorUnits[0]]; - $article = \in_array($type[0], ['a', 'e', 'i', 'o', 'u'], true) ? 'an' : 'a'; - $supportedUnits = implode(', ', self::UNITS_BY_TYPE[$type]); - - return SassScriptException::forArgument("Expected $this to have $article $type unit ($supportedUnits).", $name); - } - - return SassScriptException::forArgument(sprintf('Expected %s to have unit%s %s.', $this, \count($newNumeratorUnits) + \count($newDenominatorUnits) !== 1 ? 's' : '', self::buildUnitString($newNumeratorUnits, $newDenominatorUnits)), $name); - } - - /** - * @param float $value - * @param list $otherNumerators - * @param list $otherDenominators - * - * @return SassNumber - */ - protected function multiplyUnits(float $value, array $otherNumerators, array $otherDenominators): SassNumber - { - $newNumerators = array(); - - foreach ($this->getNumeratorUnits() as $numerator) { - foreach ($otherDenominators as $key => $denominator) { - $conversionFactor = self::getConversionFactor($numerator, $denominator); - - if (\is_null($conversionFactor)) { - continue; - } - - $value /= $conversionFactor; - unset($otherDenominators[$key]); - continue 2; - } - - $newNumerators[] = $numerator; - } - - $denominators = $this->getDenominatorUnits(); - - foreach ($otherNumerators as $numerator) { - foreach ($denominators as $key => $denominator) { - $conversionFactor = self::getConversionFactor($numerator, $denominator); - - if (\is_null($conversionFactor)) { - continue; - } - - $value /= $conversionFactor; - unset($denominators[$key]); - continue 2; - } - - $newNumerators[] = $numerator; - } - - $newDenominators = array_values(array_merge($denominators, $otherDenominators)); - - return self::withUnits($value, $newNumerators, $newDenominators); - } - - /** - * Returns the number of [unit1]s per [unit2]. - * - * Equivalently, `1unit2 * conversionFactor(unit1, unit2) = 1unit1`. - * - * @param string $unit1 - * @param string $unit2 - * - * @return float|null - */ - protected static function getConversionFactor(string $unit1, string $unit2): ?float - { - if ($unit1 === $unit2) { - return 1; - } - - foreach (self::CONVERSIONS as $unitVariants) { - if (isset($unitVariants[$unit1]) && isset($unitVariants[$unit2])) { - return $unitVariants[$unit1] / $unitVariants[$unit2]; - } - } - - return null; - } - - /** - * Returns unit(s) as the product of numerator units divided by the product of denominator units - * - * @param list $numerators - * @param list $denominators - * - * @return string - */ - private static function buildUnitString(array $numerators, array $denominators): string - { - if (!\count($numerators)) { - if (\count($denominators) === 0) { - return 'no units'; - } - - if (\count($denominators) === 1) { - return $denominators[0] . '^-1'; - } - - return '(' . implode('*', $denominators) . ')^-1'; - } - - return implode('*', $numerators) . (\count($denominators) ? '/' . implode('*', $denominators) : ''); - } -} diff --git a/scssphp/src/Value/SassString.php b/scssphp/src/Value/SassString.php deleted file mode 100644 index d8cd440..0000000 --- a/scssphp/src/Value/SassString.php +++ /dev/null @@ -1,243 +0,0 @@ -text = $text; - $this->quotes = $quotes; - } - - public function getText(): string - { - return $this->text; - } - - public function hasQuotes(): bool - { - return $this->quotes; - } - - public function getSassLength(): int - { - return Util::mbStrlen($this->text); - } - - public function isSpecialNumber(): bool - { - if ($this->quotes) { - return false; - } - - if (\strlen($this->text) < \strlen('min(_)')) { - return false; - } - - $first = $this->text[0]; - - if ($first === 'c' || $first === 'C') { - $second = $this->text[1]; - - if ($second === 'l' || $second === 'L') { - return ($this->text[2] === 'a' || $this->text[2] === 'A') - && ($this->text[3] === 'm' || $this->text[3] === 'M') - && ($this->text[4] === 'p' || $this->text[4] === 'P') - && $this->text[5] === '('; - } - - if ($second === 'a' || $second === 'A') { - return ($this->text[2] === 'l' || $this->text[2] === 'L') - && ($this->text[3] === 'c' || $this->text[3] === 'C') - && $this->text[4] === '('; - } - - return false; - } - - if ($first === 'v' || $first === 'V') { - return ($this->text[1] === 'a' || $this->text[1] === 'A') - && ($this->text[2] === 'r' || $this->text[2] === 'R') - && $this->text[3] === '('; - } - - if ($first === 'e' || $first === 'E') { - return ($this->text[1] === 'n' || $this->text[1] === 'N') - && ($this->text[2] === 'v' || $this->text[2] === 'V') - && $this->text[3] === '('; - } - - if ($first === 'm' || $first === 'M') { - $second = $this->text[1]; - - if ($second === 'a' || $second === 'A') { - return ($this->text[2] === 'x' || $this->text[2] === 'X') - && $this->text[3] === '('; - } - - if ($second === 'i' || $second === 'I') { - return ($this->text[2] === 'n' || $this->text[2] === 'N') - && $this->text[3] === '('; - } - - return false; - } - - return false; - } - - public function isVar(): bool - { - if ($this->quotes) { - return false; - } - - if (\strlen($this->text) < \strlen('var(--_)')) { - return false; - } - - return ($this->text[0] === 'v' || $this->text[0] === 'V') - && ($this->text[1] === 'a' || $this->text[1] === 'A') - && ($this->text[2] === 'r' || $this->text[2] === 'R') - && $this->text[3] === '('; - } - - public function isBlank(): bool - { - return !$this->quotes && $this->text === ''; - } - - /** - * Converts $sassIndex into a PHP-style index into {@see text}. - * - * Sass indexes are one-based, while PHP indexes are zero-based. Sass - * indexes may also be negative in order to index from the end of the string. - * - * In addition, Sass indices refer to Unicode code points while PHP string - * indices refer to bytes. For example, the character U+1F60A, - * Smiling Face With Smiling Eyes, is a single Unicode code point but is - * represented in UTF-8 as several bytes (`0xF0`, `0x9F`, `0x98` and `0x8A`). So in - * PHP, `substr("a😊b", 1, 1)` returns `"\xF0"`, whereas in Sass - * `str-slice("a😊b", 1, 1)` returns `"😊"`. - * - * @throws SassScriptException if $sassIndex isn't a number, if that - * number isn't an integer, or if that integer isn't a valid index for this - * string. If $sassIndex came from a function argument, $name is the - * argument name (without the `$`). It's used for error reporting. - */ - public function sassIndexToStringIndex(Value $sassIndex, ?string $name = null): int - { - $codepointIndex = $this->sassIndexToCodePointIndex($sassIndex, $name); - - if ($codepointIndex === 0) { - return 0; - } - - return \strlen(Util::mbSubstr($this->text, 0, $codepointIndex)); - } - - /** - * Converts $sassIndex into a PHP-style index into codepoints. - * - * This index is suitable to use with functions dealing with codepoints - * (i.e. the mbstring functions). - * - * Sass indexes are one-based, while PHP indexes are zero-based. Sass - * indexes may also be negative in order to index from the end of the string. - * - * See also {@see sassIndexToStringIndex}, which is an index into {@see getText} directly. - * - * @throws SassScriptException if $sassIndex isn't a number, if that - * number isn't an integer, or if that integer isn't a valid index for this - * string. If $sassIndex came from a function argument, $name is the - * argument name (without the `$`). It's used for error reporting. - */ - public function sassIndexToCodePointIndex(Value $sassIndex, ?string $name = null): int - { - $index = $sassIndex->assertNumber($name)->assertInt($name); - - if ($index === 0) { - throw SassScriptException::forArgument('String index may not be 0.', $name); - } - - $sassLength = $this->getSassLength(); - - if (abs($index) > $sassLength) { - throw SassScriptException::forArgument("Invalid index $sassIndex for a string with $sassLength characters.", $name); - } - - return $index < 0 ? $sassLength + $index : $index - 1; - } - - public function accept(ValueVisitor $visitor) - { - return $visitor->visitString($this); - } - - public function assertString(?string $name = null): SassString - { - return $this; - } - - public function plus(Value $other): Value - { - if ($other instanceof SassString) { - return new SassString($this->text . $other->getText(), $this->quotes); - } - - return new SassString($this->text . $other->toCssString(), $this->quotes); - } - - public function equals(object $other): bool - { - return $other instanceof SassString && $this->text === $other->text; - } -} diff --git a/scssphp/src/Value/SingleUnitSassNumber.php b/scssphp/src/Value/SingleUnitSassNumber.php deleted file mode 100644 index 744fe05..0000000 --- a/scssphp/src/Value/SingleUnitSassNumber.php +++ /dev/null @@ -1,303 +0,0 @@ - ['em', 'ex', 'ch', 'rem', 'vw', 'vh', 'vmin', 'vmax', 'cm', 'mm', 'q', 'in', 'pc', 'pt', 'px'], - 'ex' => ['em', 'ex', 'ch', 'rem', 'vw', 'vh', 'vmin', 'vmax', 'cm', 'mm', 'q', 'in', 'pc', 'pt', 'px'], - 'ch' => ['em', 'ex', 'ch', 'rem', 'vw', 'vh', 'vmin', 'vmax', 'cm', 'mm', 'q', 'in', 'pc', 'pt', 'px'], - 'rem' => ['em', 'ex', 'ch', 'rem', 'vw', 'vh', 'vmin', 'vmax', 'cm', 'mm', 'q', 'in', 'pc', 'pt', 'px'], - 'vw' => ['em', 'ex', 'ch', 'rem', 'vw', 'vh', 'vmin', 'vmax', 'cm', 'mm', 'q', 'in', 'pc', 'pt', 'px'], - 'vh' => ['em', 'ex', 'ch', 'rem', 'vw', 'vh', 'vmin', 'vmax', 'cm', 'mm', 'q', 'in', 'pc', 'pt', 'px'], - 'vmin' => ['em', 'ex', 'ch', 'rem', 'vw', 'vh', 'vmin', 'vmax', 'cm', 'mm', 'q', 'in', 'pc', 'pt', 'px'], - 'vmax' => ['em', 'ex', 'ch', 'rem', 'vw', 'vh', 'vmin', 'vmax', 'cm', 'mm', 'q', 'in', 'pc', 'pt', 'px'], - 'cm' => ['em', 'ex', 'ch', 'rem', 'vw', 'vh', 'vmin', 'vmax', 'cm', 'mm', 'q', 'in', 'pc', 'pt', 'px'], - 'mm' => ['em', 'ex', 'ch', 'rem', 'vw', 'vh', 'vmin', 'vmax', 'cm', 'mm', 'q', 'in', 'pc', 'pt', 'px'], - 'q' => ['em', 'ex', 'ch', 'rem', 'vw', 'vh', 'vmin', 'vmax', 'cm', 'mm', 'q', 'in', 'pc', 'pt', 'px'], - 'in' => ['em', 'ex', 'ch', 'rem', 'vw', 'vh', 'vmin', 'vmax', 'cm', 'mm', 'q', 'in', 'pc', 'pt', 'px'], - 'pc' => ['em', 'ex', 'ch', 'rem', 'vw', 'vh', 'vmin', 'vmax', 'cm', 'mm', 'q', 'in', 'pc', 'pt', 'px'], - 'pt' => ['em', 'ex', 'ch', 'rem', 'vw', 'vh', 'vmin', 'vmax', 'cm', 'mm', 'q', 'in', 'pc', 'pt', 'px'], - 'px' => ['em', 'ex', 'ch', 'rem', 'vw', 'vh', 'vmin', 'vmax', 'cm', 'mm', 'q', 'in', 'pc', 'pt', 'px'], - // angle - 'deg' => ['deg', 'grad', 'rad', 'turn'], - 'grad' => ['deg', 'grad', 'rad', 'turn'], - 'rad' => ['deg', 'grad', 'rad', 'turn'], - 'turn' => ['deg', 'grad', 'rad', 'turn'], - // time - 's' => ['s', 'ms'], - 'ms' => ['s', 'ms'], - // frequency - 'hz' => ['hz', 'khz'], - 'khz' => ['hz', 'khz'], - // pixel density - 'dpi' => ['dpi', 'dpcm', 'dppx'], - 'dpcm' => ['dpi', 'dpcm', 'dppx'], - 'dppx' => ['dpi', 'dpcm', 'dppx'], - ]; - - /** - * @var string - * @readonly - */ - private $unit; - - /** - * @param float $value - * @param string $unit - * @param array{SassNumber, SassNumber}|null $asSlash - */ - public function __construct(float $value, string $unit, array $asSlash = null) - { - parent::__construct($value, $asSlash); - $this->unit = $unit; - } - - public function getNumeratorUnits(): array - { - return [$this->unit]; - } - - public function getDenominatorUnits(): array - { - return []; - } - - public function hasUnits(): bool - { - return true; - } - - protected function withValue(float $value): SassNumber - { - return new self($value, $this->unit); - } - - public function withSlash(SassNumber $numerator, SassNumber $denominator): SassNumber - { - return new self($this->getValue(), $this->unit, array($numerator, $denominator)); - } - - public function hasUnit(string $unit): bool - { - return $unit === $this->unit; - } - - public function hasCompatibleUnits(SassNumber $other): bool - { - return $other instanceof SingleUnitSassNumber && $this->compatibleWithUnit($other->unit); - } - - public function hasPossiblyCompatibleUnits(SassNumber $other): bool - { - if (!$other instanceof SingleUnitSassNumber) { - return false; - } - - $knownCompatibilities = self::KNOWN_COMPATIBILITIES_BY_UNIT[strtolower($this->unit)] ?? null; - - if ($knownCompatibilities === null) { - return true; - } - - $otherUnit = strtolower($other->unit); - - return !isset(self::KNOWN_COMPATIBILITIES_BY_UNIT[$otherUnit]) || \in_array($otherUnit, $knownCompatibilities, true); - } - - public function compatibleWithUnit(string $unit): bool - { - return self::getConversionFactor($this->unit, $unit) !== null; - } - - public function coerceToMatch(SassNumber $other, ?string $name = null, ?string $otherName = null): SassNumber - { - if ($other instanceof SingleUnitSassNumber) { - $coerced = $this->tryCoerceToUnit($other->unit); - - if ($coerced !== null) { - return $coerced; - } - } - - // Call the parent to generate a consistent error message. - return parent::coerceToMatch($other, $name, $otherName); - } - - public function coerceValueToMatch(SassNumber $other, ?string $name = null, ?string $otherName = null): float - { - if ($other instanceof SingleUnitSassNumber) { - $coerced = $this->tryCoerceValueToUnit($other->unit); - - if ($coerced !== null) { - return $coerced; - } - } - - // Call the parent to generate a consistent error message. - return parent::coerceValueToMatch($other, $name, $otherName); - } - - public function convertToMatch(SassNumber $other, ?string $name = null, ?string $otherName = null): SassNumber - { - if ($other instanceof SingleUnitSassNumber) { - $coerced = $this->tryCoerceToUnit($other->unit); - - if ($coerced !== null) { - return $coerced; - } - } - - // Call the parent to generate a consistent error message. - return parent::convertToMatch($other, $name, $otherName); - } - - public function convertValueToMatch(SassNumber $other, ?string $name = null, ?string $otherName = null): float - { - if ($other instanceof SingleUnitSassNumber) { - $coerced = $this->tryCoerceValueToUnit($other->unit); - - if ($coerced !== null) { - return $coerced; - } - } - - // Call the parent to generate a consistent error message. - return parent::convertValueToMatch($other, $name, $otherName); - } - - public function coerce(array $newNumeratorUnits, array $newDenominatorUnits, ?string $name = null): SassNumber - { - if (\count($newNumeratorUnits) === 1 && \count($newDenominatorUnits) === 0) { - $coerced = $this->tryCoerceToUnit($newNumeratorUnits[0]); - - if ($coerced !== null) { - return $coerced; - } - } - - // Call the parent to generate a consistent error message. - return parent::coerce($newNumeratorUnits, $newDenominatorUnits, $name); - } - - public function coerceValue(array $newNumeratorUnits, array $newDenominatorUnits, ?string $name = null): float - { - if (\count($newNumeratorUnits) === 1 && \count($newDenominatorUnits) === 0) { - $coerced = $this->tryCoerceValueToUnit($newNumeratorUnits[0]); - - if ($coerced !== null) { - return $coerced; - } - } - - // Call the parent to generate a consistent error message. - return parent::coerceValue($newNumeratorUnits, $newDenominatorUnits, $name); - } - - public function coerceValueToUnit(string $unit, ?string $name = null): float - { - $coerced = $this->tryCoerceValueToUnit($unit); - - if ($coerced !== null) { - return $coerced; - } - - // Call the parent to generate a consistent error message. - return parent::coerceValueToUnit($unit, $name); - } - - public function unaryMinus(): Value - { - return new self(-$this->getValue(), $this->unit); - } - - public function equals(object $other): bool - { - if ($other instanceof SingleUnitSassNumber) { - $factor = self::getConversionFactor($other->unit, $this->unit); - - return $factor !== null && NumberUtil::fuzzyEquals($this->getValue() * $factor, $other->getValue()); - } - - return false; - } - - /** - * @param float $value - * @param list $otherNumerators - * @param list $otherDenominators - * - * @return SassNumber - */ - protected function multiplyUnits(float $value, array $otherNumerators, array $otherDenominators): SassNumber - { - $newNumerators = $otherDenominators; - $removed = false; - - foreach ($otherDenominators as $key => $denominator) { - $conversionFactor = self::getConversionFactor($denominator, $this->unit); - - if (\is_null($conversionFactor)) { - continue; - } - - $value *= $conversionFactor; - unset($otherDenominators[$key]); - $removed = true; - break; - } - - if ($removed) { - $otherDenominators = array_values($otherDenominators); - } else { - array_unshift($newNumerators, $this->unit); - } - - return SassNumber::withUnits($value, $newNumerators, $otherDenominators); - } - - private function tryCoerceToUnit(string $unit): ?SassNumber - { - if ($unit === $this->unit) { - return $this; - } - - $factor = self::getConversionFactor($unit, $this->unit); - - if ($factor === null) { - return null; - } - - return new SingleUnitSassNumber($this->getValue() * $factor, $unit); - } - - private function tryCoerceValueToUnit(string $unit): ?float - { - $factor = self::getConversionFactor($unit, $this->unit); - - if ($factor === null) { - return null; - } - - return $this->getValue() * $factor; - } -} diff --git a/scssphp/src/Value/SpanColorFormat.php b/scssphp/src/Value/SpanColorFormat.php deleted file mode 100644 index abbce07..0000000 --- a/scssphp/src/Value/SpanColorFormat.php +++ /dev/null @@ -1,36 +0,0 @@ -span = $span; - } - - public function getOriginal(): string - { - return $this->span->getText(); - } -} diff --git a/scssphp/src/Value/UnitlessSassNumber.php b/scssphp/src/Value/UnitlessSassNumber.php deleted file mode 100644 index 96b28f1..0000000 --- a/scssphp/src/Value/UnitlessSassNumber.php +++ /dev/null @@ -1,219 +0,0 @@ -getValue(), array($numerator, $denominator)); - } - - public function hasUnit(string $unit): bool - { - return false; - } - - public function hasCompatibleUnits(SassNumber $other): bool - { - return $other instanceof UnitlessSassNumber; - } - - public function hasPossiblyCompatibleUnits(SassNumber $other): bool - { - return $other instanceof UnitlessSassNumber; - } - - public function compatibleWithUnit(string $unit): bool - { - return true; - } - - public function coerceToMatch(SassNumber $other, ?string $name = null, ?string $otherName = null): SassNumber - { - return $other->withValue($this->getValue()); - } - - public function coerceValueToMatch(SassNumber $other, ?string $name = null, ?string $otherName = null): float - { - return $this->getValue(); - } - - public function convertToMatch(SassNumber $other, ?string $name = null, ?string $otherName = null): SassNumber - { - if (!$other->hasUnits()) { - return $this; - } - - // Call the parent to generate a consistent error message. - return parent::convertToMatch($other, $name, $otherName); - } - - public function convertValueToMatch(SassNumber $other, ?string $name = null, ?string $otherName = null): float - { - if (!$other->hasUnits()) { - return $this->getValue(); - } - - // Call the parent to generate a consistent error message. - return parent::convertValueToMatch($other, $name, $otherName); - } - - public function coerce(array $newNumeratorUnits, array $newDenominatorUnits, ?string $name = null): SassNumber - { - return SassNumber::withUnits($this->getValue(), $newNumeratorUnits, $newDenominatorUnits); - } - - public function coerceValue(array $newNumeratorUnits, array $newDenominatorUnits, ?string $name = null): float - { - return $this->getValue(); - } - - public function coerceValueToUnit(string $unit, ?string $name = null): float - { - return $this->getValue(); - } - - public function greaterThan(Value $other): SassBoolean - { - if ($other instanceof SassNumber) { - return SassBoolean::create(NumberUtil::fuzzyGreaterThan($this->getValue(), $other->getValue())); - } - - return parent::greaterThan($other); - } - - public function greaterThanOrEquals(Value $other): SassBoolean - { - if ($other instanceof SassNumber) { - return SassBoolean::create(NumberUtil::fuzzyGreaterThanOrEquals($this->getValue(), $other->getValue())); - } - - return parent::greaterThanOrEquals($other); - } - - public function lessThan(Value $other): SassBoolean - { - if ($other instanceof SassNumber) { - return SassBoolean::create(NumberUtil::fuzzyLessThan($this->getValue(), $other->getValue())); - } - - return parent::lessThan($other); - } - - public function lessThanOrEquals(Value $other): SassBoolean - { - if ($other instanceof SassNumber) { - return SassBoolean::create(NumberUtil::fuzzyLessThanOrEquals($this->getValue(), $other->getValue())); - } - - return parent::lessThanOrEquals($other); - } - - public function modulo(Value $other): Value - { - if ($other instanceof SassNumber) { - return $other->withValue(NumberUtil::moduloLikeSass($this->getValue(), $other->getValue())); - } - - return parent::modulo($other); - } - - public function plus(Value $other): Value - { - if ($other instanceof SassNumber) { - return $other->withValue($this->getValue() + $other->getValue()); - } - - return parent::plus($other); - } - - public function minus(Value $other): Value - { - if ($other instanceof SassNumber) { - return $other->withValue($this->getValue() - $other->getValue()); - } - - return parent::minus($other); - } - - public function times(Value $other): Value - { - if ($other instanceof SassNumber) { - return $other->withValue($this->getValue() * $other->getValue()); - } - - return parent::times($other); - } - - public function dividedBy(Value $other): Value - { - if ($other instanceof SassNumber) { - $value = NumberUtil::divideLikeSass($this->getValue(), $other->getValue()); - - if ($other->hasUnits()) { - return SassNumber::withUnits($value, $other->getDenominatorUnits(), $other->getNumeratorUnits()); - } - - return new self($value); - } - - return parent::dividedBy($other); - } - - public function unaryMinus(): Value - { - return new self(-$this->getValue()); - } - - public function equals(object $other): bool - { - return $other instanceof UnitlessSassNumber && NumberUtil::fuzzyEquals($this->getValue(), $other->getValue()); - } -} diff --git a/scssphp/src/Value/Value.php b/scssphp/src/Value/Value.php deleted file mode 100644 index 21d562e..0000000 --- a/scssphp/src/Value/Value.php +++ /dev/null @@ -1,746 +0,0 @@ - - */ - public function asList(): array - { - return [$this]; - } - - /** - * The length of {@see asList}. - * - * This is used to compute {@see sassIndexToListIndex} without allocating a new - * list. - */ - protected function getLengthAsList(): int - { - return 1; - } - - /** - * Calls the appropriate visit method on $visitor. - * - * @template T - * - * @param ValueVisitor $visitor - * - * @return T - * - * @internal - */ - abstract public function accept(ValueVisitor $visitor); - - /** - * Converts $sassIndex into a PHP-style index into the list returned by - * {@see asList}. - * - * Sass indexes are one-based, while PHP indexes are zero-based. Sass - * indexes may also be negative in order to index from the end of the list. - * - * @throws SassScriptException if $sassIndex isn't a number, if that - * number isn't an integer, or if that integer isn't a valid index for - * {@see asList}. If $sassIndex came from a function argument, $name is the - * argument name (without the `$`). It's used for error reporting. - */ - public function sassIndexToListIndex(Value $sassIndex, ?string $name = null): int - { - $index = $sassIndex->assertNumber($name)->assertInt($name); - - if ($index === 0) { - throw SassScriptException::forArgument('List index may not be 0.', $name); - } - - $lengthAsList = $this->getLengthAsList(); - - if (abs($index) > $lengthAsList) { - throw SassScriptException::forArgument("Invalid index $sassIndex for a list with $lengthAsList elements.", $name); - } - - return $index < 0 ? $lengthAsList + $index : $index - 1; - } - - /** - * Throws a {@see SassScriptException} if $this isn't a boolean. - * - * Note that generally, functions should use {@see isTruthy} rather than requiring - * a literal boolean. - * - * If this came from a function argument, $name is the argument name - * (without the `$`). It's used for error reporting. - * - * @param string|null $name - * - * @return SassBoolean - * - * @throws SassScriptException - */ - public function assertBoolean(?string $name = null): SassBoolean - { - throw SassScriptException::forArgument("$this is not a boolean.", $name); - } - - /** - * Throws a {@see SassScriptException} if $this isn't a calculation. - * - * If this came from a function argument, $name is the argument name - * (without the `$`). It's used for error reporting. - * - * @param string|null $name - * - * @return SassCalculation - * - * @throws SassScriptException - */ - public function assertCalculation(?string $name = null): SassCalculation - { - throw SassScriptException::forArgument("$this is not a calculation.", $name); - } - - /** - * Throws a {@see SassScriptException} if $this isn't a color. - * - * If this came from a function argument, $name is the argument name - * (without the `$`). It's used for error reporting. - * - * @param string|null $name - * - * @return SassColor - * - * @throws SassScriptException - */ - public function assertColor(?string $name = null): SassColor - { - throw SassScriptException::forArgument("$this is not a color.", $name); - } - - /** - * Throws a {@see SassScriptException} if $this isn't a string. - * - * If this came from a function argument, $name is the argument name - * (without the `$`). It's used for error reporting. - * - * @param string|null $name - * - * @return SassFunction - * - * @throws SassScriptException - */ - public function assertFunction(?string $name = null): SassFunction - { - throw SassScriptException::forArgument("$this is not a function.", $name); - } - - /** - * Throws a {@see SassScriptException} if $this isn't a map. - * - * If this came from a function argument, $name is the argument name - * (without the `$`). It's used for error reporting. - * - * @param string|null $name - * - * @return SassMap - * - * @throws SassScriptException - */ - public function assertMap(?string $name = null): SassMap - { - throw SassScriptException::forArgument("$this is not a map.", $name); - } - - /** - * Return $this as a SassMap if it is one (including empty lists) or null otherwise. - * - * @return SassMap|null - */ - public function tryMap(): ?SassMap - { - return null; - } - - /** - * Throws a {@see SassScriptException} if $this isn't a number. - * - * If this came from a function argument, $name is the argument name - * (without the `$`). It's used for error reporting. - * - * @param string|null $name - * - * @return SassNumber - * - * @throws SassScriptException - */ - public function assertNumber(?string $name = null): SassNumber - { - throw SassScriptException::forArgument("$this is not a number.", $name); - } - - /** - * Throws a {@see SassScriptException} if $this isn't a string. - * - * If this came from a function argument, $name is the argument name - * (without the `$`). It's used for error reporting. - * - * @param string|null $name - * - * @return SassString - * - * @throws SassScriptException - */ - public function assertString(?string $name = null): SassString - { - throw SassScriptException::forArgument("$this is not a string.", $name); - } - - /** - * Parses $this as a selector list, in the same manner as the - * `selector-parse()` function. - * - * @throws SassScriptException if this isn't a type that can be parsed as a - * selector, or if parsing fails. If $allowParent is `true`, this allows - * {@see ParentSelector}s. Otherwise, they're considered parse errors. - * - * If this came from a function argument, $name is the argument name - * (without the `$`). It's used for error reporting. - * - * @internal - */ - public function assertSelector(?string $name = null, bool $allowParent = false): SelectorList - { - $string = $this->selectorString($name); - - try { - return SelectorList::parse($string, null, null, $allowParent); - } catch (SassFormatException $e) { - throw SassScriptException::forArgument($e->getMessage(), $name, $e); - } - } - - /** - * Parses $this as a simple selector, in the same manner as the - * `selector-parse()` function. - * - * @throws SassScriptException if this isn't a type that can be parsed as a - * selector, or if parsing fails. If $allowParent is `true`, this allows - * {@see ParentSelector}s. Otherwise, they're considered parse errors. - * - * If this came from a function argument, $name is the argument name - * (without the `$`). It's used for error reporting. - * - * @internal - */ - public function assertSimpleSelector(?string $name = null, bool $allowParent = false): SimpleSelector - { - $string = $this->selectorString($name); - - try { - return SimpleSelector::parse($string, null, null, $allowParent); - } catch (SassFormatException $e) { - throw SassScriptException::forArgument($e->getMessage(), $name, $e); - } - } - - /** - * Parses $this as a compound selector, in the same manner as the - * `selector-parse()` function. - * - * @throws SassScriptException if this isn't a type that can be parsed as a - * selector, or if parsing fails. If $allowParent is `true`, this allows - * {@see ParentSelector}s. Otherwise, they're considered parse errors. - * - * If this came from a function argument, $name is the argument name - * (without the `$`). It's used for error reporting. - * - * @internal - */ - public function assertCompoundSelector(?string $name = null, bool $allowParent = false): CompoundSelector - { - $string = $this->selectorString($name); - - try { - return CompoundSelector::parse($string, null, null, $allowParent); - } catch (SassFormatException $e) { - throw SassScriptException::forArgument($e->getMessage(), $name, $e); - } - } - - /** - * Parses $this as a complex selector, in the same manner as the - * `selector-parse()` function. - * - * @throws SassScriptException if this isn't a type that can be parsed as a - * selector, or if parsing fails. If $allowParent is `true`, this allows - * {@see ParentSelector}s. Otherwise, they're considered parse errors. - * - * If this came from a function argument, $name is the argument name - * (without the `$`). It's used for error reporting. - * - * @internal - */ - public function assertComplexSelector(?string $name = null, bool $allowParent = false): ComplexSelector - { - $string = $this->selectorString($name); - - try { - return ComplexSelector::parse($string, null, null, $allowParent); - } catch (SassFormatException $e) { - throw SassScriptException::forArgument($e->getMessage(), $name, $e); - } - } - - /** - * Converts a `selector-parse()`-style input into a string that can be - * parsed. - * - * @throws SassScriptException if $this isn't a type or a structure that - * can be parsed as a selector. - */ - private function selectorString(?string $name): string - { - $string = $this->selectorStringOrNull(); - - if ($string !== null) { - return $string; - } - - throw SassScriptException::forArgument("$this is not a valid selector: it must be a string,\na list of strings, or a list of lists of strings.", $name); - } - - /** - * Converts a `selector-parse()`-style input into a string that can be - * parsed. - * - * Returns `null` if $this isn't a type or a structure that can be parsed as - * a selector. - */ - private function selectorStringOrNull(): ?string - { - if ($this instanceof SassString) { - return $this->getText(); - } - - if (!$this instanceof SassList) { - return null; - } - - $list = $this; - if (\count($list->asList()) === 0) { - return null; - } - - $result = []; - switch ($list->getSeparator()) { - case ListSeparator::COMMA: - foreach ($list->asList() as $complex) { - if ($complex instanceof SassString) { - $result[] = $complex->getText(); - } elseif ($complex instanceof SassList && $complex->getSeparator() === ListSeparator::SPACE) { - $string = $complex->selectorStringOrNull(); - - if ($string === null) { - return null; - } - - $result[] = $string; - } else { - return null; - } - } - break; - - case ListSeparator::SLASH: - return null; - - default: - foreach ($list->asList() as $compound) { - if ($compound instanceof SassString) { - $result[] = $compound->getText(); - } else { - return null; - } - } - break; - } - - return implode($list->getSeparator() === ListSeparator::COMMA ? ', ' : ' ', $result); - } - - /** - * Whether the value will be represented in CSS as the empty string. - * - * @return bool - * - * @internal - */ - public function isBlank(): bool - { - return false; - } - - /** - * Whether this is a value that CSS may treat as a number, such as `calc()` or `var()`. - * - * Functions that shadow plain CSS functions need to gracefully handle when - * these arguments are passed in. - * - * @return bool - * - * @internal - */ - public function isSpecialNumber(): bool - { - return false; - } - - /** - * Whether this is a call to `var()`, which may be substituted in CSS for a custom property value. - * - * Functions that shadow plain CSS functions need to gracefully handle when - * these arguments are passed in. - * - * @return bool - * - * @internal - */ - public function isVar(): bool - { - return false; - } - - /** - * Returns a new list containing $contents that defaults to this value's - * separator and brackets. - * - * @param list $contents - * @param string|null $separator - * @param bool|null $brackets - * - * @return SassList - * - * @phpstan-param ListSeparator::*|null $separator - */ - public function withListContents(array $contents, ?string $separator = null, ?bool $brackets = null): SassList - { - return new SassList($contents, $separator ?? $this->getSeparator(), $brackets ?? $this->hasBrackets()); - } - - /** - * The SassScript = operation - * - * @param Value $other - * - * @return Value - * - * @internal - */ - public function singleEquals(Value $other): Value - { - return new SassString(sprintf('%s=%s', $this->toCssString(), $other->toCssString()), false); - } - - /** - * The SassScript `>` operation. - * - * @param Value $other - * - * @return SassBoolean - * - * @internal - */ - public function greaterThan(Value $other): SassBoolean - { - throw new SassScriptException("Undefined operation \"$this > $other\"."); - } - - /** - * The SassScript `>=` operation. - * - * @param Value $other - * - * @return SassBoolean - * - * @internal - */ - public function greaterThanOrEquals(Value $other): SassBoolean - { - throw new SassScriptException("Undefined operation \"$this >= $other\"."); - } - - /** - * The SassScript `<` operation. - * - * @param Value $other - * - * @return SassBoolean - * - * @internal - */ - public function lessThan(Value $other): SassBoolean - { - throw new SassScriptException("Undefined operation \"$this < $other\"."); - } - - /** - * The SassScript `<=` operation. - * - * @param Value $other - * - * @return SassBoolean - * - * @internal - */ - public function lessThanOrEquals(Value $other): SassBoolean - { - throw new SassScriptException("Undefined operation \"$this <= $other\"."); - } - - /** - * The SassScript `*` operation. - * - * @param Value $other - * - * @return Value - * - * @internal - */ - public function times(Value $other): Value - { - throw new SassScriptException("Undefined operation \"$this * $other\"."); - } - - /** - * The SassScript `%` operation. - * - * @param Value $other - * - * @return Value - * - * @internal - */ - public function modulo(Value $other): Value - { - throw new SassScriptException("Undefined operation \"$this % $other\"."); - } - - /** - * The SassScript `+` operation. - * - * @param Value $other - * - * @return Value - * - * @internal - */ - public function plus(Value $other): Value - { - if ($other instanceof SassString) { - return new SassString($this->toCssString() . $other->getText(), $other->hasQuotes()); - } - - if ($other instanceof SassCalculation) { - throw new SassScriptException("Undefined operation \"$this + $other\"."); - } - - return new SassString($this->toCssString() . $other->toCssString(), false); - } - - /** - * The SassScript `-` operation. - * - * @param Value $other - * - * @return Value - * - * @internal - */ - public function minus(Value $other): Value - { - if ($other instanceof SassCalculation) { - throw new SassScriptException("Undefined operation \"$this - $other\"."); - } - - return new SassString(sprintf('%s-%s', $this->toCssString(), $other->toCssString()), false); - } - - /** - * The SassScript `/` operation. - * - * @param Value $other - * - * @return Value - * - * @internal - */ - public function dividedBy(Value $other): Value - { - return new SassString(sprintf('%s/%s', $this->toCssString(), $other->toCssString()), false); - } - - /** - * The SassScript unary `+` operation. - * - * @return Value - * - * @internal - */ - public function unaryPlus(): Value - { - return new SassString(sprintf('+%s', $this->toCssString()), false); - } - - /** - * The SassScript unary `-` operation. - * - * @return Value - * - * @internal - */ - public function unaryMinus(): Value - { - return new SassString(sprintf('-%s', $this->toCssString()), false); - } - - /** - * The SassScript unary `/` operation. - * - * @return Value - * - * @internal - */ - public function unaryDivide(): Value - { - return new SassString(sprintf('/%s', $this->toCssString()), false); - } - - /** - * The SassScript unary `not` operation. - * - * @return Value - * - * @internal - */ - public function unaryNot(): Value - { - return SassBoolean::create(false); - } - - /** - * Returns a copy of $this without {@see SassNumber::$asSlash} set. - * - * If this isn't a SassNumber, return it as-is. - * - * @return Value - * - * @internal - */ - public function withoutSlash(): Value - { - return $this; - } - - /** - * Returns a valid CSS representation of $this. - * - * Use {@see toString} instead to get a string representation even if this - * isn't valid CSS. - * - * Internal-only: If $quote is `false`, quoted strings are emitted without - * quotes. - * - * @throws SassScriptException if $this cannot be represented in plain CSS. - */ - final public function toCssString(bool $quote = true): string - { - return Serializer::serializeValue($this, false, $quote); - } - - /** - * Returns a Sass representation of $this. - * - * Note that this is equivalent to calling `inspect()` on the value, and thus - * won't reflect the user's output settings. {@see toCssString} should be used - * instead to convert $this to CSS. - * - * @return string - */ - final public function __toString(): string - { - return Serializer::serializeValue($this, true); - } -} diff --git a/scssphp/src/Visitor/AnySelectorVisitor.php b/scssphp/src/Visitor/AnySelectorVisitor.php deleted file mode 100644 index 34f7ceb..0000000 --- a/scssphp/src/Visitor/AnySelectorVisitor.php +++ /dev/null @@ -1,112 +0,0 @@ - - * @internal - */ -abstract class AnySelectorVisitor implements SelectorVisitor -{ - public function visitComplexSelector(ComplexSelector $complex): bool - { - foreach ($complex->getComponents() as $component) { - if ($this->visitCompoundSelector($component->getSelector())) { - return true; - } - } - - return false; - } - - public function visitCompoundSelector(CompoundSelector $compound): bool - { - foreach ($compound->getComponents() as $simple) { - if ($simple->accept($this)) { - return true; - } - } - - return false; - } - - public function visitPseudoSelector(PseudoSelector $pseudo): bool - { - $selector = $pseudo->getSelector(); - - return $selector === null ? false : $selector->accept($this); - } - - public function visitSelectorList(SelectorList $list): bool - { - foreach ($list->getComponents() as $complex) { - if ($this->visitComplexSelector($complex)) { - return true; - } - } - - return false; - } - - public function visitAttributeSelector(AttributeSelector $attribute): bool - { - return false; - } - - public function visitClassSelector(ClassSelector $klass): bool - { - return false; - } - - public function visitIDSelector(IDSelector $id): bool - { - return false; - } - - public function visitParentSelector(ParentSelector $parent): bool - { - return false; - } - - public function visitPlaceholderSelector(PlaceholderSelector $placeholder): bool - { - return false; - } - - public function visitTypeSelector(TypeSelector $type): bool - { - return false; - } - - public function visitUniversalSelector(UniversalSelector $universal): bool - { - return false; - } -} diff --git a/scssphp/src/Visitor/CssVisitor.php b/scssphp/src/Visitor/CssVisitor.php deleted file mode 100644 index db5a413..0000000 --- a/scssphp/src/Visitor/CssVisitor.php +++ /dev/null @@ -1,98 +0,0 @@ - - */ -interface CssVisitor extends ModifiableCssVisitor -{ - /** - * @param CssAtRule $node - * - * @return T - */ - public function visitCssAtRule($node); - - /** - * @param CssComment $node - * - * @return T - */ - public function visitCssComment($node); - - /** - * @param CssDeclaration $node - * - * @return T - */ - public function visitCssDeclaration($node); - - /** - * @param CssImport $node - * - * @return T - */ - public function visitCssImport($node); - - /** - * @param CssKeyframeBlock $node - * - * @return T - */ - public function visitCssKeyframeBlock($node); - - /** - * @param CssMediaRule $node - * - * @return T - */ - public function visitCssMediaRule($node); - - /** - * @param CssStyleRule $node - * - * @return T - */ - public function visitCssStyleRule($node); - - /** - * @param CssStylesheet $node - * - * @return T - */ - public function visitCssStylesheet($node); - - /** - * @param CssSupportsRule $node - * - * @return T - */ - public function visitCssSupportsRule($node); - -} diff --git a/scssphp/src/Visitor/EveryCssVisitor.php b/scssphp/src/Visitor/EveryCssVisitor.php deleted file mode 100644 index b503038..0000000 --- a/scssphp/src/Visitor/EveryCssVisitor.php +++ /dev/null @@ -1,106 +0,0 @@ - - * @internal - */ -abstract class EveryCssVisitor implements CssVisitor -{ - public function visitCssAtRule($node): bool - { - foreach ($node->getChildren() as $child) { - if (!$child->accept($this)) { - return false; - } - } - - return true; - } - - public function visitCssComment($node): bool - { - return false; - } - - public function visitCssDeclaration($node): bool - { - return false; - } - - public function visitCssImport($node): bool - { - return false; - } - - public function visitCssKeyframeBlock($node): bool - { - foreach ($node->getChildren() as $child) { - if (!$child->accept($this)) { - return false; - } - } - - return true; - } - - public function visitCssMediaRule($node): bool - { - foreach ($node->getChildren() as $child) { - if (!$child->accept($this)) { - return false; - } - } - - return true; - } - - public function visitCssStyleRule($node): bool - { - foreach ($node->getChildren() as $child) { - if (!$child->accept($this)) { - return false; - } - } - - return true; - } - - public function visitCssStylesheet($node): bool - { - foreach ($node->getChildren() as $child) { - if (!$child->accept($this)) { - return false; - } - } - - return true; - } - - public function visitCssSupportsRule($node): bool - { - foreach ($node->getChildren() as $child) { - if (!$child->accept($this)) { - return false; - } - } - - return true; - } -} diff --git a/scssphp/src/Visitor/ExpressionVisitor.php b/scssphp/src/Visitor/ExpressionVisitor.php deleted file mode 100644 index a6e1efc..0000000 --- a/scssphp/src/Visitor/ExpressionVisitor.php +++ /dev/null @@ -1,132 +0,0 @@ - - */ -abstract class StatementSearchVisitor implements StatementVisitor -{ - public function visitAtRootRule(AtRootRule $node) - { - return $this->visitChildren($node->getChildren()); - } - - public function visitAtRule(AtRule $node) - { - if ($node->getChildren() !== null) { - return $this->visitChildren($node->getChildren()); - } - - return null; - } - - public function visitContentBlock(ContentBlock $node) - { - return $this->visitCallableDeclaration($node); - } - - public function visitContentRule(ContentRule $node) - { - return null; - } - - public function visitDebugRule(DebugRule $node) - { - return null; - } - - public function visitDeclaration(Declaration $node) - { - if ($node->getChildren() !== null) { - return $this->visitChildren($node->getChildren()); - } - - return null; - } - - public function visitEachRule(EachRule $node) - { - return $this->visitChildren($node->getChildren()); - } - - public function visitErrorRule(ErrorRule $node) - { - return null; - } - - public function visitExtendRule(ExtendRule $node) - { - return null; - } - - public function visitForRule(ForRule $node) - { - return $this->visitChildren($node->getChildren()); - } - - public function visitFunctionRule(FunctionRule $node) - { - return $this->visitCallableDeclaration($node); - } - - public function visitIfRule(IfRule $node) - { - $value = $this->searchIterable($node->getClauses(), function (IfClause $clause) { - return $this->searchIterable($clause->getChildren(), function (Statement $child) { - return $child->accept($this); - }); - }); - - if ($node->getLastClause() !== null) { - $value = $value ?? $this->searchIterable($node->getLastClause()->getChildren(), function (Statement $child) { - return $child->accept($this); - }); - } - - return $value; - } - - public function visitImportRule(ImportRule $node) - { - return null; - } - - public function visitIncludeRule(IncludeRule $node) - { - if ($node->getContent() !== null) { - return $this->visitContentBlock($node->getContent()); - } - - return null; - } - - public function visitLoudComment(LoudComment $node) - { - return null; - } - - public function visitMediaRule(MediaRule $node) - { - return $this->visitChildren($node->getChildren()); - } - - public function visitMixinRule(MixinRule $node) - { - return $this->visitCallableDeclaration($node); - } - - public function visitReturnRule(ReturnRule $node) - { - return null; - } - - public function visitSilentComment(SilentComment $node) - { - return null; - } - - public function visitStyleRule(StyleRule $node) - { - return $this->visitChildren($node->getChildren()); - } - - public function visitStylesheet(Stylesheet $node) - { - return $this->visitChildren($node->getChildren()); - } - - public function visitSupportsRule(SupportsRule $node) - { - return $this->visitChildren($node->getChildren()); - } - - public function visitVariableDeclaration(VariableDeclaration $node) - { - return null; - } - - public function visitWarnRule(WarnRule $node) - { - return null; - } - - public function visitWhileRule(WhileRule $node) - { - return $this->visitChildren($node->getChildren()); - } - - /** - * Visits each of $node's expressions and children. - * - * The default implementations of {@see visitFunctionRule} and {@see visitMixinRule} - * call this. - * - * @return T|null - */ - protected function visitCallableDeclaration(CallableDeclaration $node) - { - return $this->visitChildren($node->getChildren()); - } - - /** - * Visits each child in $children. - * - * The default implementation of the visit methods for all {@see ParentStatement}s - * call this. - * - * @param Statement[] $children - * - * @return T|null - */ - protected function visitChildren(array $children) - { - foreach ($children as $child) { - $result = $child->accept($this); - - if ($result !== null) { - return $result; - } - } - - return null; - } - - /** - * Returns the first `T` returned by $callback for an element of $iterable, - * or `null` if it returns `null` for every element. - * - * @template E - * @param iterable $iterable - * @param callable(E): (T|null) $callback - * - * @return T|null - */ - private function searchIterable(iterable $iterable, callable $callback) - { - foreach ($iterable as $element) { - $value = $callback($element); - - if ($value !== null) { - return $value; - } - } - - return null; - } -} diff --git a/scssphp/src/Visitor/StatementVisitor.php b/scssphp/src/Visitor/StatementVisitor.php deleted file mode 100644 index 5634234..0000000 --- a/scssphp/src/Visitor/StatementVisitor.php +++ /dev/null @@ -1,174 +0,0 @@ -