aboutsummaryrefslogtreecommitdiffstats
path: root/vendor/scssphp
diff options
context:
space:
mode:
Diffstat (limited to 'vendor/scssphp')
-rw-r--r--vendor/scssphp/scssphp/LICENSE.md8
-rw-r--r--vendor/scssphp/scssphp/README.md2
-rwxr-xr-xvendor/scssphp/scssphp/bin/pscss244
-rw-r--r--vendor/scssphp/scssphp/composer.json24
-rw-r--r--vendor/scssphp/scssphp/scss.inc.php21
-rw-r--r--vendor/scssphp/scssphp/src/Ast/AstNode.php25
-rw-r--r--vendor/scssphp/scssphp/src/Ast/Css/CssAtRule.php35
-rw-r--r--vendor/scssphp/scssphp/src/Ast/Css/CssComment.php34
-rw-r--r--vendor/scssphp/scssphp/src/Ast/Css/CssDeclaration.php84
-rw-r--r--vendor/scssphp/scssphp/src/Ast/Css/CssImport.php37
-rw-r--r--vendor/scssphp/scssphp/src/Ast/Css/CssKeyframeBlock.php30
-rw-r--r--vendor/scssphp/scssphp/src/Ast/Css/CssMediaQuery.php250
-rw-r--r--vendor/scssphp/scssphp/src/Ast/Css/CssMediaRule.php30
-rw-r--r--vendor/scssphp/scssphp/src/Ast/Css/CssNode.php68
-rw-r--r--vendor/scssphp/scssphp/src/Ast/Css/CssParentNode.php37
-rw-r--r--vendor/scssphp/scssphp/src/Ast/Css/CssStyleRule.php44
-rw-r--r--vendor/scssphp/scssphp/src/Ast/Css/CssStylesheet.php (renamed from vendor/scssphp/scssphp/src/Exception/CompilerException.php)8
-rw-r--r--vendor/scssphp/scssphp/src/Ast/Css/CssSupportsRule.php28
-rw-r--r--vendor/scssphp/scssphp/src/Ast/Css/CssValue.php79
-rw-r--r--vendor/scssphp/scssphp/src/Ast/Css/IsInvisibleVisitor.php57
-rw-r--r--vendor/scssphp/scssphp/src/Ast/Css/MediaQueryMergeResult.php23
-rw-r--r--vendor/scssphp/scssphp/src/Ast/Css/MediaQuerySingletonMergeResult.php19
-rw-r--r--vendor/scssphp/scssphp/src/Ast/Css/ModifiableCssAtRule.php97
-rw-r--r--vendor/scssphp/scssphp/src/Ast/Css/ModifiableCssComment.php54
-rw-r--r--vendor/scssphp/scssphp/src/Ast/Css/ModifiableCssDeclaration.php121
-rw-r--r--vendor/scssphp/scssphp/src/Ast/Css/ModifiableCssImport.php71
-rw-r--r--vendor/scssphp/scssphp/src/Ast/Css/ModifiableCssKeyframeBlock.php67
-rw-r--r--vendor/scssphp/scssphp/src/Ast/Css/ModifiableCssMediaRule.php67
-rw-r--r--vendor/scssphp/scssphp/src/Ast/Css/ModifiableCssNode.php152
-rw-r--r--vendor/scssphp/scssphp/src/Ast/Css/ModifiableCssParentNode.php85
-rw-r--r--vendor/scssphp/scssphp/src/Ast/Css/ModifiableCssStyleRule.php88
-rw-r--r--vendor/scssphp/scssphp/src/Ast/Css/ModifiableCssStylesheet.php55
-rw-r--r--vendor/scssphp/scssphp/src/Ast/Css/ModifiableCssSupportsRule.php67
-rw-r--r--vendor/scssphp/scssphp/src/Ast/FakeAstNode.php46
-rw-r--r--vendor/scssphp/scssphp/src/Ast/Sass/Argument.php87
-rw-r--r--vendor/scssphp/scssphp/src/Ast/Sass/ArgumentDeclaration.php242
-rw-r--r--vendor/scssphp/scssphp/src/Ast/Sass/ArgumentInvocation.php125
-rw-r--r--vendor/scssphp/scssphp/src/Ast/Sass/AtRootQuery.php155
-rw-r--r--vendor/scssphp/scssphp/src/Ast/Sass/CallableInvocation.php18
-rw-r--r--vendor/scssphp/scssphp/src/Ast/Sass/ConfiguredVariable.php70
-rw-r--r--vendor/scssphp/scssphp/src/Ast/Sass/Expression.php30
-rw-r--r--vendor/scssphp/scssphp/src/Ast/Sass/Expression/BinaryOperationExpression.php146
-rw-r--r--vendor/scssphp/scssphp/src/Ast/Sass/Expression/BinaryOperator.php83
-rw-r--r--vendor/scssphp/scssphp/src/Ast/Sass/Expression/BooleanExpression.php55
-rw-r--r--vendor/scssphp/scssphp/src/Ast/Sass/Expression/ColorExpression.php56
-rw-r--r--vendor/scssphp/scssphp/src/Ast/Sass/Expression/FunctionExpression.php134
-rw-r--r--vendor/scssphp/scssphp/src/Ast/Sass/Expression/IfExpression.php79
-rw-r--r--vendor/scssphp/scssphp/src/Ast/Sass/Expression/InterpolatedFunctionExpression.php74
-rw-r--r--vendor/scssphp/scssphp/src/Ast/Sass/Expression/IsCalculationSafeVisitor.php129
-rw-r--r--vendor/scssphp/scssphp/src/Ast/Sass/Expression/ListExpression.php132
-rw-r--r--vendor/scssphp/scssphp/src/Ast/Sass/Expression/MapExpression.php64
-rw-r--r--vendor/scssphp/scssphp/src/Ast/Sass/Expression/NullExpression.php47
-rw-r--r--vendor/scssphp/scssphp/src/Ast/Sass/Expression/NumberExpression.php64
-rw-r--r--vendor/scssphp/scssphp/src/Ast/Sass/Expression/ParenthesizedExpression.php55
-rw-r--r--vendor/scssphp/scssphp/src/Ast/Sass/Expression/SelectorExpression.php47
-rw-r--r--vendor/scssphp/scssphp/src/Ast/Sass/Expression/StringExpression.php181
-rw-r--r--vendor/scssphp/scssphp/src/Ast/Sass/Expression/SupportsExpression.php56
-rw-r--r--vendor/scssphp/scssphp/src/Ast/Sass/Expression/UnaryOperationExpression.php82
-rw-r--r--vendor/scssphp/scssphp/src/Ast/Sass/Expression/UnaryOperator.php37
-rw-r--r--vendor/scssphp/scssphp/src/Ast/Sass/Expression/ValueExpression.php59
-rw-r--r--vendor/scssphp/scssphp/src/Ast/Sass/Expression/VariableExpression.php90
-rw-r--r--vendor/scssphp/scssphp/src/Ast/Sass/Import.php22
-rw-r--r--vendor/scssphp/scssphp/src/Ast/Sass/Import/DynamicImport.php62
-rw-r--r--vendor/scssphp/scssphp/src/Ast/Sass/Import/StaticImport.php73
-rw-r--r--vendor/scssphp/scssphp/src/Ast/Sass/Interpolation.php139
-rw-r--r--vendor/scssphp/scssphp/src/Ast/Sass/SassDeclaration.php37
-rw-r--r--vendor/scssphp/scssphp/src/Ast/Sass/SassNode.php24
-rw-r--r--vendor/scssphp/scssphp/src/Ast/Sass/SassReference.php50
-rw-r--r--vendor/scssphp/scssphp/src/Ast/Sass/Statement.php30
-rw-r--r--vendor/scssphp/scssphp/src/Ast/Sass/Statement/AtRootRule.php72
-rw-r--r--vendor/scssphp/scssphp/src/Ast/Sass/Statement/AtRule.php81
-rw-r--r--vendor/scssphp/scssphp/src/Ast/Sass/Statement/CallableDeclaration.php82
-rw-r--r--vendor/scssphp/scssphp/src/Ast/Sass/Statement/ContentBlock.php46
-rw-r--r--vendor/scssphp/scssphp/src/Ast/Sass/Statement/ContentRule.php64
-rw-r--r--vendor/scssphp/scssphp/src/Ast/Sass/Statement/DebugRule.php58
-rw-r--r--vendor/scssphp/scssphp/src/Ast/Sass/Statement/Declaration.php120
-rw-r--r--vendor/scssphp/scssphp/src/Ast/Sass/Statement/EachRule.php79
-rw-r--r--vendor/scssphp/scssphp/src/Ast/Sass/Statement/ElseClause.php26
-rw-r--r--vendor/scssphp/scssphp/src/Ast/Sass/Statement/ErrorRule.php58
-rw-r--r--vendor/scssphp/scssphp/src/Ast/Sass/Statement/ExtendRule.php72
-rw-r--r--vendor/scssphp/scssphp/src/Ast/Sass/Statement/ForRule.php91
-rw-r--r--vendor/scssphp/scssphp/src/Ast/Sass/Statement/FunctionRule.php43
-rw-r--r--vendor/scssphp/scssphp/src/Ast/Sass/Statement/HasContentVisitor.php31
-rw-r--r--vendor/scssphp/scssphp/src/Ast/Sass/Statement/IfClause.php40
-rw-r--r--vendor/scssphp/scssphp/src/Ast/Sass/Statement/IfRule.php95
-rw-r--r--vendor/scssphp/scssphp/src/Ast/Sass/Statement/IfRuleClause.php64
-rw-r--r--vendor/scssphp/scssphp/src/Ast/Sass/Statement/ImportRule.php65
-rw-r--r--vendor/scssphp/scssphp/src/Ast/Sass/Statement/IncludeRule.php141
-rw-r--r--vendor/scssphp/scssphp/src/Ast/Sass/Statement/LoudComment.php53
-rw-r--r--vendor/scssphp/scssphp/src/Ast/Sass/Statement/MediaRule.php67
-rw-r--r--vendor/scssphp/scssphp/src/Ast/Sass/Statement/MixinRule.php79
-rw-r--r--vendor/scssphp/scssphp/src/Ast/Sass/Statement/ParentStatement.php81
-rw-r--r--vendor/scssphp/scssphp/src/Ast/Sass/Statement/ReturnRule.php58
-rw-r--r--vendor/scssphp/scssphp/src/Ast/Sass/Statement/SilentComment.php55
-rw-r--r--vendor/scssphp/scssphp/src/Ast/Sass/Statement/StyleRule.php69
-rw-r--r--vendor/scssphp/scssphp/src/Ast/Sass/Statement/Stylesheet.php106
-rw-r--r--vendor/scssphp/scssphp/src/Ast/Sass/Statement/SupportsRule.php62
-rw-r--r--vendor/scssphp/scssphp/src/Ast/Sass/Statement/VariableDeclaration.php146
-rw-r--r--vendor/scssphp/scssphp/src/Ast/Sass/Statement/WarnRule.php58
-rw-r--r--vendor/scssphp/scssphp/src/Ast/Sass/Statement/WhileRule.php65
-rw-r--r--vendor/scssphp/scssphp/src/Ast/Sass/SupportsCondition.php22
-rw-r--r--vendor/scssphp/scssphp/src/Ast/Sass/SupportsCondition/SupportsAnything.php54
-rw-r--r--vendor/scssphp/scssphp/src/Ast/Sass/SupportsCondition/SupportsDeclaration.php80
-rw-r--r--vendor/scssphp/scssphp/src/Ast/Sass/SupportsCondition/SupportsFunction.php64
-rw-r--r--vendor/scssphp/scssphp/src/Ast/Sass/SupportsCondition/SupportsInterpolation.php53
-rw-r--r--vendor/scssphp/scssphp/src/Ast/Sass/SupportsCondition/SupportsNegation.php56
-rw-r--r--vendor/scssphp/scssphp/src/Ast/Sass/SupportsCondition/SupportsOperation.php80
-rw-r--r--vendor/scssphp/scssphp/src/Ast/Selector/AttributeOperator.php65
-rw-r--r--vendor/scssphp/scssphp/src/Ast/Selector/AttributeSelector.php123
-rw-r--r--vendor/scssphp/scssphp/src/Ast/Selector/ClassSelector.php58
-rw-r--r--vendor/scssphp/scssphp/src/Ast/Selector/Combinator.php49
-rw-r--r--vendor/scssphp/scssphp/src/Ast/Selector/ComplexSelector.php271
-rw-r--r--vendor/scssphp/scssphp/src/Ast/Selector/ComplexSelectorComponent.php99
-rw-r--r--vendor/scssphp/scssphp/src/Ast/Selector/CompoundSelector.php156
-rw-r--r--vendor/scssphp/scssphp/src/Ast/Selector/IDSelector.php74
-rw-r--r--vendor/scssphp/scssphp/src/Ast/Selector/IsBogusVisitor.php64
-rw-r--r--vendor/scssphp/scssphp/src/Ast/Selector/IsInvisibleVisitor.php69
-rw-r--r--vendor/scssphp/scssphp/src/Ast/Selector/IsUselessVisitor.php43
-rw-r--r--vendor/scssphp/scssphp/src/Ast/Selector/ParentSelector.php62
-rw-r--r--vendor/scssphp/scssphp/src/Ast/Selector/ParentSelectorVisitor.php20
-rw-r--r--vendor/scssphp/scssphp/src/Ast/Selector/PlaceholderSelector.php69
-rw-r--r--vendor/scssphp/scssphp/src/Ast/Selector/PseudoSelector.php347
-rw-r--r--vendor/scssphp/scssphp/src/Ast/Selector/QualifiedName.php65
-rw-r--r--vendor/scssphp/scssphp/src/Ast/Selector/Selector.php127
-rw-r--r--vendor/scssphp/scssphp/src/Ast/Selector/SelectorList.php355
-rw-r--r--vendor/scssphp/scssphp/src/Ast/Selector/SimpleSelector.php179
-rw-r--r--vendor/scssphp/scssphp/src/Ast/Selector/TypeSelector.php87
-rw-r--r--vendor/scssphp/scssphp/src/Ast/Selector/UniversalSelector.php110
-rw-r--r--vendor/scssphp/scssphp/src/Base/Range.php57
-rw-r--r--vendor/scssphp/scssphp/src/Block.php73
-rw-r--r--vendor/scssphp/scssphp/src/Block/AtRootBlock.php37
-rw-r--r--vendor/scssphp/scssphp/src/Block/CallableBlock.php46
-rw-r--r--vendor/scssphp/scssphp/src/Block/ContentBlock.php38
-rw-r--r--vendor/scssphp/scssphp/src/Block/DirectiveBlock.php37
-rw-r--r--vendor/scssphp/scssphp/src/Block/EachBlock.php37
-rw-r--r--vendor/scssphp/scssphp/src/Block/ElseBlock.php27
-rw-r--r--vendor/scssphp/scssphp/src/Block/ElseifBlock.php32
-rw-r--r--vendor/scssphp/scssphp/src/Block/ForBlock.php47
-rw-r--r--vendor/scssphp/scssphp/src/Block/IfBlock.php37
-rw-r--r--vendor/scssphp/scssphp/src/Block/MediaBlock.php37
-rw-r--r--vendor/scssphp/scssphp/src/Block/NestedPropertyBlock.php37
-rw-r--r--vendor/scssphp/scssphp/src/Block/WhileBlock.php32
-rw-r--r--vendor/scssphp/scssphp/src/Cache.php272
-rw-r--r--vendor/scssphp/scssphp/src/Collection/Map.php193
-rw-r--r--vendor/scssphp/scssphp/src/Colors.php50
-rw-r--r--vendor/scssphp/scssphp/src/CompilationResult.php35
-rw-r--r--vendor/scssphp/scssphp/src/Compiler.php10339
-rw-r--r--vendor/scssphp/scssphp/src/Compiler/CachedResult.php77
-rw-r--r--vendor/scssphp/scssphp/src/Compiler/Environment.php68
-rw-r--r--vendor/scssphp/scssphp/src/Compiler/LegacyValueVisitor.php117
-rw-r--r--vendor/scssphp/scssphp/src/Deprecation.php172
-rw-r--r--vendor/scssphp/scssphp/src/DeprecationStatus.php11
-rw-r--r--vendor/scssphp/scssphp/src/Evaluation/ArgumentResults.php105
-rw-r--r--vendor/scssphp/scssphp/src/Evaluation/Environment.php551
-rw-r--r--vendor/scssphp/scssphp/src/Evaluation/EvaluateResult.php53
-rw-r--r--vendor/scssphp/scssphp/src/Evaluation/EvaluateVisitor.php3529
-rw-r--r--vendor/scssphp/scssphp/src/Evaluation/EvaluationContext.php82
-rw-r--r--vendor/scssphp/scssphp/src/Evaluation/LoadedStylesheet.php61
-rw-r--r--vendor/scssphp/scssphp/src/Evaluation/VisitorEvaluationContext.php62
-rw-r--r--vendor/scssphp/scssphp/src/Exception/MultiSpanSassException.php73
-rw-r--r--vendor/scssphp/scssphp/src/Exception/MultiSpanSassFormatException.php23
-rw-r--r--vendor/scssphp/scssphp/src/Exception/MultiSpanSassRuntimeException.php41
-rw-r--r--vendor/scssphp/scssphp/src/Exception/MultiSpanSassScriptException.php48
-rw-r--r--vendor/scssphp/scssphp/src/Exception/ParserException.php58
-rw-r--r--vendor/scssphp/scssphp/src/Exception/SassException.php47
-rw-r--r--vendor/scssphp/scssphp/src/Exception/SassFormatException.php (renamed from vendor/scssphp/scssphp/src/Exception/RangeException.php)6
-rw-r--r--vendor/scssphp/scssphp/src/Exception/SassRuntimeException.php23
-rw-r--r--vendor/scssphp/scssphp/src/Exception/SassScriptException.php23
-rw-r--r--vendor/scssphp/scssphp/src/Exception/ServerException.php26
-rw-r--r--vendor/scssphp/scssphp/src/Exception/SimpleSassException.php61
-rw-r--r--vendor/scssphp/scssphp/src/Exception/SimpleSassFormatException.php63
-rw-r--r--vendor/scssphp/scssphp/src/Exception/SimpleSassRuntimeException.php74
-rw-r--r--vendor/scssphp/scssphp/src/Extend/ComplexSelectorMap.php41
-rw-r--r--vendor/scssphp/scssphp/src/Extend/ConcreteExtensionStore.php1245
-rw-r--r--vendor/scssphp/scssphp/src/Extend/EmptyExtensionStore.php66
-rw-r--r--vendor/scssphp/scssphp/src/Extend/ExtendMode.php44
-rw-r--r--vendor/scssphp/scssphp/src/Extend/ExtendUtil.php1292
-rw-r--r--vendor/scssphp/scssphp/src/Extend/Extender.php86
-rw-r--r--vendor/scssphp/scssphp/src/Extend/Extension.php68
-rw-r--r--vendor/scssphp/scssphp/src/Extend/ExtensionStore.php63
-rw-r--r--vendor/scssphp/scssphp/src/Extend/MergedExtension.php83
-rw-r--r--vendor/scssphp/scssphp/src/Extend/ObjectSet.php59
-rw-r--r--vendor/scssphp/scssphp/src/Extend/SimpleSelectorMap.php31
-rw-r--r--vendor/scssphp/scssphp/src/Formatter.php377
-rw-r--r--vendor/scssphp/scssphp/src/Formatter/Compact.php52
-rw-r--r--vendor/scssphp/scssphp/src/Formatter/Compressed.php83
-rw-r--r--vendor/scssphp/scssphp/src/Formatter/Crunched.php87
-rw-r--r--vendor/scssphp/scssphp/src/Formatter/Debug.php127
-rw-r--r--vendor/scssphp/scssphp/src/Formatter/Expanded.php72
-rw-r--r--vendor/scssphp/scssphp/src/Formatter/Nested.php238
-rw-r--r--vendor/scssphp/scssphp/src/Formatter/OutputBlock.php68
-rw-r--r--vendor/scssphp/scssphp/src/Function/ColorFunctions.php886
-rw-r--r--vendor/scssphp/scssphp/src/Function/FunctionRegistry.php182
-rw-r--r--vendor/scssphp/scssphp/src/Function/ListFunctions.php183
-rw-r--r--vendor/scssphp/scssphp/src/Function/MapFunctions.php213
-rw-r--r--vendor/scssphp/scssphp/src/Function/MathFunctions.php205
-rw-r--r--vendor/scssphp/scssphp/src/Function/MetaFunctions.php96
-rw-r--r--vendor/scssphp/scssphp/src/Function/SelectorFunctions.php204
-rw-r--r--vendor/scssphp/scssphp/src/Function/StringFunctions.php219
-rw-r--r--vendor/scssphp/scssphp/src/Importer/CanonicalizeContext.php76
-rw-r--r--vendor/scssphp/scssphp/src/Importer/CanonicalizeResult.php28
-rw-r--r--vendor/scssphp/scssphp/src/Importer/FilesystemImporter.php92
-rw-r--r--vendor/scssphp/scssphp/src/Importer/ImportCache.php297
-rw-r--r--vendor/scssphp/scssphp/src/Importer/ImportContext.php84
-rw-r--r--vendor/scssphp/scssphp/src/Importer/ImportUtil.php141
-rw-r--r--vendor/scssphp/scssphp/src/Importer/Importer.php170
-rw-r--r--vendor/scssphp/scssphp/src/Importer/ImporterResult.php59
-rw-r--r--vendor/scssphp/scssphp/src/Importer/LegacyCallbackImporter.php66
-rw-r--r--vendor/scssphp/scssphp/src/Importer/NoOpImporter.php44
-rw-r--r--vendor/scssphp/scssphp/src/Importer/SpecialCacheValue.php21
-rw-r--r--vendor/scssphp/scssphp/src/Logger/DeprecationProcessingLogger.php195
-rw-r--r--vendor/scssphp/scssphp/src/Logger/LoggerInterface.php25
-rw-r--r--vendor/scssphp/scssphp/src/Logger/QuietLogger.php13
-rw-r--r--vendor/scssphp/scssphp/src/Logger/StreamLogger.php47
-rw-r--r--vendor/scssphp/scssphp/src/Node/Number.php35
-rw-r--r--vendor/scssphp/scssphp/src/OutputStyle.php42
-rw-r--r--vendor/scssphp/scssphp/src/Parser.php4220
-rw-r--r--vendor/scssphp/scssphp/src/Parser/AtRootQueryParser.php54
-rw-r--r--vendor/scssphp/scssphp/src/Parser/CssParser.php187
-rw-r--r--vendor/scssphp/scssphp/src/Parser/FormatException.php36
-rw-r--r--vendor/scssphp/scssphp/src/Parser/InterpolationBuffer.php106
-rw-r--r--vendor/scssphp/scssphp/src/Parser/InterpolationMap.php220
-rw-r--r--vendor/scssphp/scssphp/src/Parser/KeyframeSelectorParser.php103
-rw-r--r--vendor/scssphp/scssphp/src/Parser/LineScanner.php181
-rw-r--r--vendor/scssphp/scssphp/src/Parser/MediaQueryParser.php153
-rw-r--r--vendor/scssphp/scssphp/src/Parser/MultiSourceFormatException.php40
-rw-r--r--vendor/scssphp/scssphp/src/Parser/Parser.php1021
-rw-r--r--vendor/scssphp/scssphp/src/Parser/SassParser.php568
-rw-r--r--vendor/scssphp/scssphp/src/Parser/ScssParser.php277
-rw-r--r--vendor/scssphp/scssphp/src/Parser/SelectorParser.php614
-rw-r--r--vendor/scssphp/scssphp/src/Parser/StringScanner.php309
-rw-r--r--vendor/scssphp/scssphp/src/Parser/StylesheetParser.php4356
-rw-r--r--vendor/scssphp/scssphp/src/SassCallable/BuiltInCallable.php199
-rw-r--r--vendor/scssphp/scssphp/src/SassCallable/PlainCssCallable.php42
-rw-r--r--vendor/scssphp/scssphp/src/SassCallable/SassCallable.php71
-rw-r--r--vendor/scssphp/scssphp/src/SassCallable/UserDefinedCallable.php57
-rw-r--r--vendor/scssphp/scssphp/src/Serializer/SerializeResult.php29
-rw-r--r--vendor/scssphp/scssphp/src/Serializer/SerializeVisitor.php1904
-rw-r--r--vendor/scssphp/scssphp/src/Serializer/Serializer.php82
-rw-r--r--vendor/scssphp/scssphp/src/Serializer/SimpleStringBuffer.php60
-rw-r--r--vendor/scssphp/scssphp/src/Serializer/SourceMapBuffer.php43
-rw-r--r--vendor/scssphp/scssphp/src/Serializer/StringBuffer.php31
-rw-r--r--vendor/scssphp/scssphp/src/Serializer/TrackingSourceMapBuffer.php194
-rw-r--r--vendor/scssphp/scssphp/src/SourceMap/Base64.php94
-rw-r--r--vendor/scssphp/scssphp/src/SourceMap/Base64VLQ.php70
-rw-r--r--vendor/scssphp/scssphp/src/SourceMap/Builder/Entry.php62
-rw-r--r--vendor/scssphp/scssphp/src/SourceMap/SingleMapping.php197
-rw-r--r--vendor/scssphp/scssphp/src/SourceMap/SourceMapGenerator.php390
-rw-r--r--vendor/scssphp/scssphp/src/SourceMap/TargetEntry.php29
-rw-r--r--vendor/scssphp/scssphp/src/SourceMap/TargetLineEntry.php28
-rw-r--r--vendor/scssphp/scssphp/src/SourceSpan/LazyFileSpan.php131
-rw-r--r--vendor/scssphp/scssphp/src/SourceSpan/MultiSpan.php127
-rw-r--r--vendor/scssphp/scssphp/src/StackTrace/Frame.php112
-rw-r--r--vendor/scssphp/scssphp/src/StackTrace/Trace.php53
-rw-r--r--vendor/scssphp/scssphp/src/Syntax.php44
-rw-r--r--vendor/scssphp/scssphp/src/Type.php178
-rw-r--r--vendor/scssphp/scssphp/src/Util.php183
-rw-r--r--vendor/scssphp/scssphp/src/Util/ArrayUtil.php48
-rw-r--r--vendor/scssphp/scssphp/src/Util/AstUtil.php39
-rw-r--r--vendor/scssphp/scssphp/src/Util/Box.php50
-rw-r--r--vendor/scssphp/scssphp/src/Util/Character.php169
-rw-r--r--vendor/scssphp/scssphp/src/Util/Equatable.php21
-rw-r--r--vendor/scssphp/scssphp/src/Util/EquatableUtil.php88
-rw-r--r--vendor/scssphp/scssphp/src/Util/ErrorUtil.php97
-rw-r--r--vendor/scssphp/scssphp/src/Util/IterableUtil.php98
-rw-r--r--vendor/scssphp/scssphp/src/Util/ListUtil.php157
-rw-r--r--vendor/scssphp/scssphp/src/Util/LoggerUtil.php34
-rw-r--r--vendor/scssphp/scssphp/src/Util/MakeExpressionCalculationSafe.php71
-rw-r--r--vendor/scssphp/scssphp/src/Util/ModifiableBox.php67
-rw-r--r--vendor/scssphp/scssphp/src/Util/NumberUtil.php305
-rw-r--r--vendor/scssphp/scssphp/src/Util/ParserUtil.php68
-rw-r--r--vendor/scssphp/scssphp/src/Util/Path.php238
-rw-r--r--vendor/scssphp/scssphp/src/Util/SpanUtil.php148
-rw-r--r--vendor/scssphp/scssphp/src/Util/StringUtil.php181
-rw-r--r--vendor/scssphp/scssphp/src/Util/UriUtil.php40
-rw-r--r--vendor/scssphp/scssphp/src/Value/CalculationOperation.php77
-rw-r--r--vendor/scssphp/scssphp/src/Value/CalculationOperator.php49
-rw-r--r--vendor/scssphp/scssphp/src/Value/ColorFormat.php23
-rw-r--r--vendor/scssphp/scssphp/src/Value/ColorFormatEnum.php28
-rw-r--r--vendor/scssphp/scssphp/src/Value/ComplexSassNumber.php92
-rw-r--r--vendor/scssphp/scssphp/src/Value/ListSeparator.php34
-rw-r--r--vendor/scssphp/scssphp/src/Value/SassArgumentList.php60
-rw-r--r--vendor/scssphp/scssphp/src/Value/SassBoolean.php75
-rw-r--r--vendor/scssphp/scssphp/src/Value/SassCalculation.php1153
-rw-r--r--vendor/scssphp/scssphp/src/Value/SassColor.php407
-rw-r--r--vendor/scssphp/scssphp/src/Value/SassFunction.php59
-rw-r--r--vendor/scssphp/scssphp/src/Value/SassList.php139
-rw-r--r--vendor/scssphp/scssphp/src/Value/SassMap.php127
-rw-r--r--vendor/scssphp/scssphp/src/Value/SassMixin.php62
-rw-r--r--vendor/scssphp/scssphp/src/Value/SassNull.php62
-rw-r--r--vendor/scssphp/scssphp/src/Value/SassNumber.php1044
-rw-r--r--vendor/scssphp/scssphp/src/Value/SassString.php236
-rw-r--r--vendor/scssphp/scssphp/src/Value/SingleUnitSassNumber.php333
-rw-r--r--vendor/scssphp/scssphp/src/Value/SpanColorFormat.php33
-rw-r--r--vendor/scssphp/scssphp/src/Value/UnitlessSassNumber.php223
-rw-r--r--vendor/scssphp/scssphp/src/Value/Value.php683
-rw-r--r--vendor/scssphp/scssphp/src/ValueConverter.php60
-rw-r--r--vendor/scssphp/scssphp/src/Version.php4
-rw-r--r--vendor/scssphp/scssphp/src/Visitor/AnySelectorVisitor.php97
-rw-r--r--vendor/scssphp/scssphp/src/Visitor/CssVisitor.php79
-rw-r--r--vendor/scssphp/scssphp/src/Visitor/EveryCssVisitor.php82
-rw-r--r--vendor/scssphp/scssphp/src/Visitor/ExpressionVisitor.php126
-rw-r--r--vendor/scssphp/scssphp/src/Visitor/ModifiableCssVisitor.php78
-rw-r--r--vendor/scssphp/scssphp/src/Visitor/ReplaceExpressionVisitor.php216
-rw-r--r--vendor/scssphp/scssphp/src/Visitor/SelectorSearchVisitor.php92
-rw-r--r--vendor/scssphp/scssphp/src/Visitor/SelectorVisitor.php90
-rw-r--r--vendor/scssphp/scssphp/src/Visitor/StatementSearchVisitor.php230
-rw-r--r--vendor/scssphp/scssphp/src/Visitor/StatementVisitor.php174
-rw-r--r--vendor/scssphp/scssphp/src/Visitor/ValueVisitor.php83
-rw-r--r--vendor/scssphp/scssphp/src/Warn.php56
-rw-r--r--vendor/scssphp/source-span/LICENSE.md20
-rw-r--r--vendor/scssphp/source-span/README.md22
-rw-r--r--vendor/scssphp/source-span/composer.json42
-rw-r--r--vendor/scssphp/source-span/src/ConcreteFileSpan.php156
-rw-r--r--vendor/scssphp/source-span/src/FileLocation.php52
-rw-r--r--vendor/scssphp/source-span/src/FileSpan.php20
-rw-r--r--vendor/scssphp/source-span/src/Highlighter/AsciiGlyph.php18
-rw-r--r--vendor/scssphp/source-span/src/Highlighter/Highlight.php205
-rw-r--r--vendor/scssphp/source-span/src/Highlighter/Highlighter.php538
-rw-r--r--vendor/scssphp/source-span/src/Highlighter/Line.php40
-rw-r--r--vendor/scssphp/source-span/src/SimpleSourceLocation.php59
-rw-r--r--vendor/scssphp/source-span/src/SimpleSourceSpan.php53
-rw-r--r--vendor/scssphp/source-span/src/SimpleSourceSpanWithContext.php68
-rw-r--r--vendor/scssphp/source-span/src/SourceFile.php272
-rw-r--r--vendor/scssphp/source-span/src/SourceLocation.php47
-rw-r--r--vendor/scssphp/source-span/src/SourceLocationMixin.php34
-rw-r--r--vendor/scssphp/source-span/src/SourceSpan.php110
-rw-r--r--vendor/scssphp/source-span/src/SourceSpanMixin.php132
-rw-r--r--vendor/scssphp/source-span/src/SourceSpanWithContext.php16
-rw-r--r--vendor/scssphp/source-span/src/Util.php358
330 files changed, 44608 insertions, 17735 deletions
diff --git a/vendor/scssphp/scssphp/LICENSE.md b/vendor/scssphp/scssphp/LICENSE.md
index afcfdfb26..91450be0c 100644
--- a/vendor/scssphp/scssphp/LICENSE.md
+++ b/vendor/scssphp/scssphp/LICENSE.md
@@ -1,5 +1,5 @@
-Copyright (c) 2015 Leaf Corcoran, http://scssphp.github.io/scssphp
-
+Copyright (c) 2015 Leaf Corcoran, https://scssphp.github.io/scssphp
+
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
@@ -7,10 +7,10 @@ without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
-
+
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
-
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
diff --git a/vendor/scssphp/scssphp/README.md b/vendor/scssphp/scssphp/README.md
index 65bb93ea7..acf4723ed 100644
--- a/vendor/scssphp/scssphp/README.md
+++ b/vendor/scssphp/scssphp/README.md
@@ -44,7 +44,7 @@ To enable the full `sass-spec` compatibility tests:
Run the following command from the root directory to check the code for "sniffs".
- vendor/bin/phpcs --standard=PSR12 --extensions=php bin src tests *.php
+ vendor/bin/phpcs --standard=PSR12 --extensions=php bin src tests
## Static Analysis
diff --git a/vendor/scssphp/scssphp/bin/pscss b/vendor/scssphp/scssphp/bin/pscss
deleted file mode 100755
index 0f009d6bd..000000000
--- a/vendor/scssphp/scssphp/bin/pscss
+++ /dev/null
@@ -1,244 +0,0 @@
-#!/usr/bin/env php
-<?php
-
-/**
- * SCSSPHP
- *
- * @copyright 2012-2020 Leaf Corcoran
- *
- * @license http://opensource.org/licenses/MIT MIT
- *
- * @link http://scssphp.github.io/scssphp
- */
-
-error_reporting(E_ALL);
-
-if (version_compare(PHP_VERSION, '5.6') < 0) {
- die('Requires PHP 5.6 or above');
-}
-
-include __DIR__ . '/../scss.inc.php';
-
-use ScssPhp\ScssPhp\Compiler;
-use ScssPhp\ScssPhp\Exception\SassException;
-use ScssPhp\ScssPhp\OutputStyle;
-use ScssPhp\ScssPhp\Parser;
-use ScssPhp\ScssPhp\Version;
-
-$style = null;
-$loadPaths = [];
-$dumpTree = false;
-$inputFile = null;
-$changeDir = false;
-$encoding = false;
-$sourceMap = false;
-$embedSources = false;
-$embedSourceMap = false;
-
-/**
- * Parse argument
- *
- * @param int $i
- * @param string[] $options
- *
- * @return string|null
- */
-function parseArgument(&$i, $options) {
- global $argc;
- global $argv;
-
- if (! preg_match('/^(?:' . implode('|', (array) $options) . ')=?(.*)/', $argv[$i], $matches)) {
- return;
- }
-
- if (strlen($matches[1])) {
- return $matches[1];
- }
-
- if ($i + 1 < $argc) {
- $i++;
-
- return $argv[$i];
- }
-}
-
-$arguments = [];
-
-for ($i = 1; $i < $argc; $i++) {
- if ($argv[$i] === '-?' || $argv[$i] === '-h' || $argv[$i] === '--help') {
- $exe = $argv[0];
-
- $HELP = <<<EOT
-Usage: $exe [options] [input-file] [output-file]
-
-Options include:
-
- --help Show this message [-h, -?]
- --continue-on-error [deprecated] Ignored
- --debug-info [deprecated] Ignored [-g]
- --dump-tree [deprecated] Dump formatted parse tree [-T]
- --iso8859-1 Use iso8859-1 encoding instead of default utf-8
- --line-numbers [deprecated] Ignored [--line-comments]
- --load-path=PATH Set import path [-I]
- --precision=N [deprecated] Ignored. (default 10) [-p]
- --sourcemap Create source map file
- --embed-sources Embed source file contents in source maps
- --embed-source-map Embed the source map contents in CSS (default if writing to stdout)
- --style=FORMAT Set the output style (compressed or expanded) [-s, -t]
- --version Print the version [-v]
-
-EOT;
- exit($HELP);
- }
-
- if ($argv[$i] === '-v' || $argv[$i] === '--version') {
- exit(Version::VERSION . "\n");
- }
-
- // Keep parsing --continue-on-error to avoid BC breaks for scripts using it
- if ($argv[$i] === '--continue-on-error') {
- // TODO report it as a warning ?
- continue;
- }
-
- // Keep parsing it to avoid BC breaks for scripts using it
- if ($argv[$i] === '-g' || $argv[$i] === '--debug-info') {
- // TODO report it as a warning ?
- continue;
- }
-
- if ($argv[$i] === '--iso8859-1') {
- $encoding = 'iso8859-1';
- continue;
- }
-
- // Keep parsing it to avoid BC breaks for scripts using it
- if ($argv[$i] === '--line-numbers' || $argv[$i] === '--line-comments') {
- // TODO report it as a warning ?
- continue;
- }
-
- if ($argv[$i] === '--sourcemap') {
- $sourceMap = true;
- continue;
- }
-
- if ($argv[$i] === '--embed-sources') {
- $embedSources = true;
- continue;
- }
-
- if ($argv[$i] === '--embed-source-map') {
- $embedSourceMap = true;
- continue;
- }
-
- if ($argv[$i] === '-T' || $argv[$i] === '--dump-tree') {
- $dumpTree = true;
- continue;
- }
-
- $value = parseArgument($i, array('-t', '-s', '--style'));
-
- if (isset($value)) {
- $style = $value;
- continue;
- }
-
- $value = parseArgument($i, array('-I', '--load-path'));
-
- if (isset($value)) {
- $loadPaths[] = $value;
- continue;
- }
-
- // Keep parsing --precision to avoid BC breaks for scripts using it
- $value = parseArgument($i, array('-p', '--precision'));
-
- if (isset($value)) {
- // TODO report it as a warning ?
- continue;
- }
-
- $arguments[] = $argv[$i];
-}
-
-
-if (isset($arguments[0]) && file_exists($arguments[0])) {
- $inputFile = $arguments[0];
- $data = file_get_contents($inputFile);
-} else {
- $data = '';
-
- while (! feof(STDIN)) {
- $data .= fread(STDIN, 8192);
- }
-}
-
-if ($dumpTree) {
- $parser = new Parser($inputFile);
-
- print_r(json_decode(json_encode($parser->parse($data)), true));
-
- fwrite(STDERR, 'Warning: the --dump-tree option is deprecated. Use proper debugging tools instead.');
-
- exit();
-}
-
-$scss = new Compiler();
-
-if ($loadPaths) {
- $scss->setImportPaths($loadPaths);
-}
-
-if ($style) {
- if ($style === OutputStyle::COMPRESSED || $style === OutputStyle::EXPANDED) {
- $scss->setOutputStyle($style);
- } else {
- fwrite(STDERR, "WARNING: the $style style is deprecated.\n");
- $scss->setFormatter('ScssPhp\\ScssPhp\\Formatter\\' . ucfirst($style));
- }
-}
-
-$outputFile = isset($arguments[1]) ? $arguments[1] : null;
-$sourceMapFile = null;
-
-if ($sourceMap) {
- $sourceMapOptions = array(
- 'outputSourceFiles' => $embedSources,
- );
- if ($embedSourceMap || $outputFile === null) {
- $scss->setSourceMap(Compiler::SOURCE_MAP_INLINE);
- } else {
- $sourceMapFile = $outputFile . '.map';
- $sourceMapOptions['sourceMapWriteTo'] = $sourceMapFile;
- $sourceMapOptions['sourceMapURL'] = basename($sourceMapFile);
- $sourceMapOptions['sourceMapBasepath'] = getcwd();
- $sourceMapOptions['sourceMapFilename'] = basename($outputFile);
-
- $scss->setSourceMap(Compiler::SOURCE_MAP_FILE);
- }
-
- $scss->setSourceMapOptions($sourceMapOptions);
-}
-
-if ($encoding) {
- $scss->setEncoding($encoding);
-}
-
-try {
- $result = $scss->compileString($data, $inputFile);
-} catch (SassException $e) {
- fwrite(STDERR, 'Error: '.$e->getMessage()."\n");
- exit(1);
-}
-
-if ($outputFile) {
- file_put_contents($outputFile, $result->getCss());
-
- if ($sourceMapFile !== null && $result->getSourceMap() !== null) {
- file_put_contents($sourceMapFile, $result->getSourceMap());
- }
-} else {
- echo $result->getCss();
-}
diff --git a/vendor/scssphp/scssphp/composer.json b/vendor/scssphp/scssphp/composer.json
index d17ffb924..599b58fd1 100644
--- a/vendor/scssphp/scssphp/composer.json
+++ b/vendor/scssphp/scssphp/composer.json
@@ -26,20 +26,25 @@
"psr-4": { "ScssPhp\\ScssPhp\\Tests\\": "tests/" }
},
"require": {
- "php": ">=5.6.0",
+ "php": ">=8.1",
+ "ext-ctype": "*",
"ext-json": "*",
- "ext-ctype": "*"
+ "league/uri": "^7.4",
+ "league/uri-interfaces": "^7.4",
+ "scssphp/source-span": "^1.0",
+ "symfony/filesystem": "^5.4 || ^6.0 || ^7.0",
+ "symfony/polyfill-mbstring": "^1.30"
},
"suggest": {
- "ext-mbstring": "For best performance, mbstring should be installed as it is faster than ext-iconv",
- "ext-iconv": "Can be used as fallback when ext-mbstring is not available"
+ "ext-mbstring": "For best performance, mbstring should be installed as it is faster than the polyfill"
},
"require-dev": {
"bamarni/composer-bin-plugin": "^1.4",
- "phpunit/phpunit": "^5.7 || ^6.5 || ^7.5 || ^8.3 || ^9.4",
+ "phpunit/phpunit": "^9.5.6",
"sass/sass-spec": "*",
"squizlabs/php_codesniffer": "~3.5",
"symfony/phpunit-bridge": "^5.1",
+ "symfony/var-dumper": "^6.3",
"thoughtbot/bourbon": "^7.0",
"twbs/bootstrap": "~5.0",
"twbs/bootstrap4": "4.6.1",
@@ -50,16 +55,16 @@
"type": "package",
"package": {
"name": "sass/sass-spec",
- "version": "2022.08.19",
+ "version": "2024.06.24",
"source": {
"type": "git",
"url": "https://github.com/sass/sass-spec.git",
- "reference": "2bdc199723a3445d5badac3ac774105698f08861"
+ "reference": "7ac806618da724333c60ad7b9c16b969470b9302"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/sass/sass-spec/zipball/2bdc199723a3445d5badac3ac774105698f08861",
- "reference": "2bdc199723a3445d5badac3ac774105698f08861",
+ "url": "https://api.github.com/repos/sass/sass-spec/zipball/7ac806618da724333c60ad7b9c16b969470b9302",
+ "reference": "7ac806618da724333c60ad7b9c16b969470b9302",
"shasum": ""
}
}
@@ -101,7 +106,6 @@
}
}
],
- "bin": ["bin/pscss"],
"config": {
"sort-packages": true,
"allow-plugins": {
diff --git a/vendor/scssphp/scssphp/scss.inc.php b/vendor/scssphp/scssphp/scss.inc.php
deleted file mode 100644
index 459837805..000000000
--- a/vendor/scssphp/scssphp/scss.inc.php
+++ /dev/null
@@ -1,21 +0,0 @@
-<?php
-
-if (version_compare(PHP_VERSION, '5.6') < 0) {
- throw new \Exception('scssphp requires PHP 5.6 or above');
-}
-
-if (! class_exists('ScssPhp\ScssPhp\Version')) {
- spl_autoload_register(function ($class) {
- if (0 !== strpos($class, 'ScssPhp\ScssPhp\\')) {
- // Not a ScssPhp class
- return;
- }
-
- $subClass = substr($class, strlen('ScssPhp\ScssPhp\\'));
- $path = __DIR__ . '/src/' . str_replace('\\', '/', $subClass) . '.php';
-
- if (file_exists($path)) {
- require $path;
- }
- });
-}
diff --git a/vendor/scssphp/scssphp/src/Ast/AstNode.php b/vendor/scssphp/scssphp/src/Ast/AstNode.php
new file mode 100644
index 000000000..2acf6c641
--- /dev/null
+++ b/vendor/scssphp/scssphp/src/Ast/AstNode.php
@@ -0,0 +1,25 @@
+<?php
+
+/**
+ * SCSSPHP
+ *
+ * @copyright 2012-2020 Leaf Corcoran
+ *
+ * @license http://opensource.org/licenses/MIT MIT
+ *
+ * @link http://scssphp.github.io/scssphp
+ */
+
+namespace ScssPhp\ScssPhp\Ast;
+
+use SourceSpan\FileSpan;
+
+/**
+ * A node in an abstract syntax tree.
+ *
+ * @internal
+ */
+interface AstNode extends \Stringable
+{
+ public function getSpan(): FileSpan;
+}
diff --git a/vendor/scssphp/scssphp/src/Ast/Css/CssAtRule.php b/vendor/scssphp/scssphp/src/Ast/Css/CssAtRule.php
new file mode 100644
index 000000000..64667366a
--- /dev/null
+++ b/vendor/scssphp/scssphp/src/Ast/Css/CssAtRule.php
@@ -0,0 +1,35 @@
+<?php
+
+/**
+ * SCSSPHP
+ *
+ * @copyright 2012-2020 Leaf Corcoran
+ *
+ * @license http://opensource.org/licenses/MIT MIT
+ *
+ * @link http://scssphp.github.io/scssphp
+ */
+
+namespace ScssPhp\ScssPhp\Ast\Css;
+
+/**
+ * An unknown plain CSS at-rule.
+ *
+ * @internal
+ */
+interface CssAtRule extends CssParentNode
+{
+ /**
+ * The name of this rule.
+ *
+ * @return CssValue<string>
+ */
+ public function getName(): CssValue;
+
+ /**
+ * The value of this rule.
+ *
+ * @return CssValue<string>|null
+ */
+ public function getValue(): ?CssValue;
+}
diff --git a/vendor/scssphp/scssphp/src/Ast/Css/CssComment.php b/vendor/scssphp/scssphp/src/Ast/Css/CssComment.php
new file mode 100644
index 000000000..27a828551
--- /dev/null
+++ b/vendor/scssphp/scssphp/src/Ast/Css/CssComment.php
@@ -0,0 +1,34 @@
+<?php
+
+/**
+ * SCSSPHP
+ *
+ * @copyright 2012-2020 Leaf Corcoran
+ *
+ * @license http://opensource.org/licenses/MIT MIT
+ *
+ * @link http://scssphp.github.io/scssphp
+ */
+
+namespace ScssPhp\ScssPhp\Ast\Css;
+
+/**
+ * A plain CSS comment.
+ *
+ * This is always a multi-line comment.
+ *
+ * @internal
+ */
+interface CssComment extends CssNode
+{
+ /**
+ * The contents of this comment, including `/*` and `* /`.
+ */
+ public function getText(): string;
+
+ /**
+ * Whether this comment starts with `/*!` and so should be preserved even in
+ * compressed mode.
+ */
+ public function isPreserved(): bool;
+}
diff --git a/vendor/scssphp/scssphp/src/Ast/Css/CssDeclaration.php b/vendor/scssphp/scssphp/src/Ast/Css/CssDeclaration.php
new file mode 100644
index 000000000..1702a72a3
--- /dev/null
+++ b/vendor/scssphp/scssphp/src/Ast/Css/CssDeclaration.php
@@ -0,0 +1,84 @@
+<?php
+
+/**
+ * SCSSPHP
+ *
+ * @copyright 2012-2020 Leaf Corcoran
+ *
+ * @license http://opensource.org/licenses/MIT MIT
+ *
+ * @link http://scssphp.github.io/scssphp
+ */
+
+namespace ScssPhp\ScssPhp\Ast\Css;
+
+use ScssPhp\ScssPhp\StackTrace\Trace;
+use ScssPhp\ScssPhp\Value\Value;
+use SourceSpan\FileSpan;
+
+/**
+ * A plain CSS declaration (that is, a `name: value` pair).
+ *
+ * @internal
+ */
+interface CssDeclaration extends CssNode
+{
+ /**
+ * The name of this declaration.
+ *
+ * @return CssValue<string>
+ */
+ public function getName(): CssValue;
+
+ /**
+ * The value of this declaration.
+ *
+ * @return CssValue<Value>
+ */
+ public function getValue(): CssValue;
+
+ /**
+ * A list of style rules that appeared before this declaration in the Sass
+ * input but after it in the CSS output.
+ *
+ * These are used to emit mixed declaration deprecation warnings during
+ * serialization, so we can check based on specificity whether the warnings
+ * are really necessary without worrying about `@extend` potentially changing
+ * things up.
+ *
+ * @return list<CssStyleRule>
+ */
+ public function getInterleavedRules(): array;
+
+ /**
+ * The stack trace indicating where this node was created.
+ *
+ * This is used to emit interleaved declaration warnings, and only needs to be set if
+ * {@see getInterleavedRules} isn't empty.
+ */
+ public function getTrace(): ?Trace;
+
+ /**
+ * 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/vendor/scssphp/scssphp/src/Ast/Css/CssImport.php b/vendor/scssphp/scssphp/src/Ast/Css/CssImport.php
new file mode 100644
index 000000000..59562ec64
--- /dev/null
+++ b/vendor/scssphp/scssphp/src/Ast/Css/CssImport.php
@@ -0,0 +1,37 @@
+<?php
+
+/**
+ * SCSSPHP
+ *
+ * @copyright 2012-2020 Leaf Corcoran
+ *
+ * @license http://opensource.org/licenses/MIT MIT
+ *
+ * @link http://scssphp.github.io/scssphp
+ */
+
+namespace ScssPhp\ScssPhp\Ast\Css;
+
+/**
+ * A plain CSS `@import`.
+ *
+ * @internal
+ */
+interface CssImport extends CssNode
+{
+ /**
+ * The URL being imported.
+ *
+ * This includes quotes.
+ *
+ * @return CssValue<string>
+ */
+ public function getUrl(): CssValue;
+
+ /**
+ * The modifiers (such as media or supports queries) attached to this import.
+ *
+ * @return CssValue<string>|null
+ */
+ public function getModifiers(): ?CssValue;
+}
diff --git a/vendor/scssphp/scssphp/src/Ast/Css/CssKeyframeBlock.php b/vendor/scssphp/scssphp/src/Ast/Css/CssKeyframeBlock.php
new file mode 100644
index 000000000..8a4f3f938
--- /dev/null
+++ b/vendor/scssphp/scssphp/src/Ast/Css/CssKeyframeBlock.php
@@ -0,0 +1,30 @@
+<?php
+
+/**
+ * SCSSPHP
+ *
+ * @copyright 2012-2020 Leaf Corcoran
+ *
+ * @license http://opensource.org/licenses/MIT MIT
+ *
+ * @link http://scssphp.github.io/scssphp
+ */
+
+namespace ScssPhp\ScssPhp\Ast\Css;
+
+/**
+ * A block within a `@keyframes` rule.
+ *
+ * For example, `10% {opacity: 0.5}`.
+ *
+ * @internal
+ */
+interface CssKeyframeBlock extends CssParentNode
+{
+ /**
+ * The selector for this block.
+ *
+ * @return CssValue<list<string>>
+ */
+ public function getSelector(): CssValue;
+}
diff --git a/vendor/scssphp/scssphp/src/Ast/Css/CssMediaQuery.php b/vendor/scssphp/scssphp/src/Ast/Css/CssMediaQuery.php
new file mode 100644
index 000000000..53ac9b2ee
--- /dev/null
+++ b/vendor/scssphp/scssphp/src/Ast/Css/CssMediaQuery.php
@@ -0,0 +1,250 @@
+<?php
+
+/**
+ * SCSSPHP
+ *
+ * @copyright 2012-2020 Leaf Corcoran
+ *
+ * @license http://opensource.org/licenses/MIT MIT
+ *
+ * @link http://scssphp.github.io/scssphp
+ */
+
+namespace ScssPhp\ScssPhp\Ast\Css;
+
+use League\Uri\Contracts\UriInterface;
+use ScssPhp\ScssPhp\Exception\SassFormatException;
+use ScssPhp\ScssPhp\Logger\LoggerInterface;
+use ScssPhp\ScssPhp\Parser\InterpolationMap;
+use ScssPhp\ScssPhp\Parser\MediaQueryParser;
+use ScssPhp\ScssPhp\Util\Equatable;
+
+/**
+ * A plain CSS media query, as used in `@media` and `@import`.
+ *
+ * @internal
+ */
+final class CssMediaQuery implements MediaQueryMergeResult, Equatable
+{
+ /**
+ * The modifier, probably either "not" or "only".
+ *
+ * This may be `null` if no modifier is in use.
+ */
+ private readonly ?string $modifier;
+
+ /**
+ * The media type, for example "screen" or "print".
+ *
+ * This may be `null`. If so, {@see $conditions} will not be empty.
+ */
+ private readonly ?string $type;
+
+ /**
+ * Whether {@see $conditions} is a conjunction or a disjunction.
+ *
+ * In other words, if this is `true` this query matches when _all_
+ * {@see $conditions} are met, and if it's `false` this query matches when _any_
+ * condition in {@see $conditions} is met.
+ *
+ * If this is `false`, {@see $modifier} and {@see $type} will both be `null`.
+ */
+ private readonly bool $conjunction;
+
+ /**
+ * Media conditions, including parentheses.
+ *
+ * This is anything that can appear in the [`<media-in-parens>`] production.
+ *
+ * [`<media-in-parens>`]: https://drafts.csswg.org/mediaqueries-4/#typedef-media-in-parens
+ *
+ * @var list<string>
+ */
+ private readonly array $conditions;
+
+ /**
+ * Parses a media query from $contents.
+ *
+ * If passed, $url is the name of the file from which $contents comes.
+ *
+ * @return list<CssMediaQuery>
+ *
+ * @throws SassFormatException if parsing fails
+ */
+ public static function parseList(string $contents, ?LoggerInterface $logger = null, ?UriInterface $url = null, ?InterpolationMap $interpolationMap = null): array
+ {
+ return (new MediaQueryParser($contents, $logger, $url, $interpolationMap))->parse();
+ }
+
+ /**
+ * @param list<string> $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<string> $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<string> $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<string>
+ */
+ 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.
+ */
+ public function merge(CssMediaQuery $other): MediaQueryMergeResult
+ {
+ if (!$this->conjunction || !$other->conjunction) {
+ return MediaQuerySingletonMergeResult::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 MediaQuerySingletonMergeResult::empty;
+ }
+
+ return MediaQuerySingletonMergeResult::unrepresentable;
+ }
+
+ if ($this->matchesAllTypes() || $other->matchesAllTypes()) {
+ return MediaQuerySingletonMergeResult::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 MediaQuerySingletonMergeResult::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 MediaQuerySingletonMergeResult::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 MediaQuerySingletonMergeResult::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
+ );
+ }
+
+ public function equals(object $other): bool
+ {
+ return $other instanceof CssMediaQuery && $other->modifier === $this->modifier && $other->type === $this->type && $other->conditions === $this->conditions;
+ }
+}
diff --git a/vendor/scssphp/scssphp/src/Ast/Css/CssMediaRule.php b/vendor/scssphp/scssphp/src/Ast/Css/CssMediaRule.php
new file mode 100644
index 000000000..f1287c3c4
--- /dev/null
+++ b/vendor/scssphp/scssphp/src/Ast/Css/CssMediaRule.php
@@ -0,0 +1,30 @@
+<?php
+
+/**
+ * SCSSPHP
+ *
+ * @copyright 2012-2020 Leaf Corcoran
+ *
+ * @license http://opensource.org/licenses/MIT MIT
+ *
+ * @link http://scssphp.github.io/scssphp
+ */
+
+namespace ScssPhp\ScssPhp\Ast\Css;
+
+/**
+ * A plain CSS `@media` rule.
+ *
+ * @internal
+ */
+interface CssMediaRule extends CssParentNode
+{
+ /**
+ * The queries for this rule.
+ *
+ * This is never empty.
+ *
+ * @return list<CssMediaQuery>
+ */
+ public function getQueries(): array;
+}
diff --git a/vendor/scssphp/scssphp/src/Ast/Css/CssNode.php b/vendor/scssphp/scssphp/src/Ast/Css/CssNode.php
new file mode 100644
index 000000000..785411291
--- /dev/null
+++ b/vendor/scssphp/scssphp/src/Ast/Css/CssNode.php
@@ -0,0 +1,68 @@
+<?php
+
+/**
+ * SCSSPHP
+ *
+ * @copyright 2012-2020 Leaf Corcoran
+ *
+ * @license http://opensource.org/licenses/MIT MIT
+ *
+ * @link http://scssphp.github.io/scssphp
+ */
+
+namespace ScssPhp\ScssPhp\Ast\Css;
+
+use ScssPhp\ScssPhp\Ast\AstNode;
+use ScssPhp\ScssPhp\Visitor\CssVisitor;
+
+/**
+ * A statement in a plain CSS syntax tree.
+ *
+ * @internal
+ */
+interface CssNode extends AstNode
+{
+ /**
+ * The node that contains this, or `null` for the root {@see CssStylesheet} node.
+ */
+ public function getParent(): ?CssParentNode;
+
+ /**
+ * Whether this was generated from the last node in a nested Sass tree that
+ * got flattened during evaluation.
+ */
+ public function isGroupEnd(): bool;
+
+ /**
+ * Calls the appropriate visit method on $visitor.
+ *
+ * @template T
+ *
+ * @param CssVisitor<T> $visitor
+ *
+ * @return T
+ */
+ public function accept(CssVisitor $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/vendor/scssphp/scssphp/src/Ast/Css/CssParentNode.php b/vendor/scssphp/scssphp/src/Ast/Css/CssParentNode.php
new file mode 100644
index 000000000..3e4637a72
--- /dev/null
+++ b/vendor/scssphp/scssphp/src/Ast/Css/CssParentNode.php
@@ -0,0 +1,37 @@
+<?php
+
+/**
+ * SCSSPHP
+ *
+ * @copyright 2012-2020 Leaf Corcoran
+ *
+ * @license http://opensource.org/licenses/MIT MIT
+ *
+ * @link http://scssphp.github.io/scssphp
+ */
+
+namespace ScssPhp\ScssPhp\Ast\Css;
+
+/**
+ * A {@see CssNode} that can have child statements.
+ *
+ * @internal
+ */
+interface CssParentNode extends CssNode
+{
+ /**
+ * The child statements of this node.
+ *
+ * @return list<CssNode>
+ */
+ 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/vendor/scssphp/scssphp/src/Ast/Css/CssStyleRule.php b/vendor/scssphp/scssphp/src/Ast/Css/CssStyleRule.php
new file mode 100644
index 000000000..4df9120fa
--- /dev/null
+++ b/vendor/scssphp/scssphp/src/Ast/Css/CssStyleRule.php
@@ -0,0 +1,44 @@
+<?php
+
+/**
+ * SCSSPHP
+ *
+ * @copyright 2012-2020 Leaf Corcoran
+ *
+ * @license http://opensource.org/licenses/MIT MIT
+ *
+ * @link http://scssphp.github.io/scssphp
+ */
+
+namespace ScssPhp\ScssPhp\Ast\Css;
+
+use ScssPhp\ScssPhp\Ast\Selector\SelectorList;
+
+/**
+ * A plain CSS style rule.
+* *
+* * This applies style declarations to elements that match a given selector.
+* * Note that this isn't *strictly* plain CSS, since {@see getSelector} may still
+* * contain placeholder selectors.
+ *
+ * @internal
+ */
+interface CssStyleRule extends CssParentNode
+{
+ /**
+ * The selector for this rule.
+ */
+ public function getSelector(): SelectorList;
+
+ /**
+ * The selector for this rule, before any extensions were applied.
+ */
+ public function getOriginalSelector(): SelectorList;
+
+ /**
+ * Whether this style rule was originally defined in a plain CSS stylesheet.
+ *
+ * @internal
+ */
+ public function isFromPlainCss(): bool;
+}
diff --git a/vendor/scssphp/scssphp/src/Exception/CompilerException.php b/vendor/scssphp/scssphp/src/Ast/Css/CssStylesheet.php
index 0b00cf525..6e69e3938 100644
--- a/vendor/scssphp/scssphp/src/Exception/CompilerException.php
+++ b/vendor/scssphp/scssphp/src/Ast/Css/CssStylesheet.php
@@ -10,15 +10,15 @@
* @link http://scssphp.github.io/scssphp
*/
-namespace ScssPhp\ScssPhp\Exception;
+namespace ScssPhp\ScssPhp\Ast\Css;
/**
- * Compiler exception
+ * A plain CSS stylesheet.
*
- * @author Oleksandr Savchenko <traveltino@gmail.com>
+ * This is the root plain CSS node. It contains top-level statements.
*
* @internal
*/
-class CompilerException extends \Exception implements SassException
+interface CssStylesheet extends CssParentNode
{
}
diff --git a/vendor/scssphp/scssphp/src/Ast/Css/CssSupportsRule.php b/vendor/scssphp/scssphp/src/Ast/Css/CssSupportsRule.php
new file mode 100644
index 000000000..84e595277
--- /dev/null
+++ b/vendor/scssphp/scssphp/src/Ast/Css/CssSupportsRule.php
@@ -0,0 +1,28 @@
+<?php
+
+/**
+ * SCSSPHP
+ *
+ * @copyright 2012-2020 Leaf Corcoran
+ *
+ * @license http://opensource.org/licenses/MIT MIT
+ *
+ * @link http://scssphp.github.io/scssphp
+ */
+
+namespace ScssPhp\ScssPhp\Ast\Css;
+
+/**
+ * A plain CSS `@supports` rule.
+ *
+ * @internal
+ */
+interface CssSupportsRule extends CssParentNode
+{
+ /**
+ * The supports condition.
+ *
+ * @return CssValue<string>
+ */
+ public function getCondition(): CssValue;
+}
diff --git a/vendor/scssphp/scssphp/src/Ast/Css/CssValue.php b/vendor/scssphp/scssphp/src/Ast/Css/CssValue.php
new file mode 100644
index 000000000..a9e578dd5
--- /dev/null
+++ b/vendor/scssphp/scssphp/src/Ast/Css/CssValue.php
@@ -0,0 +1,79 @@
+<?php
+
+/**
+ * SCSSPHP
+ *
+ * @copyright 2012-2020 Leaf Corcoran
+ *
+ * @license http://opensource.org/licenses/MIT MIT
+ *
+ * @link http://scssphp.github.io/scssphp
+ */
+
+namespace ScssPhp\ScssPhp\Ast\Css;
+
+use ScssPhp\ScssPhp\Ast\AstNode;
+use ScssPhp\ScssPhp\Ast\Selector\Combinator;
+use ScssPhp\ScssPhp\Util\Equatable;
+use ScssPhp\ScssPhp\Util\EquatableUtil;
+use SourceSpan\FileSpan;
+
+/**
+ * A value in a plain CSS tree.
+ *
+ * This is used to associate a span with a value that doesn't otherwise track
+ * its span. It has value equality semantics.
+ *
+ * @template-covariant T of string|\Stringable|array<string|\Stringable>|Combinator|null
+ *
+ * @internal
+ */
+final class CssValue implements AstNode, Equatable
+{
+ /**
+ * @var T
+ */
+ private readonly mixed $value;
+
+ private readonly FileSpan $span;
+
+ /**
+ * @param T $value
+ */
+ public function __construct(mixed $value, FileSpan $span)
+ {
+ $this->value = $value;
+ $this->span = $span;
+ }
+
+ /**
+ * @return T
+ */
+ public function getValue(): mixed
+ {
+ return $this->value;
+ }
+
+ public function getSpan(): FileSpan
+ {
+ return $this->span;
+ }
+
+ public function equals(object $other): bool
+ {
+ return $other instanceof CssValue && EquatableUtil::equals($this->value, $other->value);
+ }
+
+ public function __toString(): string
+ {
+ if ($this->value instanceof Combinator) {
+ return $this->value->getText();
+ }
+
+ if (\is_array($this->value)) {
+ return implode($this->value);
+ }
+
+ return (string) $this->value;
+ }
+}
diff --git a/vendor/scssphp/scssphp/src/Ast/Css/IsInvisibleVisitor.php b/vendor/scssphp/scssphp/src/Ast/Css/IsInvisibleVisitor.php
new file mode 100644
index 000000000..4c64bae6f
--- /dev/null
+++ b/vendor/scssphp/scssphp/src/Ast/Css/IsInvisibleVisitor.php
@@ -0,0 +1,57 @@
+<?php
+
+/**
+ * SCSSPHP
+ *
+ * @copyright 2012-2020 Leaf Corcoran
+ *
+ * @license http://opensource.org/licenses/MIT MIT
+ *
+ * @link http://scssphp.github.io/scssphp
+ */
+
+namespace ScssPhp\ScssPhp\Ast\Css;
+
+use ScssPhp\ScssPhp\Visitor\EveryCssVisitor;
+
+/**
+ * The visitor used to implement {@see CssNode::isInvisible}
+ *
+ * @internal
+ */
+final class IsInvisibleVisitor extends EveryCssVisitor
+{
+ /**
+ * Whether to consider selectors with bogus combinators invisible.
+ */
+ private readonly bool $includeBogus;
+
+ /**
+ * Whether to consider comments invisible.
+ */
+ private readonly bool $includeComments;
+
+ public function __construct(bool $includeBogus, bool $includeComments)
+ {
+ $this->includeBogus = $includeBogus;
+ $this->includeComments = $includeComments;
+ }
+
+ public function visitCssAtRule(CssAtRule $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(CssComment $node): bool
+ {
+ return $this->includeComments && !$node->isPreserved();
+ }
+
+ public function visitCssStyleRule(CssStyleRule $node): bool
+ {
+ return ($this->includeBogus ? $node->getSelector()->isInvisible() : $node->getSelector()->isInvisibleOtherThanBogusCombinators()) || parent::visitCssStyleRule($node);
+ }
+}
diff --git a/vendor/scssphp/scssphp/src/Ast/Css/MediaQueryMergeResult.php b/vendor/scssphp/scssphp/src/Ast/Css/MediaQueryMergeResult.php
new file mode 100644
index 000000000..c364e8761
--- /dev/null
+++ b/vendor/scssphp/scssphp/src/Ast/Css/MediaQueryMergeResult.php
@@ -0,0 +1,23 @@
+<?php
+
+/**
+ * SCSSPHP
+ *
+ * @copyright 2012-2020 Leaf Corcoran
+ *
+ * @license http://opensource.org/licenses/MIT MIT
+ *
+ * @link http://scssphp.github.io/scssphp
+ */
+
+namespace ScssPhp\ScssPhp\Ast\Css;
+
+use JiriPudil\SealedClasses\Sealed;
+
+/**
+ * @internal
+ */
+#[Sealed(permits: [CssMediaQuery::class, MediaQuerySingletonMergeResult::class])]
+interface MediaQueryMergeResult
+{
+}
diff --git a/vendor/scssphp/scssphp/src/Ast/Css/MediaQuerySingletonMergeResult.php b/vendor/scssphp/scssphp/src/Ast/Css/MediaQuerySingletonMergeResult.php
new file mode 100644
index 000000000..56ca7abf7
--- /dev/null
+++ b/vendor/scssphp/scssphp/src/Ast/Css/MediaQuerySingletonMergeResult.php
@@ -0,0 +1,19 @@
+<?php
+
+/**
+ * SCSSPHP
+ *
+ * @copyright 2012-2020 Leaf Corcoran
+ *
+ * @license http://opensource.org/licenses/MIT MIT
+ *
+ * @link http://scssphp.github.io/scssphp
+ */
+
+namespace ScssPhp\ScssPhp\Ast\Css;
+
+enum MediaQuerySingletonMergeResult implements MediaQueryMergeResult
+{
+ case empty;
+ case unrepresentable;
+}
diff --git a/vendor/scssphp/scssphp/src/Ast/Css/ModifiableCssAtRule.php b/vendor/scssphp/scssphp/src/Ast/Css/ModifiableCssAtRule.php
new file mode 100644
index 000000000..ba6889974
--- /dev/null
+++ b/vendor/scssphp/scssphp/src/Ast/Css/ModifiableCssAtRule.php
@@ -0,0 +1,97 @@
+<?php
+
+/**
+ * SCSSPHP
+ *
+ * @copyright 2012-2020 Leaf Corcoran
+ *
+ * @license http://opensource.org/licenses/MIT MIT
+ *
+ * @link http://scssphp.github.io/scssphp
+ */
+
+namespace ScssPhp\ScssPhp\Ast\Css;
+
+use ScssPhp\ScssPhp\Util\EquatableUtil;
+use ScssPhp\ScssPhp\Visitor\ModifiableCssVisitor;
+use SourceSpan\FileSpan;
+
+/**
+ * A modifiable version of {@see CssAtRule} for use in the evaluation step.
+ *
+ * @internal
+ */
+final class ModifiableCssAtRule extends ModifiableCssParentNode implements CssAtRule
+{
+ /**
+ * @var CssValue<string>
+ */
+ private readonly CssValue $name;
+
+ /**
+ * @var CssValue<string>|null
+ */
+ private readonly ?CssValue $value;
+
+ private readonly bool $childless;
+
+ private readonly FileSpan $span;
+
+ /**
+ * @param CssValue<string> $name
+ * @param CssValue<string>|null $value
+ */
+ 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(ModifiableCssVisitor $visitor)
+ {
+ return $visitor->visitCssAtRule($this);
+ }
+
+ public function equalsIgnoringChildren(ModifiableCssNode $other): bool
+ {
+ return $other instanceof ModifiableCssAtRule && EquatableUtil::equals($this->name, $other->name) && EquatableUtil::equals($this->value, $other->value) && $this->childless === $other->childless;
+ }
+
+ public function copyWithoutChildren(): ModifiableCssAtRule
+ {
+ 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/vendor/scssphp/scssphp/src/Ast/Css/ModifiableCssComment.php b/vendor/scssphp/scssphp/src/Ast/Css/ModifiableCssComment.php
new file mode 100644
index 000000000..2eda6bd3d
--- /dev/null
+++ b/vendor/scssphp/scssphp/src/Ast/Css/ModifiableCssComment.php
@@ -0,0 +1,54 @@
+<?php
+
+/**
+ * SCSSPHP
+ *
+ * @copyright 2012-2020 Leaf Corcoran
+ *
+ * @license http://opensource.org/licenses/MIT MIT
+ *
+ * @link http://scssphp.github.io/scssphp
+ */
+
+namespace ScssPhp\ScssPhp\Ast\Css;
+
+use ScssPhp\ScssPhp\Visitor\ModifiableCssVisitor;
+use SourceSpan\FileSpan;
+
+/**
+ * A modifiable version of {@see CssComment} for use in the evaluation step.
+ *
+ * @internal
+ */
+final class ModifiableCssComment extends ModifiableCssNode implements CssComment
+{
+ private readonly string $text;
+
+ private readonly FileSpan $span;
+
+ public function __construct(string $text, FileSpan $span)
+ {
+ $this->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(ModifiableCssVisitor $visitor)
+ {
+ return $visitor->visitCssComment($this);
+ }
+}
diff --git a/vendor/scssphp/scssphp/src/Ast/Css/ModifiableCssDeclaration.php b/vendor/scssphp/scssphp/src/Ast/Css/ModifiableCssDeclaration.php
new file mode 100644
index 000000000..d8fd83451
--- /dev/null
+++ b/vendor/scssphp/scssphp/src/Ast/Css/ModifiableCssDeclaration.php
@@ -0,0 +1,121 @@
+<?php
+
+/**
+ * SCSSPHP
+ *
+ * @copyright 2012-2020 Leaf Corcoran
+ *
+ * @license http://opensource.org/licenses/MIT MIT
+ *
+ * @link http://scssphp.github.io/scssphp
+ */
+
+namespace ScssPhp\ScssPhp\Ast\Css;
+
+use ScssPhp\ScssPhp\StackTrace\Trace;
+use ScssPhp\ScssPhp\Value\SassString;
+use ScssPhp\ScssPhp\Value\Value;
+use ScssPhp\ScssPhp\Visitor\ModifiableCssVisitor;
+use SourceSpan\FileSpan;
+
+/**
+ * A modifiable version of {@see CssDeclaration} for use in the evaluation step.
+ *
+ * @internal
+ */
+final class ModifiableCssDeclaration extends ModifiableCssNode implements CssDeclaration
+{
+ /**
+ * @var CssValue<string>
+ */
+ private readonly CssValue $name;
+
+ /**
+ * @var CssValue<Value>
+ */
+ private readonly CssValue $value;
+
+ /**
+ * @var list<CssStyleRule>
+ */
+ private readonly array $interleavedRules;
+
+ private readonly ?Trace $trace;
+
+ private readonly bool $parsedAsCustomProperty;
+
+ private readonly FileSpan $valueSpanForMap;
+
+ private readonly FileSpan $span;
+
+ /**
+ * @param CssValue<string> $name
+ * @param CssValue<Value> $value
+ * @param list<CssStyleRule> $interleavedRules
+ */
+ public function __construct(CssValue $name, CssValue $value, FileSpan $span, bool $parsedAsCustomProperty, array $interleavedRules = [], ?Trace $trace = null, ?FileSpan $valueSpanForMap = null)
+ {
+ $this->name = $name;
+ $this->value = $value;
+ $this->parsedAsCustomProperty = $parsedAsCustomProperty;
+ $this->interleavedRules = $interleavedRules;
+ $this->trace = $trace;
+ $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_debug_type($value->getValue())));
+ }
+ }
+ }
+
+ public function getName(): CssValue
+ {
+ return $this->name;
+ }
+
+ public function getValue(): CssValue
+ {
+ return $this->value;
+ }
+
+ public function getInterleavedRules(): array
+ {
+ return $this->interleavedRules;
+ }
+
+ public function getTrace(): ?Trace
+ {
+ return $this->trace;
+ }
+
+ 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 str_starts_with($this->name->getValue(), '--');
+ }
+
+ public function accept(ModifiableCssVisitor $visitor)
+ {
+ return $visitor->visitCssDeclaration($this);
+ }
+}
diff --git a/vendor/scssphp/scssphp/src/Ast/Css/ModifiableCssImport.php b/vendor/scssphp/scssphp/src/Ast/Css/ModifiableCssImport.php
new file mode 100644
index 000000000..0598d118a
--- /dev/null
+++ b/vendor/scssphp/scssphp/src/Ast/Css/ModifiableCssImport.php
@@ -0,0 +1,71 @@
+<?php
+
+/**
+ * SCSSPHP
+ *
+ * @copyright 2012-2020 Leaf Corcoran
+ *
+ * @license http://opensource.org/licenses/MIT MIT
+ *
+ * @link http://scssphp.github.io/scssphp
+ */
+
+namespace ScssPhp\ScssPhp\Ast\Css;
+
+use ScssPhp\ScssPhp\Visitor\ModifiableCssVisitor;
+use SourceSpan\FileSpan;
+
+/**
+ * A modifiable version of {@see CssImport} for use in the evaluation step.
+ *
+ * @internal
+ */
+final class ModifiableCssImport extends ModifiableCssNode implements CssImport
+{
+ /**
+ * The URL being imported.
+ *
+ * This includes quotes.
+ *
+ * @var CssValue<string>
+ */
+ private readonly CssValue $url;
+
+ /**
+ * @var CssValue<string>|null
+ */
+ private readonly ?CssValue $modifiers;
+
+ private readonly FileSpan $span;
+
+ /**
+ * @param CssValue<string> $url
+ * @param CssValue<string>|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(ModifiableCssVisitor $visitor)
+ {
+ return $visitor->visitCssImport($this);
+ }
+}
diff --git a/vendor/scssphp/scssphp/src/Ast/Css/ModifiableCssKeyframeBlock.php b/vendor/scssphp/scssphp/src/Ast/Css/ModifiableCssKeyframeBlock.php
new file mode 100644
index 000000000..160987305
--- /dev/null
+++ b/vendor/scssphp/scssphp/src/Ast/Css/ModifiableCssKeyframeBlock.php
@@ -0,0 +1,67 @@
+<?php
+
+/**
+ * SCSSPHP
+ *
+ * @copyright 2012-2020 Leaf Corcoran
+ *
+ * @license http://opensource.org/licenses/MIT MIT
+ *
+ * @link http://scssphp.github.io/scssphp
+ */
+
+namespace ScssPhp\ScssPhp\Ast\Css;
+
+use ScssPhp\ScssPhp\Util\EquatableUtil;
+use ScssPhp\ScssPhp\Visitor\ModifiableCssVisitor;
+use SourceSpan\FileSpan;
+
+/**
+ * A modifiable version of {@see CssKeyframeBlock} for use in the evaluation step.
+ *
+ * @internal
+ */
+final class ModifiableCssKeyframeBlock extends ModifiableCssParentNode implements CssKeyframeBlock
+{
+ /**
+ * @var CssValue<list<string>>
+ */
+ private readonly CssValue $selector;
+
+ private readonly FileSpan $span;
+
+ /**
+ * @param CssValue<list<string>> $selector
+ */
+ 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(ModifiableCssVisitor $visitor)
+ {
+ return $visitor->visitCssKeyframeBlock($this);
+ }
+
+ public function equalsIgnoringChildren(ModifiableCssNode $other): bool
+ {
+ return $other instanceof ModifiableCssKeyframeBlock && EquatableUtil::listEquals($this->selector->getValue(), $other->selector->getValue());
+ }
+
+ public function copyWithoutChildren(): ModifiableCssKeyframeBlock
+ {
+ return new ModifiableCssKeyframeBlock($this->selector, $this->span);
+ }
+}
diff --git a/vendor/scssphp/scssphp/src/Ast/Css/ModifiableCssMediaRule.php b/vendor/scssphp/scssphp/src/Ast/Css/ModifiableCssMediaRule.php
new file mode 100644
index 000000000..3f9f5bb4b
--- /dev/null
+++ b/vendor/scssphp/scssphp/src/Ast/Css/ModifiableCssMediaRule.php
@@ -0,0 +1,67 @@
+<?php
+
+/**
+ * SCSSPHP
+ *
+ * @copyright 2012-2020 Leaf Corcoran
+ *
+ * @license http://opensource.org/licenses/MIT MIT
+ *
+ * @link http://scssphp.github.io/scssphp
+ */
+
+namespace ScssPhp\ScssPhp\Ast\Css;
+
+use ScssPhp\ScssPhp\Util\EquatableUtil;
+use ScssPhp\ScssPhp\Visitor\ModifiableCssVisitor;
+use SourceSpan\FileSpan;
+
+/**
+ * A modifiable version of {@see CssMediaRule} for use in the evaluation step.
+ *
+ * @internal
+ */
+final class ModifiableCssMediaRule extends ModifiableCssParentNode implements CssMediaRule
+{
+ /**
+ * @var list<CssMediaQuery>
+ */
+ private readonly array $queries;
+
+ private readonly FileSpan $span;
+
+ /**
+ * @param list<CssMediaQuery> $queries
+ */
+ 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(ModifiableCssVisitor $visitor)
+ {
+ return $visitor->visitCssMediaRule($this);
+ }
+
+ public function equalsIgnoringChildren(ModifiableCssNode $other): bool
+ {
+ return $other instanceof ModifiableCssMediaRule && EquatableUtil::listEquals($this->queries, $other->queries);
+ }
+
+ public function copyWithoutChildren(): ModifiableCssMediaRule
+ {
+ return new ModifiableCssMediaRule($this->queries, $this->span);
+ }
+}
diff --git a/vendor/scssphp/scssphp/src/Ast/Css/ModifiableCssNode.php b/vendor/scssphp/scssphp/src/Ast/Css/ModifiableCssNode.php
new file mode 100644
index 000000000..5d944682b
--- /dev/null
+++ b/vendor/scssphp/scssphp/src/Ast/Css/ModifiableCssNode.php
@@ -0,0 +1,152 @@
+<?php
+
+/**
+ * SCSSPHP
+ *
+ * @copyright 2012-2020 Leaf Corcoran
+ *
+ * @license http://opensource.org/licenses/MIT MIT
+ *
+ * @link http://scssphp.github.io/scssphp
+ */
+
+namespace ScssPhp\ScssPhp\Ast\Css;
+
+use ScssPhp\ScssPhp\Serializer\Serializer;
+use ScssPhp\ScssPhp\Visitor\ModifiableCssVisitor;
+
+/**
+ * A modifiable version of {@see CssNode}.
+ *
+ * Almost all CSS nodes are the modifiable classes under the covers. However,
+ * modification should only be done within the evaluation step, so the
+ * unmodifiable types are used elsewhere to enforce that constraint.
+ *
+ * @internal
+ */
+abstract class ModifiableCssNode implements CssNode
+{
+ private ?ModifiableCssParentNode $parent = null;
+
+ /**
+ * The index of `$this` in parent's children.
+ *
+ * This makes {@see remove} more efficient.
+ */
+ private ?int $indexInParent = null;
+
+ private bool $groupEnd = false;
+
+ public function getParent(): ?ModifiableCssParentNode
+ {
+ return $this->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<T> $visitor
+ *
+ * @return T
+ */
+ abstract public function accept(ModifiableCssVisitor $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;
+ }
+
+ /**
+ * @@internal
+ */
+ protected function resetParentReferences(): void
+ {
+ $this->parent = null;
+ $this->indexInParent = null;
+ }
+
+ public function __toString(): string
+ {
+ return Serializer::serialize($this, true)->css;
+ }
+}
diff --git a/vendor/scssphp/scssphp/src/Ast/Css/ModifiableCssParentNode.php b/vendor/scssphp/scssphp/src/Ast/Css/ModifiableCssParentNode.php
new file mode 100644
index 000000000..29b46bae5
--- /dev/null
+++ b/vendor/scssphp/scssphp/src/Ast/Css/ModifiableCssParentNode.php
@@ -0,0 +1,85 @@
+<?php
+
+/**
+ * SCSSPHP
+ *
+ * @copyright 2012-2020 Leaf Corcoran
+ *
+ * @license http://opensource.org/licenses/MIT MIT
+ *
+ * @link http://scssphp.github.io/scssphp
+ */
+
+namespace ScssPhp\ScssPhp\Ast\Css;
+
+/**
+ * A modifiable version of {@see CssParentNode} for use in the evaluation step.
+ *
+ * @internal
+ */
+abstract class ModifiableCssParentNode extends ModifiableCssNode implements CssParentNode
+{
+ /**
+ * @var list<ModifiableCssNode>
+ */
+ private array $children;
+
+ /**
+ * @param list<ModifiableCssNode> $children
+ */
+ public function __construct(array $children = [])
+ {
+ $this->children = $children;
+ }
+
+ /**
+ * @return list<ModifiableCssNode>
+ */
+ public function getChildren(): array
+ {
+ return $this->children;
+ }
+
+ public function isChildless(): bool
+ {
+ return false;
+ }
+
+ /**
+ * Returns whether $this is equal to $other, ignoring their child nodes.
+ */
+ abstract public function equalsIgnoringChildren(ModifiableCssNode $other): bool;
+
+ /**
+ * 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);
+ }
+
+ /**
+ * Destructively removes all elements from {@see children}.
+ */
+ public function clearChildren(): void
+ {
+ foreach ($this->children as $child) {
+ $child->resetParentReferences();
+ }
+ $this->children = [];
+ }
+}
diff --git a/vendor/scssphp/scssphp/src/Ast/Css/ModifiableCssStyleRule.php b/vendor/scssphp/scssphp/src/Ast/Css/ModifiableCssStyleRule.php
new file mode 100644
index 000000000..089037447
--- /dev/null
+++ b/vendor/scssphp/scssphp/src/Ast/Css/ModifiableCssStyleRule.php
@@ -0,0 +1,88 @@
+<?php
+
+/**
+ * SCSSPHP
+ *
+ * @copyright 2012-2020 Leaf Corcoran
+ *
+ * @license http://opensource.org/licenses/MIT MIT
+ *
+ * @link http://scssphp.github.io/scssphp
+ */
+
+namespace ScssPhp\ScssPhp\Ast\Css;
+
+use ScssPhp\ScssPhp\Ast\Selector\SelectorList;
+use ScssPhp\ScssPhp\Util\Box;
+use ScssPhp\ScssPhp\Util\EquatableUtil;
+use ScssPhp\ScssPhp\Visitor\ModifiableCssVisitor;
+use SourceSpan\FileSpan;
+
+/**
+ * A modifiable version of {@see CssStyleRule} for use in the evaluation step.
+ *
+ * @internal
+ */
+final class ModifiableCssStyleRule extends ModifiableCssParentNode implements CssStyleRule
+{
+ /**
+ * A reference to the modifiable selector list provided by the extension
+ * store, which may update it over time as new extensions are applied.
+ *
+ * @var Box<SelectorList>
+ */
+ private readonly Box $selector;
+
+ private readonly SelectorList $originalSelector;
+
+ private readonly FileSpan $span;
+
+ private readonly bool $fromPlainCss;
+
+ /**
+ * @param Box<SelectorList> $selector
+ */
+ public function __construct(Box $selector, FileSpan $span, ?SelectorList $originalSelector = null, bool $fromPlainCss = false)
+ {
+ parent::__construct();
+ $this->selector = $selector;
+ $this->originalSelector = $originalSelector ?? $selector->getValue();
+ $this->span = $span;
+ $this->fromPlainCss = $fromPlainCss;
+ }
+
+ public function getSelector(): SelectorList
+ {
+ return $this->selector->getValue();
+ }
+
+ public function getOriginalSelector(): SelectorList
+ {
+ return $this->originalSelector;
+ }
+
+ public function isFromPlainCss(): bool
+ {
+ return $this->fromPlainCss;
+ }
+
+ public function getSpan(): FileSpan
+ {
+ return $this->span;
+ }
+
+ public function accept(ModifiableCssVisitor $visitor)
+ {
+ return $visitor->visitCssStyleRule($this);
+ }
+
+ public function equalsIgnoringChildren(ModifiableCssNode $other): bool
+ {
+ return $other instanceof ModifiableCssStyleRule && EquatableUtil::equals($this->selector, $other->selector);
+ }
+
+ public function copyWithoutChildren(): ModifiableCssStyleRule
+ {
+ return new ModifiableCssStyleRule($this->selector, $this->span, $this->originalSelector);
+ }
+}
diff --git a/vendor/scssphp/scssphp/src/Ast/Css/ModifiableCssStylesheet.php b/vendor/scssphp/scssphp/src/Ast/Css/ModifiableCssStylesheet.php
new file mode 100644
index 000000000..ec98f4f80
--- /dev/null
+++ b/vendor/scssphp/scssphp/src/Ast/Css/ModifiableCssStylesheet.php
@@ -0,0 +1,55 @@
+<?php
+
+/**
+ * SCSSPHP
+ *
+ * @copyright 2012-2020 Leaf Corcoran
+ *
+ * @license http://opensource.org/licenses/MIT MIT
+ *
+ * @link http://scssphp.github.io/scssphp
+ */
+
+namespace ScssPhp\ScssPhp\Ast\Css;
+
+use ScssPhp\ScssPhp\Visitor\ModifiableCssVisitor;
+use SourceSpan\FileSpan;
+
+/**
+ * A modifiable version of {@see CssStylesheet} for use in the evaluation step.
+ *
+ * @internal
+ */
+final class ModifiableCssStylesheet extends ModifiableCssParentNode implements CssStylesheet
+{
+ private readonly FileSpan $span;
+
+ /**
+ * @param list<ModifiableCssNode> $children
+ */
+ public function __construct(FileSpan $span, array $children = [])
+ {
+ parent::__construct($children);
+ $this->span = $span;
+ }
+
+ public function getSpan(): FileSpan
+ {
+ return $this->span;
+ }
+
+ public function accept(ModifiableCssVisitor $visitor)
+ {
+ return $visitor->visitCssStylesheet($this);
+ }
+
+ public function equalsIgnoringChildren(ModifiableCssNode $other): bool
+ {
+ return $other instanceof ModifiableCssStylesheet;
+ }
+
+ public function copyWithoutChildren(): ModifiableCssStylesheet
+ {
+ return new ModifiableCssStylesheet($this->span);
+ }
+}
diff --git a/vendor/scssphp/scssphp/src/Ast/Css/ModifiableCssSupportsRule.php b/vendor/scssphp/scssphp/src/Ast/Css/ModifiableCssSupportsRule.php
new file mode 100644
index 000000000..49d30d109
--- /dev/null
+++ b/vendor/scssphp/scssphp/src/Ast/Css/ModifiableCssSupportsRule.php
@@ -0,0 +1,67 @@
+<?php
+
+/**
+ * SCSSPHP
+ *
+ * @copyright 2012-2020 Leaf Corcoran
+ *
+ * @license http://opensource.org/licenses/MIT MIT
+ *
+ * @link http://scssphp.github.io/scssphp
+ */
+
+namespace ScssPhp\ScssPhp\Ast\Css;
+
+use ScssPhp\ScssPhp\Util\EquatableUtil;
+use ScssPhp\ScssPhp\Visitor\ModifiableCssVisitor;
+use SourceSpan\FileSpan;
+
+/**
+ * A modifiable version of {@see CssSupportsRule} for use in the evaluation step.
+ *
+ * @internal
+ */
+final class ModifiableCssSupportsRule extends ModifiableCssParentNode implements CssSupportsRule
+{
+ /**
+ * @var CssValue<string>
+ */
+ private readonly CssValue $condition;
+
+ private readonly FileSpan $span;
+
+ /**
+ * @param CssValue<string> $condition
+ */
+ 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(ModifiableCssVisitor $visitor)
+ {
+ return $visitor->visitCssSupportsRule($this);
+ }
+
+ public function equalsIgnoringChildren(ModifiableCssNode $other): bool
+ {
+ return $other instanceof ModifiableCssSupportsRule && EquatableUtil::equals($this->condition, $other->condition);
+ }
+
+ public function copyWithoutChildren(): ModifiableCssSupportsRule
+ {
+ return new ModifiableCssSupportsRule($this->condition, $this->span);
+ }
+}
diff --git a/vendor/scssphp/scssphp/src/Ast/FakeAstNode.php b/vendor/scssphp/scssphp/src/Ast/FakeAstNode.php
new file mode 100644
index 000000000..c3ad1d2c3
--- /dev/null
+++ b/vendor/scssphp/scssphp/src/Ast/FakeAstNode.php
@@ -0,0 +1,46 @@
+<?php
+
+/**
+ * SCSSPHP
+ *
+ * @copyright 2012-2020 Leaf Corcoran
+ *
+ * @license http://opensource.org/licenses/MIT MIT
+ *
+ * @link http://scssphp.github.io/scssphp
+ */
+
+namespace ScssPhp\ScssPhp\Ast;
+
+use SourceSpan\FileSpan;
+
+/**
+ * An {@see AstNode} that just exposes a single span generated by a callback.
+ *
+ * @internal
+ */
+final class FakeAstNode implements AstNode
+{
+ /**
+ * @var \Closure(): FileSpan
+ */
+ private readonly \Closure $callback;
+
+ /**
+ * @param callable(): FileSpan $callback
+ */
+ public function __construct(callable $callback)
+ {
+ $this->callback = $callback(...);
+ }
+
+ public function getSpan(): FileSpan
+ {
+ return ($this->callback)();
+ }
+
+ public function __toString(): string
+ {
+ return '';
+ }
+}
diff --git a/vendor/scssphp/scssphp/src/Ast/Sass/Argument.php b/vendor/scssphp/scssphp/src/Ast/Sass/Argument.php
new file mode 100644
index 000000000..52690ed98
--- /dev/null
+++ b/vendor/scssphp/scssphp/src/Ast/Sass/Argument.php
@@ -0,0 +1,87 @@
+<?php
+
+/**
+ * SCSSPHP
+ *
+ * @copyright 2012-2020 Leaf Corcoran
+ *
+ * @license http://opensource.org/licenses/MIT MIT
+ *
+ * @link http://scssphp.github.io/scssphp
+ */
+
+namespace ScssPhp\ScssPhp\Ast\Sass;
+
+use ScssPhp\ScssPhp\Util;
+use ScssPhp\ScssPhp\Util\SpanUtil;
+use SourceSpan\FileSpan;
+
+/**
+ * An argument declared as part of an {@see ArgumentDeclaration}.
+ *
+ * @internal
+ */
+final class Argument implements SassNode, SassDeclaration
+{
+ private readonly string $name;
+
+ private readonly ?Expression $defaultValue;
+
+ private readonly FileSpan $span;
+
+ public function __construct(string $name, FileSpan $span, ?Expression $defaultValue = null)
+ {
+ $this->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/vendor/scssphp/scssphp/src/Ast/Sass/ArgumentDeclaration.php b/vendor/scssphp/scssphp/src/Ast/Sass/ArgumentDeclaration.php
new file mode 100644
index 000000000..4098f328f
--- /dev/null
+++ b/vendor/scssphp/scssphp/src/Ast/Sass/ArgumentDeclaration.php
@@ -0,0 +1,242 @@
+<?php
+
+/**
+ * SCSSPHP
+ *
+ * @copyright 2012-2020 Leaf Corcoran
+ *
+ * @license http://opensource.org/licenses/MIT MIT
+ *
+ * @link http://scssphp.github.io/scssphp
+ */
+
+namespace ScssPhp\ScssPhp\Ast\Sass;
+
+use League\Uri\Contracts\UriInterface;
+use ScssPhp\ScssPhp\Exception\SassFormatException;
+use ScssPhp\ScssPhp\Exception\SassScriptException;
+use ScssPhp\ScssPhp\Logger\LoggerInterface;
+use ScssPhp\ScssPhp\Parser\ScssParser;
+use ScssPhp\ScssPhp\Util\Character;
+use ScssPhp\ScssPhp\Util\SpanUtil;
+use ScssPhp\ScssPhp\Util\StringUtil;
+use SourceSpan\FileSpan;
+
+/**
+ * An argument declaration, as for a function or mixin definition.
+ *
+ * @internal
+ */
+final class ArgumentDeclaration implements SassNode
+{
+ /**
+ * @var list<Argument>
+ */
+ private readonly array $arguments;
+
+ private readonly ?string $restArgument;
+
+ private readonly FileSpan $span;
+
+ /**
+ * @param list<Argument> $arguments
+ */
+ 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, ?UriInterface $url = null): ArgumentDeclaration
+ {
+ return (new ScssParser($contents, $logger, $url))->parseArgumentDeclaration();
+ }
+
+ public function isEmpty(): bool
+ {
+ return \count($this->arguments) === 0 && $this->restArgument === null;
+ }
+
+ /**
+ * @return list<Argument>
+ */
+ public function getArguments(): array
+ {
+ return $this->arguments;
+ }
+
+ public function getRestArgument(): ?string
+ {
+ return $this->restArgument;
+ }
+
+ public function getSpan(): FileSpan
+ {
+ return $this->span;
+ }
+
+ /**
+ * Returns {@see $span} expanded to include an identifier immediately before the
+ * declaration, if possible.
+ */
+ public function getSpanWithName(): FileSpan
+ {
+ $text = $this->span->getFile()->getText(0);
+
+ // Move backwards through any whitespace between the name and the arguments.
+ $i = $this->span->getStart()->getOffset() - 1;
+ while ($i > 0 && Character::isWhitespace($text[$i])) {
+ $i--;
+ }
+
+ // Then move backwards through the name itself.
+ if (!Character::isName($text[$i])) {
+ return $this->span;
+ }
+ $i--;
+ while ($i >= 0 && Character::isName($text[$i])) {
+ $i--;
+ }
+
+ // Trim because it's possible that this span is empty (for example, a mixin
+ // may be declared without an argument list).
+ return SpanUtil::trim($this->span->getFile()->span($i + 1, $this->span->getEnd()->getOffset()));
+ }
+
+ /**
+ * @param array<string, mixed> $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 %s%s allowed, but %d %s passed.',
+ \count($this->arguments),
+ empty($names) ? '' : 'positional ',
+ StringUtil::pluralize('argument', \count($this->arguments)),
+ $positional,
+ StringUtil::pluralize('was', $positional, 'were')
+ );
+ throw new SassScriptException($message);
+ }
+
+ if ($nameUsed < \count($names)) {
+ $unknownNames = array_values(array_diff(array_keys($names), array_map(fn($argument) => $argument->getName(), $this->arguments)));
+ \assert(\count($unknownNames) > 0);
+ $message = sprintf(
+ 'No %s named %s.',
+ StringUtil::pluralize('argument', \count($unknownNames)),
+ StringUtil::toSentence(array_map(fn ($name) => '$' . $name, $unknownNames), 'or')
+ );
+ 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 array<string, mixed> $names Only keys are relevant
+ */
+ 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/vendor/scssphp/scssphp/src/Ast/Sass/ArgumentInvocation.php b/vendor/scssphp/scssphp/src/Ast/Sass/ArgumentInvocation.php
new file mode 100644
index 000000000..df201928b
--- /dev/null
+++ b/vendor/scssphp/scssphp/src/Ast/Sass/ArgumentInvocation.php
@@ -0,0 +1,125 @@
+<?php
+
+/**
+ * SCSSPHP
+ *
+ * @copyright 2012-2020 Leaf Corcoran
+ *
+ * @license http://opensource.org/licenses/MIT MIT
+ *
+ * @link http://scssphp.github.io/scssphp
+ */
+
+namespace ScssPhp\ScssPhp\Ast\Sass;
+
+use ScssPhp\ScssPhp\Ast\Sass\Expression\ListExpression;
+use ScssPhp\ScssPhp\Value\ListSeparator;
+use SourceSpan\FileSpan;
+
+/**
+ * A set of arguments passed in to a function or mixin.
+ *
+ * @internal
+ */
+final class ArgumentInvocation implements SassNode
+{
+ /**
+ * @var list<Expression>
+ */
+ private readonly array $positional;
+
+ /**
+ * @var array<string, Expression>
+ */
+ private readonly array $named;
+
+ private readonly ?Expression $rest;
+
+ private readonly ?Expression $keywordRest;
+
+ private readonly FileSpan $span;
+
+ /**
+ * @param list<Expression> $positional
+ * @param array<string, Expression> $named
+ */
+ 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<Expression>
+ */
+ public function getPositional(): array
+ {
+ return $this->positional;
+ }
+
+ /**
+ * @return array<string, Expression>
+ */
+ 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 = [];
+ foreach ($this->positional as $argument) {
+ $parts[] = $this->parenthesizeArgument($argument);
+ }
+ foreach ($this->named as $name => $arg) {
+ $parts[] = "\$$name: {$this->parenthesizeArgument($arg)}";
+ }
+ if ($this->rest !== null) {
+ $parts[] = "{$this->parenthesizeArgument($this->rest)}...";
+ }
+ if ($this->keywordRest !== null) {
+ $parts[] = "{$this->parenthesizeArgument($this->keywordRest)}...";
+ }
+
+ return '(' . implode(', ', $parts) . ')';
+ }
+
+ private function parenthesizeArgument(Expression $argument): string
+ {
+ if ($argument instanceof ListExpression && $argument->getSeparator() === ListSeparator::COMMA && !$argument->hasBrackets() && \count($argument->getContents()) > 1) {
+ return "($argument)";
+ }
+
+ return (string) $argument;
+ }
+}
diff --git a/vendor/scssphp/scssphp/src/Ast/Sass/AtRootQuery.php b/vendor/scssphp/scssphp/src/Ast/Sass/AtRootQuery.php
new file mode 100644
index 000000000..8e40deea4
--- /dev/null
+++ b/vendor/scssphp/scssphp/src/Ast/Sass/AtRootQuery.php
@@ -0,0 +1,155 @@
+<?php
+
+/**
+ * SCSSPHP
+ *
+ * @copyright 2012-2020 Leaf Corcoran
+ *
+ * @license http://opensource.org/licenses/MIT MIT
+ *
+ * @link http://scssphp.github.io/scssphp
+ */
+
+namespace ScssPhp\ScssPhp\Ast\Sass;
+
+use League\Uri\Contracts\UriInterface;
+use ScssPhp\ScssPhp\Ast\Css\CssAtRule;
+use ScssPhp\ScssPhp\Ast\Css\CssMediaRule;
+use ScssPhp\ScssPhp\Ast\Css\CssParentNode;
+use ScssPhp\ScssPhp\Ast\Css\CssStyleRule;
+use ScssPhp\ScssPhp\Ast\Css\CssSupportsRule;
+use ScssPhp\ScssPhp\Exception\SassFormatException;
+use ScssPhp\ScssPhp\Logger\LoggerInterface;
+use ScssPhp\ScssPhp\Parser\AtRootQueryParser;
+use ScssPhp\ScssPhp\Parser\InterpolationMap;
+
+/**
+ * A query for the `@at-root` rule.
+ *
+ * @internal
+ */
+final class AtRootQuery
+{
+ /**
+ * Whether the query includes or excludes rules with the specified names.
+ */
+ private readonly bool $include;
+
+ /**
+ * The names of the rules included or excluded by this query.
+ *
+ * There are two special names. "all" indicates that all rules are included
+ * or excluded, and "rule" indicates style rules are included or excluded.
+ *
+ * @var string[]
+ */
+ private readonly array $names;
+
+ /**
+ * Whether this includes or excludes *all* rules.
+ */
+ private readonly bool $all;
+
+ /**
+ * Whether this includes or excludes style rules.
+ */
+ private readonly bool $rule;
+
+ /**
+ * Parses an at-root query from $contents.
+ *
+ * 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, ?UriInterface $url = null, ?InterpolationMap $interpolationMap = null): AtRootQuery
+ {
+ return (new AtRootQueryParser($contents, $logger, $url, $interpolationMap))->parse();
+ }
+
+ /**
+ * @param string[] $names
+ */
+ 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
+ */
+ 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/vendor/scssphp/scssphp/src/Ast/Sass/CallableInvocation.php b/vendor/scssphp/scssphp/src/Ast/Sass/CallableInvocation.php
new file mode 100644
index 000000000..94be58d9c
--- /dev/null
+++ b/vendor/scssphp/scssphp/src/Ast/Sass/CallableInvocation.php
@@ -0,0 +1,18 @@
+<?php
+
+/**
+ * SCSSPHP
+ *
+ * @copyright 2012-2020 Leaf Corcoran
+ *
+ * @license http://opensource.org/licenses/MIT MIT
+ *
+ * @link http://scssphp.github.io/scssphp
+ */
+
+namespace ScssPhp\ScssPhp\Ast\Sass;
+
+interface CallableInvocation extends SassNode
+{
+ public function getArguments(): ArgumentInvocation;
+}
diff --git a/vendor/scssphp/scssphp/src/Ast/Sass/ConfiguredVariable.php b/vendor/scssphp/scssphp/src/Ast/Sass/ConfiguredVariable.php
new file mode 100644
index 000000000..1d9e6e10e
--- /dev/null
+++ b/vendor/scssphp/scssphp/src/Ast/Sass/ConfiguredVariable.php
@@ -0,0 +1,70 @@
+<?php
+
+/**
+ * SCSSPHP
+ *
+ * @copyright 2012-2020 Leaf Corcoran
+ *
+ * @license http://opensource.org/licenses/MIT MIT
+ *
+ * @link http://scssphp.github.io/scssphp
+ */
+
+namespace ScssPhp\ScssPhp\Ast\Sass;
+
+use ScssPhp\ScssPhp\Util\SpanUtil;
+use SourceSpan\FileSpan;
+
+/**
+ * A variable configured by a `with` clause in a `@use` or `@forward` rule.
+ *
+ * @internal
+ */
+final class ConfiguredVariable implements SassNode, SassDeclaration
+{
+ private readonly string $name;
+
+ private readonly Expression $expression;
+
+ private readonly FileSpan $span;
+
+ private readonly bool $guarded;
+
+ public function __construct(string $name, Expression $expression, FileSpan $span, bool $guarded = false)
+ {
+ $this->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/vendor/scssphp/scssphp/src/Ast/Sass/Expression.php b/vendor/scssphp/scssphp/src/Ast/Sass/Expression.php
new file mode 100644
index 000000000..99fb06769
--- /dev/null
+++ b/vendor/scssphp/scssphp/src/Ast/Sass/Expression.php
@@ -0,0 +1,30 @@
+<?php
+
+/**
+ * SCSSPHP
+ *
+ * @copyright 2012-2020 Leaf Corcoran
+ *
+ * @license http://opensource.org/licenses/MIT MIT
+ *
+ * @link http://scssphp.github.io/scssphp
+ */
+
+namespace ScssPhp\ScssPhp\Ast\Sass;
+
+use ScssPhp\ScssPhp\Visitor\ExpressionVisitor;
+
+/**
+ * A SassScript expression in a Sass syntax tree.
+ *
+ * @internal
+ */
+interface Expression extends SassNode
+{
+ /**
+ * @template T
+ * @param ExpressionVisitor<T> $visitor
+ * @return T
+ */
+ public function accept(ExpressionVisitor $visitor);
+}
diff --git a/vendor/scssphp/scssphp/src/Ast/Sass/Expression/BinaryOperationExpression.php b/vendor/scssphp/scssphp/src/Ast/Sass/Expression/BinaryOperationExpression.php
new file mode 100644
index 000000000..ed9f53fd8
--- /dev/null
+++ b/vendor/scssphp/scssphp/src/Ast/Sass/Expression/BinaryOperationExpression.php
@@ -0,0 +1,146 @@
+<?php
+
+/**
+ * SCSSPHP
+ *
+ * @copyright 2012-2020 Leaf Corcoran
+ *
+ * @license http://opensource.org/licenses/MIT MIT
+ *
+ * @link http://scssphp.github.io/scssphp
+ */
+
+namespace ScssPhp\ScssPhp\Ast\Sass\Expression;
+
+use ScssPhp\ScssPhp\Ast\Sass\Expression;
+use ScssPhp\ScssPhp\Util\SpanUtil;
+use ScssPhp\ScssPhp\Visitor\ExpressionVisitor;
+use SourceSpan\FileSpan;
+
+/**
+ * A binary operator, as in `1 + 2` or `$this and $other`.
+ *
+ * @internal
+ */
+final class BinaryOperationExpression implements Expression
+{
+ private readonly BinaryOperator $operator;
+
+ private readonly Expression $left;
+
+ private readonly Expression $right;
+
+ /**
+ * Whether this is a dividedBy operation that may be interpreted as slash-separated numbers.
+ */
+ private bool $allowsSlash = false;
+
+ public function __construct(BinaryOperator $operator, Expression $left, Expression $right)
+ {
+ $this->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;
+ }
+
+ public function getOperator(): BinaryOperator
+ {
+ 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);
+ }
+
+ /**
+ * Returns the span that covers only {@see $operator}.
+ *
+ * @internal
+ */
+ public function getOperatorSpan(): FileSpan
+ {
+ $leftSpan = $this->left->getSpan();
+ $rightSpan = $this->right->getSpan();
+
+ if ($leftSpan->getFile() === $rightSpan->getFile() && $leftSpan->getEnd()->getOffset() < $rightSpan->getStart()->getOffset()) {
+ return SpanUtil::trim($leftSpan->getFile()->span($leftSpan->getEnd()->getOffset(), $rightSpan->getStart()->getOffset()));
+ }
+
+ return $this->getSpan();
+ }
+
+ public function accept(ExpressionVisitor $visitor)
+ {
+ return $visitor->visitBinaryOperationExpression($this);
+ }
+
+ public function __toString(): string
+ {
+ $buffer = '';
+
+ $leftNeedsParens = ($this->left instanceof BinaryOperationExpression && $this->left->getOperator()->getPrecedence() < $this->operator->getPrecedence()) || ($this->left instanceof ListExpression && !$this->left->hasBrackets() && \count($this->left->getContents()) > 1);
+ if ($leftNeedsParens) {
+ $buffer .= '(';
+ }
+ $buffer .= $this->left;
+ if ($leftNeedsParens) {
+ $buffer .= ')';
+ }
+
+ $buffer .= ' ';
+ $buffer .= $this->operator->getOperator();
+ $buffer .= ' ';
+
+ $rightNeedsParens = ($this->right instanceof BinaryOperationExpression && $this->right->getOperator()->getPrecedence() <= $this->operator->getPrecedence() && !($this->right->operator === $this->operator && $this->operator->isAssociative())) || ($this->right instanceof ListExpression && !$this->right->hasBrackets() && \count($this->right->getContents()) > 1);
+ if ($rightNeedsParens) {
+ $buffer .= '(';
+ }
+ $buffer .= $this->right;
+ if ($rightNeedsParens) {
+ $buffer .= ')';
+ }
+
+ return $buffer;
+ }
+}
diff --git a/vendor/scssphp/scssphp/src/Ast/Sass/Expression/BinaryOperator.php b/vendor/scssphp/scssphp/src/Ast/Sass/Expression/BinaryOperator.php
new file mode 100644
index 000000000..c43c2b54b
--- /dev/null
+++ b/vendor/scssphp/scssphp/src/Ast/Sass/Expression/BinaryOperator.php
@@ -0,0 +1,83 @@
+<?php
+
+/**
+ * SCSSPHP
+ *
+ * @copyright 2012-2020 Leaf Corcoran
+ *
+ * @license http://opensource.org/licenses/MIT MIT
+ *
+ * @link http://scssphp.github.io/scssphp
+ */
+
+namespace ScssPhp\ScssPhp\Ast\Sass\Expression;
+
+/**
+ * @internal
+ */
+enum BinaryOperator
+{
+ case SINGLE_EQUALS;
+ case OR;
+ case AND;
+ case EQUALS;
+ case NOT_EQUALS;
+ case GREATER_THAN;
+ case GREATER_THAN_OR_EQUALS;
+ case LESS_THAN;
+ case LESS_THAN_OR_EQUALS;
+ case PLUS;
+ case MINUS;
+ case TIMES;
+ case DIVIDED_BY;
+ case MODULO;
+
+ /**
+ * The Sass syntax for this operator
+ */
+ public function getOperator(): string
+ {
+ return match ($this) {
+ self::SINGLE_EQUALS => '=',
+ self::OR => 'or',
+ self::AND => 'and',
+ self::EQUALS => '==',
+ self::NOT_EQUALS => '!=',
+ self::GREATER_THAN => '>',
+ self::GREATER_THAN_OR_EQUALS => '>=',
+ self::LESS_THAN => '<',
+ self::LESS_THAN_OR_EQUALS => '<=',
+ self::PLUS => '+',
+ self::MINUS => '-',
+ self::TIMES => '*',
+ self::DIVIDED_BY => '/',
+ self::MODULO => '%',
+ };
+ }
+
+ public function getPrecedence(): int
+ {
+ return match ($this) {
+ self::SINGLE_EQUALS => 0,
+ self::OR => 1,
+ self::AND => 2,
+ self::EQUALS, self::NOT_EQUALS => 3,
+ self::GREATER_THAN, self::GREATER_THAN_OR_EQUALS, self::LESS_THAN, self::LESS_THAN_OR_EQUALS => 4,
+ self::PLUS, self::MINUS => 5,
+ self::TIMES, self::DIVIDED_BY, self::MODULO => 6,
+ };
+ }
+
+ /**
+ * Whether this operation has the [associative property].
+ *
+ * [associative property]: https://en.wikipedia.org/wiki/Associative_property
+ */
+ public function isAssociative(): bool
+ {
+ return match ($this) {
+ self::OR, self::AND, self::PLUS, self::TIMES => true,
+ default => false,
+ };
+ }
+}
diff --git a/vendor/scssphp/scssphp/src/Ast/Sass/Expression/BooleanExpression.php b/vendor/scssphp/scssphp/src/Ast/Sass/Expression/BooleanExpression.php
new file mode 100644
index 000000000..5464a76ce
--- /dev/null
+++ b/vendor/scssphp/scssphp/src/Ast/Sass/Expression/BooleanExpression.php
@@ -0,0 +1,55 @@
+<?php
+
+/**
+ * SCSSPHP
+ *
+ * @copyright 2012-2020 Leaf Corcoran
+ *
+ * @license http://opensource.org/licenses/MIT MIT
+ *
+ * @link http://scssphp.github.io/scssphp
+ */
+
+namespace ScssPhp\ScssPhp\Ast\Sass\Expression;
+
+use ScssPhp\ScssPhp\Ast\Sass\Expression;
+use ScssPhp\ScssPhp\Visitor\ExpressionVisitor;
+use SourceSpan\FileSpan;
+
+/**
+ * A boolean literal, `true` or `false`.
+ *
+ * @internal
+ */
+final class BooleanExpression implements Expression
+{
+ private readonly bool $value;
+
+ private readonly FileSpan $span;
+
+ public function __construct(bool $value, FileSpan $span)
+ {
+ $this->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/vendor/scssphp/scssphp/src/Ast/Sass/Expression/ColorExpression.php b/vendor/scssphp/scssphp/src/Ast/Sass/Expression/ColorExpression.php
new file mode 100644
index 000000000..c8c30b496
--- /dev/null
+++ b/vendor/scssphp/scssphp/src/Ast/Sass/Expression/ColorExpression.php
@@ -0,0 +1,56 @@
+<?php
+
+/**
+ * SCSSPHP
+ *
+ * @copyright 2012-2020 Leaf Corcoran
+ *
+ * @license http://opensource.org/licenses/MIT MIT
+ *
+ * @link http://scssphp.github.io/scssphp
+ */
+
+namespace ScssPhp\ScssPhp\Ast\Sass\Expression;
+
+use ScssPhp\ScssPhp\Ast\Sass\Expression;
+use ScssPhp\ScssPhp\Value\SassColor;
+use ScssPhp\ScssPhp\Visitor\ExpressionVisitor;
+use SourceSpan\FileSpan;
+
+/**
+ * A color literal.
+ *
+ * @internal
+ */
+final class ColorExpression implements Expression
+{
+ private readonly SassColor $value;
+
+ private readonly FileSpan $span;
+
+ public function __construct(SassColor $value, FileSpan $span)
+ {
+ $this->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/vendor/scssphp/scssphp/src/Ast/Sass/Expression/FunctionExpression.php b/vendor/scssphp/scssphp/src/Ast/Sass/Expression/FunctionExpression.php
new file mode 100644
index 000000000..183cd12ec
--- /dev/null
+++ b/vendor/scssphp/scssphp/src/Ast/Sass/Expression/FunctionExpression.php
@@ -0,0 +1,134 @@
+<?php
+
+/**
+ * SCSSPHP
+ *
+ * @copyright 2012-2020 Leaf Corcoran
+ *
+ * @license http://opensource.org/licenses/MIT MIT
+ *
+ * @link http://scssphp.github.io/scssphp
+ */
+
+namespace ScssPhp\ScssPhp\Ast\Sass\Expression;
+
+use ScssPhp\ScssPhp\Ast\Sass\ArgumentInvocation;
+use ScssPhp\ScssPhp\Ast\Sass\CallableInvocation;
+use ScssPhp\ScssPhp\Ast\Sass\Expression;
+use ScssPhp\ScssPhp\Ast\Sass\SassReference;
+use ScssPhp\ScssPhp\Util\SpanUtil;
+use ScssPhp\ScssPhp\Visitor\ExpressionVisitor;
+use SourceSpan\FileSpan;
+
+/**
+ * A function invocation.
+ *
+ * This may be a plain CSS function or a Sass function, but may not include
+ * interpolation.
+ *
+ * @internal
+ */
+final class FunctionExpression implements Expression, CallableInvocation, SassReference
+{
+ /**
+ * The name of the function being invoked, with underscores converted to
+ * hyphens.
+ *
+ * If this function is a plain CSS function, use {@see $originalName} instead.
+ */
+ private readonly string $name;
+
+ /**
+ * The name of the function being invoked, with underscores left as-is.
+ */
+ private readonly string $originalName;
+
+ /**
+ * The arguments to pass to the function.
+ */
+ private readonly ArgumentInvocation $arguments;
+
+ /**
+ * The namespace of the function being invoked, or `null` if it's invoked
+ * without a namespace.
+ */
+ private readonly ?string $namespace;
+
+ private readonly FileSpan $span;
+
+ public function __construct(string $originalName, ArgumentInvocation $arguments, FileSpan $span, ?string $namespace = null)
+ {
+ $this->span = $span;
+ $this->originalName = $originalName;
+ $this->arguments = $arguments;
+ $this->namespace = $namespace;
+ $this->name = str_replace('_', '-', $this->originalName);
+ }
+
+ 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 $this->name;
+ }
+
+ 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/vendor/scssphp/scssphp/src/Ast/Sass/Expression/IfExpression.php b/vendor/scssphp/scssphp/src/Ast/Sass/Expression/IfExpression.php
new file mode 100644
index 000000000..1096ccf63
--- /dev/null
+++ b/vendor/scssphp/scssphp/src/Ast/Sass/Expression/IfExpression.php
@@ -0,0 +1,79 @@
+<?php
+
+/**
+ * SCSSPHP
+ *
+ * @copyright 2012-2020 Leaf Corcoran
+ *
+ * @license http://opensource.org/licenses/MIT MIT
+ *
+ * @link http://scssphp.github.io/scssphp
+ */
+
+namespace ScssPhp\ScssPhp\Ast\Sass\Expression;
+
+use ScssPhp\ScssPhp\Ast\Sass\ArgumentDeclaration;
+use ScssPhp\ScssPhp\Ast\Sass\ArgumentInvocation;
+use ScssPhp\ScssPhp\Ast\Sass\CallableInvocation;
+use ScssPhp\ScssPhp\Ast\Sass\Expression;
+use ScssPhp\ScssPhp\Visitor\ExpressionVisitor;
+use SourceSpan\FileSpan;
+
+/**
+ * A ternary expression.
+ *
+ * This is defined as a separate syntactic construct rather than a normal
+ * function because only one of the `$if-true` and `$if-false` arguments are
+ * evaluated.
+ *
+ * @internal
+ */
+final class IfExpression implements Expression, CallableInvocation
+{
+ /**
+ * The arguments passed to `if()`.
+ */
+ private readonly ArgumentInvocation $arguments;
+
+ private readonly FileSpan $span;
+
+ private static ?ArgumentDeclaration $declaration = null;
+
+ public function __construct(ArgumentInvocation $arguments, FileSpan $span)
+ {
+ $this->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/vendor/scssphp/scssphp/src/Ast/Sass/Expression/InterpolatedFunctionExpression.php b/vendor/scssphp/scssphp/src/Ast/Sass/Expression/InterpolatedFunctionExpression.php
new file mode 100644
index 000000000..6a7b9e6a6
--- /dev/null
+++ b/vendor/scssphp/scssphp/src/Ast/Sass/Expression/InterpolatedFunctionExpression.php
@@ -0,0 +1,74 @@
+<?php
+
+/**
+ * SCSSPHP
+ *
+ * @copyright 2012-2020 Leaf Corcoran
+ *
+ * @license http://opensource.org/licenses/MIT MIT
+ *
+ * @link http://scssphp.github.io/scssphp
+ */
+
+namespace ScssPhp\ScssPhp\Ast\Sass\Expression;
+
+use ScssPhp\ScssPhp\Ast\Sass\ArgumentInvocation;
+use ScssPhp\ScssPhp\Ast\Sass\CallableInvocation;
+use ScssPhp\ScssPhp\Ast\Sass\Expression;
+use ScssPhp\ScssPhp\Ast\Sass\Interpolation;
+use ScssPhp\ScssPhp\Visitor\ExpressionVisitor;
+use SourceSpan\FileSpan;
+
+/**
+ * An interpolated function invocation.
+ *
+ * This is always a plain CSS function.
+ *
+ * @internal
+ */
+final class InterpolatedFunctionExpression implements Expression, CallableInvocation
+{
+ /**
+ * The name of the function being invoked.
+ */
+ private readonly Interpolation $name;
+
+ /**
+ * The arguments to pass to the function.
+ */
+ private readonly ArgumentInvocation $arguments;
+
+ private readonly FileSpan $span;
+
+ public function __construct(Interpolation $name, ArgumentInvocation $arguments, FileSpan $span)
+ {
+ $this->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/vendor/scssphp/scssphp/src/Ast/Sass/Expression/IsCalculationSafeVisitor.php b/vendor/scssphp/scssphp/src/Ast/Sass/Expression/IsCalculationSafeVisitor.php
new file mode 100644
index 000000000..4d2ffdf0f
--- /dev/null
+++ b/vendor/scssphp/scssphp/src/Ast/Sass/Expression/IsCalculationSafeVisitor.php
@@ -0,0 +1,129 @@
+<?php
+
+/**
+ * SCSSPHP
+ *
+ * @copyright 2012-2020 Leaf Corcoran
+ *
+ * @license http://opensource.org/licenses/MIT MIT
+ *
+ * @link http://scssphp.github.io/scssphp
+ */
+
+namespace ScssPhp\ScssPhp\Ast\Sass\Expression;
+
+use ScssPhp\ScssPhp\Ast\Sass\Expression;
+use ScssPhp\ScssPhp\Util\IterableUtil;
+use ScssPhp\ScssPhp\Value\ListSeparator;
+use ScssPhp\ScssPhp\Visitor\ExpressionVisitor;
+
+/**
+ * @template-implements ExpressionVisitor<bool>
+ *
+ * @internal
+ */
+final class IsCalculationSafeVisitor implements ExpressionVisitor
+{
+ public function visitBinaryOperationExpression(BinaryOperationExpression $node): bool
+ {
+ return \in_array($node->getOperator(), [BinaryOperator::TIMES, BinaryOperator::DIVIDED_BY, BinaryOperator::PLUS, BinaryOperator::MINUS], true) && ($node->getLeft()->accept($this) || $node->getRight()->accept($this));
+ }
+
+ public function visitBooleanExpression(BooleanExpression $node): bool
+ {
+ return false;
+ }
+
+ public function visitColorExpression(ColorExpression $node): bool
+ {
+ return false;
+ }
+
+ public function visitFunctionExpression(FunctionExpression $node): bool
+ {
+ return true;
+ }
+
+ public function visitInterpolatedFunctionExpression(InterpolatedFunctionExpression $node): bool
+ {
+ return true;
+ }
+
+ public function visitIfExpression(IfExpression $node): bool
+ {
+ return true;
+ }
+
+ public function visitListExpression(ListExpression $node): bool
+ {
+ return $node->getSeparator() === ListSeparator::SPACE && !$node->hasBrackets() && \count($node->getContents()) > 1 && IterableUtil::every($node->getContents(), fn(Expression $expression) => $expression->accept($this));
+ }
+
+ public function visitMapExpression(MapExpression $node): bool
+ {
+ return false;
+ }
+
+ public function visitNullExpression(NullExpression $node): bool
+ {
+ return false;
+ }
+
+ public function visitNumberExpression(NumberExpression $node): bool
+ {
+ return true;
+ }
+
+ public function visitParenthesizedExpression(ParenthesizedExpression $node): bool
+ {
+ return $node->getExpression()->accept($this);
+ }
+
+ public function visitSelectorExpression(SelectorExpression $node): bool
+ {
+ return false;
+ }
+
+ public function visitStringExpression(StringExpression $node): bool
+ {
+ if ($node->hasQuotes()) {
+ return false;
+ }
+
+ /**
+ * Exclude non-identifier constructs that are parsed as {@see StringExpression}s.
+ * We could just check if they parse as valid identifiers, but this is
+ * cheaper.
+ */
+ $text = $node->getText()->getInitialPlain();
+
+ // !important
+ return !str_starts_with($text, '!')
+ // ID-style identifiers
+ && !str_starts_with($text, '#')
+ // Unicode ranges
+ && ($text[1] ?? null) !== '+'
+ // url()
+ && ($text[3] ?? null) !== '(';
+ }
+
+ public function visitSupportsExpression(SupportsExpression $node): bool
+ {
+ return false;
+ }
+
+ public function visitUnaryOperationExpression(UnaryOperationExpression $node): bool
+ {
+ return false;
+ }
+
+ public function visitValueExpression(ValueExpression $node): bool
+ {
+ return false;
+ }
+
+ public function visitVariableExpression(VariableExpression $node): bool
+ {
+ return true;
+ }
+}
diff --git a/vendor/scssphp/scssphp/src/Ast/Sass/Expression/ListExpression.php b/vendor/scssphp/scssphp/src/Ast/Sass/Expression/ListExpression.php
new file mode 100644
index 000000000..4c5eed5a3
--- /dev/null
+++ b/vendor/scssphp/scssphp/src/Ast/Sass/Expression/ListExpression.php
@@ -0,0 +1,132 @@
+<?php
+
+/**
+ * SCSSPHP
+ *
+ * @copyright 2012-2020 Leaf Corcoran
+ *
+ * @license http://opensource.org/licenses/MIT MIT
+ *
+ * @link http://scssphp.github.io/scssphp
+ */
+
+namespace ScssPhp\ScssPhp\Ast\Sass\Expression;
+
+use ScssPhp\ScssPhp\Ast\Sass\Expression;
+use ScssPhp\ScssPhp\Value\ListSeparator;
+use ScssPhp\ScssPhp\Visitor\ExpressionVisitor;
+use SourceSpan\FileSpan;
+
+/**
+ * A list literal.
+ *
+ * @internal
+ */
+final class ListExpression implements Expression
+{
+ /**
+ * @var list<Expression>
+ */
+ private readonly array $contents;
+
+ private readonly ListSeparator $separator;
+
+ private readonly FileSpan $span;
+
+ private readonly bool $brackets;
+
+ /**
+ * ListExpression constructor.
+ *
+ * @param list<Expression> $contents
+ */
+ public function __construct(array $contents, ListSeparator $separator, FileSpan $span, bool $brackets = false)
+ {
+ $this->contents = $contents;
+ $this->separator = $separator;
+ $this->span = $span;
+ $this->brackets = $brackets;
+ }
+
+ /**
+ * @return list<Expression>
+ */
+ public function getContents(): array
+ {
+ return $this->contents;
+ }
+
+ public function getSeparator(): ListSeparator
+ {
+ 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 .= '[';
+ } elseif (\count($this->contents) === 0 || (\count($this->contents) === 1 && $this->separator === ListSeparator::COMMA)) {
+ $buffer .= '(';
+ }
+
+ $buffer .= implode(
+ $this->separator === ListSeparator::COMMA ? ', ' : ' ',
+ array_map(fn($element) => $this->elementNeedsParens($element) ? "($element)" : (string) $element, $this->contents)
+ );
+
+ if ($this->hasBrackets()) {
+ $buffer .= ']';
+ } elseif (\count($this->contents) === 0) {
+ $buffer .= ')';
+ } elseif (\count($this->contents) === 1 && $this->separator === ListSeparator::COMMA) {
+ $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/vendor/scssphp/scssphp/src/Ast/Sass/Expression/MapExpression.php b/vendor/scssphp/scssphp/src/Ast/Sass/Expression/MapExpression.php
new file mode 100644
index 000000000..acd66a26f
--- /dev/null
+++ b/vendor/scssphp/scssphp/src/Ast/Sass/Expression/MapExpression.php
@@ -0,0 +1,64 @@
+<?php
+
+/**
+ * SCSSPHP
+ *
+ * @copyright 2012-2020 Leaf Corcoran
+ *
+ * @license http://opensource.org/licenses/MIT MIT
+ *
+ * @link http://scssphp.github.io/scssphp
+ */
+
+namespace ScssPhp\ScssPhp\Ast\Sass\Expression;
+
+use ScssPhp\ScssPhp\Ast\Sass\Expression;
+use ScssPhp\ScssPhp\Visitor\ExpressionVisitor;
+use SourceSpan\FileSpan;
+
+/**
+ * A map literal.
+ *
+ * @internal
+ */
+final class MapExpression implements Expression
+{
+ /**
+ * @var list<array{Expression, Expression}>
+ */
+ private readonly array $pairs;
+
+ private readonly FileSpan $span;
+
+ /**
+ * @param list<array{Expression, Expression}> $pairs
+ */
+ public function __construct(array $pairs, FileSpan $span)
+ {
+ $this->pairs = $pairs;
+ $this->span = $span;
+ }
+
+ /**
+ * @return list<array{Expression, Expression}>
+ */
+ 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(fn($pair) => $pair[0] . ': ' . $pair[1], $this->pairs)) . ')';
+ }
+}
diff --git a/vendor/scssphp/scssphp/src/Ast/Sass/Expression/NullExpression.php b/vendor/scssphp/scssphp/src/Ast/Sass/Expression/NullExpression.php
new file mode 100644
index 000000000..e405e2989
--- /dev/null
+++ b/vendor/scssphp/scssphp/src/Ast/Sass/Expression/NullExpression.php
@@ -0,0 +1,47 @@
+<?php
+
+/**
+ * SCSSPHP
+ *
+ * @copyright 2012-2020 Leaf Corcoran
+ *
+ * @license http://opensource.org/licenses/MIT MIT
+ *
+ * @link http://scssphp.github.io/scssphp
+ */
+
+namespace ScssPhp\ScssPhp\Ast\Sass\Expression;
+
+use ScssPhp\ScssPhp\Ast\Sass\Expression;
+use ScssPhp\ScssPhp\Visitor\ExpressionVisitor;
+use SourceSpan\FileSpan;
+
+/**
+ * A null literal.
+ *
+ * @internal
+ */
+final class NullExpression implements Expression
+{
+ private readonly FileSpan $span;
+
+ public function __construct(FileSpan $span)
+ {
+ $this->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/vendor/scssphp/scssphp/src/Ast/Sass/Expression/NumberExpression.php b/vendor/scssphp/scssphp/src/Ast/Sass/Expression/NumberExpression.php
new file mode 100644
index 000000000..586287a15
--- /dev/null
+++ b/vendor/scssphp/scssphp/src/Ast/Sass/Expression/NumberExpression.php
@@ -0,0 +1,64 @@
+<?php
+
+/**
+ * SCSSPHP
+ *
+ * @copyright 2012-2020 Leaf Corcoran
+ *
+ * @license http://opensource.org/licenses/MIT MIT
+ *
+ * @link http://scssphp.github.io/scssphp
+ */
+
+namespace ScssPhp\ScssPhp\Ast\Sass\Expression;
+
+use ScssPhp\ScssPhp\Ast\Sass\Expression;
+use ScssPhp\ScssPhp\Value\SassNumber;
+use ScssPhp\ScssPhp\Visitor\ExpressionVisitor;
+use SourceSpan\FileSpan;
+
+/**
+ * A number literal.
+ *
+ * @internal
+ */
+final class NumberExpression implements Expression
+{
+ private readonly float $value;
+
+ private readonly FileSpan $span;
+
+ private readonly ?string $unit;
+
+ public function __construct(float $value, FileSpan $span, ?string $unit = null)
+ {
+ $this->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/vendor/scssphp/scssphp/src/Ast/Sass/Expression/ParenthesizedExpression.php b/vendor/scssphp/scssphp/src/Ast/Sass/Expression/ParenthesizedExpression.php
new file mode 100644
index 000000000..e3ab5b0d3
--- /dev/null
+++ b/vendor/scssphp/scssphp/src/Ast/Sass/Expression/ParenthesizedExpression.php
@@ -0,0 +1,55 @@
+<?php
+
+/**
+ * SCSSPHP
+ *
+ * @copyright 2012-2020 Leaf Corcoran
+ *
+ * @license http://opensource.org/licenses/MIT MIT
+ *
+ * @link http://scssphp.github.io/scssphp
+ */
+
+namespace ScssPhp\ScssPhp\Ast\Sass\Expression;
+
+use ScssPhp\ScssPhp\Ast\Sass\Expression;
+use ScssPhp\ScssPhp\Visitor\ExpressionVisitor;
+use SourceSpan\FileSpan;
+
+/**
+ * An expression wrapped in parentheses.
+ *
+ * @internal
+ */
+final class ParenthesizedExpression implements Expression
+{
+ private readonly Expression $expression;
+
+ private readonly FileSpan $span;
+
+ public function __construct(Expression $expression, FileSpan $span)
+ {
+ $this->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/vendor/scssphp/scssphp/src/Ast/Sass/Expression/SelectorExpression.php b/vendor/scssphp/scssphp/src/Ast/Sass/Expression/SelectorExpression.php
new file mode 100644
index 000000000..39d11f641
--- /dev/null
+++ b/vendor/scssphp/scssphp/src/Ast/Sass/Expression/SelectorExpression.php
@@ -0,0 +1,47 @@
+<?php
+
+/**
+ * SCSSPHP
+ *
+ * @copyright 2012-2020 Leaf Corcoran
+ *
+ * @license http://opensource.org/licenses/MIT MIT
+ *
+ * @link http://scssphp.github.io/scssphp
+ */
+
+namespace ScssPhp\ScssPhp\Ast\Sass\Expression;
+
+use ScssPhp\ScssPhp\Ast\Sass\Expression;
+use ScssPhp\ScssPhp\Visitor\ExpressionVisitor;
+use SourceSpan\FileSpan;
+
+/**
+ * A parent selector reference, `&`.
+ *
+ * @internal
+ */
+final class SelectorExpression implements Expression
+{
+ private readonly FileSpan $span;
+
+ public function __construct(FileSpan $span)
+ {
+ $this->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/vendor/scssphp/scssphp/src/Ast/Sass/Expression/StringExpression.php b/vendor/scssphp/scssphp/src/Ast/Sass/Expression/StringExpression.php
new file mode 100644
index 000000000..24865d688
--- /dev/null
+++ b/vendor/scssphp/scssphp/src/Ast/Sass/Expression/StringExpression.php
@@ -0,0 +1,181 @@
+<?php
+
+/**
+ * SCSSPHP
+ *
+ * @copyright 2012-2020 Leaf Corcoran
+ *
+ * @license http://opensource.org/licenses/MIT MIT
+ *
+ * @link http://scssphp.github.io/scssphp
+ */
+
+namespace ScssPhp\ScssPhp\Ast\Sass\Expression;
+
+use ScssPhp\ScssPhp\Ast\Sass\Expression;
+use ScssPhp\ScssPhp\Ast\Sass\Interpolation;
+use ScssPhp\ScssPhp\Parser\InterpolationBuffer;
+use ScssPhp\ScssPhp\Util\Character;
+use ScssPhp\ScssPhp\Visitor\ExpressionVisitor;
+use SourceSpan\FileSpan;
+
+/**
+ * A string literal.
+ *
+ * @internal
+ */
+final class StringExpression implements Expression
+{
+ private readonly Interpolation $text;
+
+ private readonly bool $quotes;
+
+ public function __construct(Interpolation $text, bool $quotes = false)
+ {
+ $this->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;
+ }
+
+ /**
+ * Interpolation that, when evaluated, produces the contents of this string.
+ *
+ * Unlike {@see asInterpolation}, escapes are resolved and quotes are not
+ * included.
+ * If this is a quoted string, escapes are resolved and quotes are not
+ * included in this text (unlike {@see asInterpolation}). If it's an unquoted
+ * string, escapes are *not* resolved.
+ */
+ 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<string|Expression> $parts
+ */
+ private static function bestQuote(array $parts): string
+ {
+ $containsDoubleQuote = false;
+
+ foreach ($parts as $part) {
+ if (!\is_string($part)) {
+ continue;
+ }
+
+ if (str_contains($part, "'")) {
+ return '"';
+ }
+
+ if (str_contains($part, '"')) {
+ $containsDoubleQuote = true;
+ }
+ }
+
+ return $containsDoubleQuote ? "'" : '"';
+ }
+
+ public function __toString(): string
+ {
+ return (string) $this->asInterpolation();
+ }
+}
diff --git a/vendor/scssphp/scssphp/src/Ast/Sass/Expression/SupportsExpression.php b/vendor/scssphp/scssphp/src/Ast/Sass/Expression/SupportsExpression.php
new file mode 100644
index 000000000..7deae01bd
--- /dev/null
+++ b/vendor/scssphp/scssphp/src/Ast/Sass/Expression/SupportsExpression.php
@@ -0,0 +1,56 @@
+<?php
+
+/**
+ * SCSSPHP
+ *
+ * @copyright 2012-2020 Leaf Corcoran
+ *
+ * @license http://opensource.org/licenses/MIT MIT
+ *
+ * @link http://scssphp.github.io/scssphp
+ */
+
+namespace ScssPhp\ScssPhp\Ast\Sass\Expression;
+
+use ScssPhp\ScssPhp\Ast\Sass\Expression;
+use ScssPhp\ScssPhp\Ast\Sass\SupportsCondition;
+use ScssPhp\ScssPhp\Visitor\ExpressionVisitor;
+use SourceSpan\FileSpan;
+
+/**
+ * An expression-level `@supports` condition.
+ *
+ * This appears only in the modifiers that come after a plain-CSS `@import`. It
+ * doesn't include the function name wrapping the condition.
+ *
+ * @internal
+ */
+final class SupportsExpression implements Expression
+{
+ private readonly SupportsCondition $condition;
+
+ public function __construct(SupportsCondition $condition)
+ {
+ $this->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/vendor/scssphp/scssphp/src/Ast/Sass/Expression/UnaryOperationExpression.php b/vendor/scssphp/scssphp/src/Ast/Sass/Expression/UnaryOperationExpression.php
new file mode 100644
index 000000000..5a796b9a8
--- /dev/null
+++ b/vendor/scssphp/scssphp/src/Ast/Sass/Expression/UnaryOperationExpression.php
@@ -0,0 +1,82 @@
+<?php
+
+/**
+ * SCSSPHP
+ *
+ * @copyright 2012-2020 Leaf Corcoran
+ *
+ * @license http://opensource.org/licenses/MIT MIT
+ *
+ * @link http://scssphp.github.io/scssphp
+ */
+
+namespace ScssPhp\ScssPhp\Ast\Sass\Expression;
+
+use ScssPhp\ScssPhp\Ast\Sass\Expression;
+use ScssPhp\ScssPhp\Visitor\ExpressionVisitor;
+use SourceSpan\FileSpan;
+
+/**
+ * A unary operator, as in `+$var` or `not fn()`.
+ *
+ * @internal
+ */
+final class UnaryOperationExpression implements Expression
+{
+ private readonly UnaryOperator $operator;
+
+ private readonly Expression $operand;
+
+ private readonly FileSpan $span;
+
+ public function __construct(UnaryOperator $operator, Expression $operand, FileSpan $span)
+ {
+ $this->operator = $operator;
+ $this->operand = $operand;
+ $this->span = $span;
+ }
+
+ public function getOperator(): UnaryOperator
+ {
+ 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->getOperator();
+ if ($this->operator === UnaryOperator::NOT) {
+ $buffer .= ' ';
+ }
+
+ $needsParens = $this->operand instanceof BinaryOperationExpression
+ || $this->operand instanceof UnaryOperationExpression
+ || ($this->operand instanceof ListExpression && !$this->operand->hasBrackets() && \count($this->operand->getContents()) > 1);
+
+ if ($needsParens) {
+ $buffer .= '(';
+ }
+
+ $buffer .= $this->operand;
+
+ if ($needsParens) {
+ $buffer .= ')';
+ }
+
+ return $buffer;
+ }
+}
diff --git a/vendor/scssphp/scssphp/src/Ast/Sass/Expression/UnaryOperator.php b/vendor/scssphp/scssphp/src/Ast/Sass/Expression/UnaryOperator.php
new file mode 100644
index 000000000..33cc34968
--- /dev/null
+++ b/vendor/scssphp/scssphp/src/Ast/Sass/Expression/UnaryOperator.php
@@ -0,0 +1,37 @@
+<?php
+
+/**
+ * SCSSPHP
+ *
+ * @copyright 2012-2020 Leaf Corcoran
+ *
+ * @license http://opensource.org/licenses/MIT MIT
+ *
+ * @link http://scssphp.github.io/scssphp
+ */
+
+namespace ScssPhp\ScssPhp\Ast\Sass\Expression;
+
+/**
+ * @internal
+ */
+enum UnaryOperator
+{
+ case PLUS;
+ case MINUS;
+ case DIVIDE;
+ case NOT;
+
+ /**
+ * The Sass syntax for this operator
+ */
+ public function getOperator(): string
+ {
+ return match ($this) {
+ self::PLUS => '+',
+ self::MINUS => '-',
+ self::DIVIDE => '/',
+ self::NOT => 'not',
+ };
+ }
+}
diff --git a/vendor/scssphp/scssphp/src/Ast/Sass/Expression/ValueExpression.php b/vendor/scssphp/scssphp/src/Ast/Sass/Expression/ValueExpression.php
new file mode 100644
index 000000000..3794582bc
--- /dev/null
+++ b/vendor/scssphp/scssphp/src/Ast/Sass/Expression/ValueExpression.php
@@ -0,0 +1,59 @@
+<?php
+
+/**
+ * SCSSPHP
+ *
+ * @copyright 2012-2020 Leaf Corcoran
+ *
+ * @license http://opensource.org/licenses/MIT MIT
+ *
+ * @link http://scssphp.github.io/scssphp
+ */
+
+namespace ScssPhp\ScssPhp\Ast\Sass\Expression;
+
+use ScssPhp\ScssPhp\Ast\Sass\Expression;
+use ScssPhp\ScssPhp\Value\Value;
+use ScssPhp\ScssPhp\Visitor\ExpressionVisitor;
+use SourceSpan\FileSpan;
+
+/**
+ * An expression that directly embeds a value.
+ *
+ * This is never constructed by the parser. It's only used when ASTs are
+ * constructed dynamically, as for the `call()` function.
+ *
+ * @internal
+ */
+final class ValueExpression implements Expression
+{
+ private readonly Value $value;
+
+ private readonly FileSpan $span;
+
+ public function __construct(Value $value, FileSpan $span)
+ {
+ $this->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/vendor/scssphp/scssphp/src/Ast/Sass/Expression/VariableExpression.php b/vendor/scssphp/scssphp/src/Ast/Sass/Expression/VariableExpression.php
new file mode 100644
index 000000000..5c70a2b29
--- /dev/null
+++ b/vendor/scssphp/scssphp/src/Ast/Sass/Expression/VariableExpression.php
@@ -0,0 +1,90 @@
+<?php
+
+/**
+ * SCSSPHP
+ *
+ * @copyright 2012-2020 Leaf Corcoran
+ *
+ * @license http://opensource.org/licenses/MIT MIT
+ *
+ * @link http://scssphp.github.io/scssphp
+ */
+
+namespace ScssPhp\ScssPhp\Ast\Sass\Expression;
+
+use ScssPhp\ScssPhp\Ast\Sass\Expression;
+use ScssPhp\ScssPhp\Ast\Sass\SassReference;
+use ScssPhp\ScssPhp\Util\SpanUtil;
+use ScssPhp\ScssPhp\Visitor\ExpressionVisitor;
+use SourceSpan\FileSpan;
+
+/**
+ * A Sass variable.
+ *
+ * @internal
+ */
+final class VariableExpression implements Expression, SassReference
+{
+ /**
+ * The name of this variable, with underscores converted to hyphens.
+ */
+ private readonly string $name;
+
+ /**
+ * The namespace of the variable being referenced, or `null` if it's
+ * referenced without a namespace.
+ */
+ private ?string $namespace;
+
+ private readonly FileSpan $span;
+
+ public function __construct(string $name, FileSpan $span, ?string $namespace = null)
+ {
+ $this->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
+ {
+ return $this->span->getText();
+ }
+}
diff --git a/vendor/scssphp/scssphp/src/Ast/Sass/Import.php b/vendor/scssphp/scssphp/src/Ast/Sass/Import.php
new file mode 100644
index 000000000..0a7dbb5df
--- /dev/null
+++ b/vendor/scssphp/scssphp/src/Ast/Sass/Import.php
@@ -0,0 +1,22 @@
+<?php
+
+/**
+ * SCSSPHP
+ *
+ * @copyright 2012-2020 Leaf Corcoran
+ *
+ * @license http://opensource.org/licenses/MIT MIT
+ *
+ * @link http://scssphp.github.io/scssphp
+ */
+
+namespace ScssPhp\ScssPhp\Ast\Sass;
+
+/**
+ * An interface for different types of import.
+ *
+ * @internal
+ */
+interface Import extends SassNode
+{
+}
diff --git a/vendor/scssphp/scssphp/src/Ast/Sass/Import/DynamicImport.php b/vendor/scssphp/scssphp/src/Ast/Sass/Import/DynamicImport.php
new file mode 100644
index 000000000..c6ce7b474
--- /dev/null
+++ b/vendor/scssphp/scssphp/src/Ast/Sass/Import/DynamicImport.php
@@ -0,0 +1,62 @@
+<?php
+
+/**
+ * SCSSPHP
+ *
+ * @copyright 2012-2020 Leaf Corcoran
+ *
+ * @license http://opensource.org/licenses/MIT MIT
+ *
+ * @link http://scssphp.github.io/scssphp
+ */
+
+namespace ScssPhp\ScssPhp\Ast\Sass\Import;
+
+use League\Uri\Contracts\UriInterface;
+use League\Uri\Uri;
+use ScssPhp\ScssPhp\Ast\Sass\Expression\StringExpression;
+use ScssPhp\ScssPhp\Ast\Sass\Import;
+use SourceSpan\FileSpan;
+
+/**
+ * An import that will load a Sass file at runtime.
+ *
+ * @internal
+ */
+final class DynamicImport implements Import
+{
+ /**
+ * The URI of the file to import.
+ *
+ * If this is relative, it's relative to the containing file.
+ */
+ private readonly string $urlString;
+
+ private readonly FileSpan $span;
+
+ public function __construct(string $urlString, FileSpan $span)
+ {
+ $this->urlString = $urlString;
+ $this->span = $span;
+ }
+
+ public function getUrl(): UriInterface
+ {
+ return Uri::new($this->urlString);
+ }
+
+ 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/vendor/scssphp/scssphp/src/Ast/Sass/Import/StaticImport.php b/vendor/scssphp/scssphp/src/Ast/Sass/Import/StaticImport.php
new file mode 100644
index 000000000..8582dcee0
--- /dev/null
+++ b/vendor/scssphp/scssphp/src/Ast/Sass/Import/StaticImport.php
@@ -0,0 +1,73 @@
+<?php
+
+/**
+ * SCSSPHP
+ *
+ * @copyright 2012-2020 Leaf Corcoran
+ *
+ * @license http://opensource.org/licenses/MIT MIT
+ *
+ * @link http://scssphp.github.io/scssphp
+ */
+
+namespace ScssPhp\ScssPhp\Ast\Sass\Import;
+
+use ScssPhp\ScssPhp\Ast\Sass\Import;
+use ScssPhp\ScssPhp\Ast\Sass\Interpolation;
+use SourceSpan\FileSpan;
+
+/**
+ * An import that produces a plain CSS `@import` rule.
+ *
+ * @internal
+ */
+final class StaticImport implements Import
+{
+ /**
+ * The URL for this import.
+ *
+ * This already contains quotes.
+ */
+ private readonly Interpolation $url;
+
+ /**
+ * The modifiers (such as media or supports queries) attached to this import,
+ * or `null` if none are attached.
+ */
+ private readonly ?Interpolation $modifiers;
+
+ private readonly FileSpan $span;
+
+ public function __construct(Interpolation $url, FileSpan $span, ?Interpolation $modifiers = null)
+ {
+ $this->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/vendor/scssphp/scssphp/src/Ast/Sass/Interpolation.php b/vendor/scssphp/scssphp/src/Ast/Sass/Interpolation.php
new file mode 100644
index 000000000..cae238614
--- /dev/null
+++ b/vendor/scssphp/scssphp/src/Ast/Sass/Interpolation.php
@@ -0,0 +1,139 @@
+<?php
+
+/**
+ * SCSSPHP
+ *
+ * @copyright 2012-2020 Leaf Corcoran
+ *
+ * @license http://opensource.org/licenses/MIT MIT
+ *
+ * @link http://scssphp.github.io/scssphp
+ */
+
+namespace ScssPhp\ScssPhp\Ast\Sass;
+
+use ScssPhp\ScssPhp\Parser\InterpolationBuffer;
+use SourceSpan\FileSpan;
+
+/**
+ * Plain text interpolated with Sass expressions.
+ *
+ * @internal
+ */
+final class Interpolation implements SassNode
+{
+ /**
+ * @var list<string|Expression>
+ */
+ private readonly array $contents;
+
+ private readonly FileSpan $span;
+
+ /**
+ * Creates a new {@see Interpolation} by concatenating a sequence of strings,
+ * {@see Expression}s, or nested {@see Interpolation}s.
+ *
+ * @param array<string|Expression|Interpolation> $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.', get_debug_type($element)));
+ }
+ }
+
+ return $buffer->buildInterpolation($span);
+ }
+
+ /**
+ * @param list<string|Expression> $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<string|Expression>
+ */
+ public function getContents(): array
+ {
+ return $this->contents;
+ }
+
+ public function getSpan(): FileSpan
+ {
+ return $this->span;
+ }
+
+ /**
+ * Returns whether this contains no interpolated expressions.
+ */
+ public function isPlain(): bool
+ {
+ return $this->getAsPlain() !== null;
+ }
+
+ /**
+ * 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(fn($value) => \is_string($value) ? $value : '#{' . $value . '}', $this->contents));
+ }
+}
diff --git a/vendor/scssphp/scssphp/src/Ast/Sass/SassDeclaration.php b/vendor/scssphp/scssphp/src/Ast/Sass/SassDeclaration.php
new file mode 100644
index 000000000..569921ebf
--- /dev/null
+++ b/vendor/scssphp/scssphp/src/Ast/Sass/SassDeclaration.php
@@ -0,0 +1,37 @@
+<?php
+
+/**
+ * SCSSPHP
+ *
+ * @copyright 2012-2020 Leaf Corcoran
+ *
+ * @license http://opensource.org/licenses/MIT MIT
+ *
+ * @link http://scssphp.github.io/scssphp
+ */
+
+namespace ScssPhp\ScssPhp\Ast\Sass;
+
+use SourceSpan\FileSpan;
+
+/**
+ * A common interface for any node that declares a Sass member.
+ *
+ * @internal
+ */
+interface SassDeclaration extends SassNode
+{
+ /**
+ * The name of the declaration, with underscores converted to hyphens.
+ *
+ * This does not include the `$` for variables.
+ */
+ public function getName(): string;
+
+ /**
+ * The span containing this declaration's name.
+ *
+ * This includes the `$` for variables.
+ */
+ public function getNameSpan(): FileSpan;
+}
diff --git a/vendor/scssphp/scssphp/src/Ast/Sass/SassNode.php b/vendor/scssphp/scssphp/src/Ast/Sass/SassNode.php
new file mode 100644
index 000000000..70804ea0a
--- /dev/null
+++ b/vendor/scssphp/scssphp/src/Ast/Sass/SassNode.php
@@ -0,0 +1,24 @@
+<?php
+
+/**
+ * SCSSPHP
+ *
+ * @copyright 2012-2020 Leaf Corcoran
+ *
+ * @license http://opensource.org/licenses/MIT MIT
+ *
+ * @link http://scssphp.github.io/scssphp
+ */
+
+namespace ScssPhp\ScssPhp\Ast\Sass;
+
+use ScssPhp\ScssPhp\Ast\AstNode;
+
+/**
+ * A node in the abstract syntax tree for an unevaluated Sass file.
+ *
+ * @internal
+ */
+interface SassNode extends AstNode
+{
+}
diff --git a/vendor/scssphp/scssphp/src/Ast/Sass/SassReference.php b/vendor/scssphp/scssphp/src/Ast/Sass/SassReference.php
new file mode 100644
index 000000000..b8d635ee5
--- /dev/null
+++ b/vendor/scssphp/scssphp/src/Ast/Sass/SassReference.php
@@ -0,0 +1,50 @@
+<?php
+
+/**
+ * SCSSPHP
+ *
+ * @copyright 2012-2020 Leaf Corcoran
+ *
+ * @license http://opensource.org/licenses/MIT MIT
+ *
+ * @link http://scssphp.github.io/scssphp
+ */
+
+namespace ScssPhp\ScssPhp\Ast\Sass;
+
+use SourceSpan\FileSpan;
+
+/**
+ * A common interface for any node that references a Sass member.
+ *
+ * @internal
+ */
+interface SassReference extends SassNode
+{
+ /**
+ * The namespace of the member being referenced, or `null` if it's referenced
+ * without a namespace.
+ */
+ public function getNamespace(): ?string;
+
+ /**
+ * The name of the member being referenced, with underscores converted to
+ * hyphens.
+ *
+ * This does not include the `$` for variables.
+ */
+ public function getName(): string;
+
+ /**
+ * The span containing this reference's name.
+ *
+ * For variables, this should include the `$`.
+ */
+ public function getNameSpan(): FileSpan;
+
+ /**
+ * The span containing this reference's namespace, null if {@see getNamespace} is
+ * null.
+ */
+ public function getNamespaceSpan(): ?FileSpan;
+}
diff --git a/vendor/scssphp/scssphp/src/Ast/Sass/Statement.php b/vendor/scssphp/scssphp/src/Ast/Sass/Statement.php
new file mode 100644
index 000000000..64cabfdc1
--- /dev/null
+++ b/vendor/scssphp/scssphp/src/Ast/Sass/Statement.php
@@ -0,0 +1,30 @@
+<?php
+
+/**
+ * SCSSPHP
+ *
+ * @copyright 2012-2020 Leaf Corcoran
+ *
+ * @license http://opensource.org/licenses/MIT MIT
+ *
+ * @link http://scssphp.github.io/scssphp
+ */
+
+namespace ScssPhp\ScssPhp\Ast\Sass;
+
+use ScssPhp\ScssPhp\Visitor\StatementVisitor;
+
+/**
+ * A statement in a Sass syntax tree.
+ *
+ * @internal
+ */
+interface Statement extends SassNode
+{
+ /**
+ * @template T
+ * @param StatementVisitor<T> $visitor
+ * @return T
+ */
+ public function accept(StatementVisitor $visitor);
+}
diff --git a/vendor/scssphp/scssphp/src/Ast/Sass/Statement/AtRootRule.php b/vendor/scssphp/scssphp/src/Ast/Sass/Statement/AtRootRule.php
new file mode 100644
index 000000000..b793a44a9
--- /dev/null
+++ b/vendor/scssphp/scssphp/src/Ast/Sass/Statement/AtRootRule.php
@@ -0,0 +1,72 @@
+<?php
+
+/**
+ * SCSSPHP
+ *
+ * @copyright 2012-2020 Leaf Corcoran
+ *
+ * @license http://opensource.org/licenses/MIT MIT
+ *
+ * @link http://scssphp.github.io/scssphp
+ */
+
+namespace ScssPhp\ScssPhp\Ast\Sass\Statement;
+
+use ScssPhp\ScssPhp\Ast\Sass\Interpolation;
+use ScssPhp\ScssPhp\Ast\Sass\Statement;
+use ScssPhp\ScssPhp\Visitor\StatementVisitor;
+use SourceSpan\FileSpan;
+
+/**
+ * A `@at-root` rule.
+ *
+ * This moves it contents "up" the tree through parent nodes.
+ *
+ * @extends ParentStatement<Statement[]>
+ *
+ * @internal
+ */
+final class AtRootRule extends ParentStatement
+{
+ private readonly ?Interpolation $query;
+
+ private readonly FileSpan $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/vendor/scssphp/scssphp/src/Ast/Sass/Statement/AtRule.php b/vendor/scssphp/scssphp/src/Ast/Sass/Statement/AtRule.php
new file mode 100644
index 000000000..db7413c80
--- /dev/null
+++ b/vendor/scssphp/scssphp/src/Ast/Sass/Statement/AtRule.php
@@ -0,0 +1,81 @@
+<?php
+
+/**
+ * SCSSPHP
+ *
+ * @copyright 2012-2020 Leaf Corcoran
+ *
+ * @license http://opensource.org/licenses/MIT MIT
+ *
+ * @link http://scssphp.github.io/scssphp
+ */
+
+namespace ScssPhp\ScssPhp\Ast\Sass\Statement;
+
+use ScssPhp\ScssPhp\Ast\Sass\Interpolation;
+use ScssPhp\ScssPhp\Ast\Sass\Statement;
+use ScssPhp\ScssPhp\Visitor\StatementVisitor;
+use SourceSpan\FileSpan;
+
+/**
+ * An unknown at-rule.
+ *
+ * @extends ParentStatement<Statement[]|null>
+ *
+ * @internal
+ */
+final class AtRule extends ParentStatement
+{
+ private readonly Interpolation $name;
+
+ private readonly ?Interpolation $value;
+
+ private readonly FileSpan $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/vendor/scssphp/scssphp/src/Ast/Sass/Statement/CallableDeclaration.php b/vendor/scssphp/scssphp/src/Ast/Sass/Statement/CallableDeclaration.php
new file mode 100644
index 000000000..f93ec0ea3
--- /dev/null
+++ b/vendor/scssphp/scssphp/src/Ast/Sass/Statement/CallableDeclaration.php
@@ -0,0 +1,82 @@
+<?php
+
+/**
+ * SCSSPHP
+ *
+ * @copyright 2012-2020 Leaf Corcoran
+ *
+ * @license http://opensource.org/licenses/MIT MIT
+ *
+ * @link http://scssphp.github.io/scssphp
+ */
+
+namespace ScssPhp\ScssPhp\Ast\Sass\Statement;
+
+use ScssPhp\ScssPhp\Ast\Sass\ArgumentDeclaration;
+use ScssPhp\ScssPhp\Ast\Sass\Statement;
+use SourceSpan\FileSpan;
+
+/**
+ * An abstract class for callables (functions or mixins) that are declared in
+ * user code.
+ *
+ * @extends ParentStatement<Statement[]>
+ *
+ * @internal
+ */
+abstract class CallableDeclaration extends ParentStatement
+{
+ private readonly string $name;
+
+ private readonly string $originalName;
+
+ private readonly ArgumentDeclaration $arguments;
+
+ private readonly ?SilentComment $comment;
+
+ private readonly FileSpan $span;
+
+ /**
+ * @param Statement[] $children
+ */
+ public function __construct(string $originalName, ArgumentDeclaration $arguments, FileSpan $span, array $children, ?SilentComment $comment = null)
+ {
+ $this->originalName = $originalName;
+ $this->name = str_replace('_', '-', $originalName);
+ $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;
+ }
+
+ /**
+ * The callable's original name, without underscores converted to hyphens.
+ */
+ public function getOriginalName(): string
+ {
+ return $this->originalName;
+ }
+
+ final public function getArguments(): ArgumentDeclaration
+ {
+ return $this->arguments;
+ }
+
+ final public function getComment(): ?SilentComment
+ {
+ return $this->comment;
+ }
+
+ final public function getSpan(): FileSpan
+ {
+ return $this->span;
+ }
+}
diff --git a/vendor/scssphp/scssphp/src/Ast/Sass/Statement/ContentBlock.php b/vendor/scssphp/scssphp/src/Ast/Sass/Statement/ContentBlock.php
new file mode 100644
index 000000000..d5be3c8eb
--- /dev/null
+++ b/vendor/scssphp/scssphp/src/Ast/Sass/Statement/ContentBlock.php
@@ -0,0 +1,46 @@
+<?php
+
+/**
+ * SCSSPHP
+ *
+ * @copyright 2012-2020 Leaf Corcoran
+ *
+ * @license http://opensource.org/licenses/MIT MIT
+ *
+ * @link http://scssphp.github.io/scssphp
+ */
+
+namespace ScssPhp\ScssPhp\Ast\Sass\Statement;
+
+use ScssPhp\ScssPhp\Ast\Sass\ArgumentDeclaration;
+use ScssPhp\ScssPhp\Ast\Sass\Statement;
+use ScssPhp\ScssPhp\Visitor\StatementVisitor;
+use SourceSpan\FileSpan;
+
+/**
+ * An anonymous block of code that's invoked for a {@see ContentRule}.
+ *
+ * @internal
+ */
+final class ContentBlock extends CallableDeclaration
+{
+ /**
+ * @param Statement[] $children
+ */
+ public function __construct(ArgumentDeclaration $arguments, array $children, FileSpan $span)
+ {
+ parent::__construct('@content', $arguments, $span, $children);
+ }
+
+ public function accept(StatementVisitor $visitor)
+ {
+ return $visitor->visitContentBlock($this);
+ }
+
+ public function __toString(): string
+ {
+ $buffer = $this->getArguments()->isEmpty() ? '' : ' using (' . $this->getArguments() . ')';
+
+ return $buffer . '{' . implode(' ', $this->getChildren()) . '}';
+ }
+}
diff --git a/vendor/scssphp/scssphp/src/Ast/Sass/Statement/ContentRule.php b/vendor/scssphp/scssphp/src/Ast/Sass/Statement/ContentRule.php
new file mode 100644
index 000000000..46c46e000
--- /dev/null
+++ b/vendor/scssphp/scssphp/src/Ast/Sass/Statement/ContentRule.php
@@ -0,0 +1,64 @@
+<?php
+
+/**
+ * SCSSPHP
+ *
+ * @copyright 2012-2020 Leaf Corcoran
+ *
+ * @license http://opensource.org/licenses/MIT MIT
+ *
+ * @link http://scssphp.github.io/scssphp
+ */
+
+namespace ScssPhp\ScssPhp\Ast\Sass\Statement;
+
+use ScssPhp\ScssPhp\Ast\Sass\ArgumentInvocation;
+use ScssPhp\ScssPhp\Ast\Sass\Statement;
+use ScssPhp\ScssPhp\Visitor\StatementVisitor;
+use SourceSpan\FileSpan;
+
+/**
+ * A `@content` rule.
+ *
+ * This is used in a mixin to include statement-level content passed by the
+ * caller.
+ *
+ * @internal
+ */
+final class ContentRule implements Statement
+{
+ /**
+ * The arguments pass to this `@content` rule.
+ *
+ * This will be an empty invocation if `@content` has no arguments.
+ */
+ private readonly ArgumentInvocation $arguments;
+
+ private readonly FileSpan $span;
+
+ public function __construct(ArgumentInvocation $arguments, FileSpan $span)
+ {
+ $this->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/vendor/scssphp/scssphp/src/Ast/Sass/Statement/DebugRule.php b/vendor/scssphp/scssphp/src/Ast/Sass/Statement/DebugRule.php
new file mode 100644
index 000000000..eb1069e30
--- /dev/null
+++ b/vendor/scssphp/scssphp/src/Ast/Sass/Statement/DebugRule.php
@@ -0,0 +1,58 @@
+<?php
+
+/**
+ * SCSSPHP
+ *
+ * @copyright 2012-2020 Leaf Corcoran
+ *
+ * @license http://opensource.org/licenses/MIT MIT
+ *
+ * @link http://scssphp.github.io/scssphp
+ */
+
+namespace ScssPhp\ScssPhp\Ast\Sass\Statement;
+
+use ScssPhp\ScssPhp\Ast\Sass\Expression;
+use ScssPhp\ScssPhp\Ast\Sass\Statement;
+use ScssPhp\ScssPhp\Visitor\StatementVisitor;
+use SourceSpan\FileSpan;
+
+/**
+ * A `@debug` rule.
+ *
+ * This prints a Sass value for debugging purposes.
+ *
+ * @internal
+ */
+final class DebugRule implements Statement
+{
+ private readonly Expression $expression;
+
+ private readonly FileSpan $span;
+
+ public function __construct(Expression $expression, FileSpan $span)
+ {
+ $this->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/vendor/scssphp/scssphp/src/Ast/Sass/Statement/Declaration.php b/vendor/scssphp/scssphp/src/Ast/Sass/Statement/Declaration.php
new file mode 100644
index 000000000..c4b4544ca
--- /dev/null
+++ b/vendor/scssphp/scssphp/src/Ast/Sass/Statement/Declaration.php
@@ -0,0 +1,120 @@
+<?php
+
+/**
+ * SCSSPHP
+ *
+ * @copyright 2012-2020 Leaf Corcoran
+ *
+ * @license http://opensource.org/licenses/MIT MIT
+ *
+ * @link http://scssphp.github.io/scssphp
+ */
+
+namespace ScssPhp\ScssPhp\Ast\Sass\Statement;
+
+use ScssPhp\ScssPhp\Ast\Sass\Expression;
+use ScssPhp\ScssPhp\Ast\Sass\Expression\StringExpression;
+use ScssPhp\ScssPhp\Ast\Sass\Interpolation;
+use ScssPhp\ScssPhp\Ast\Sass\Statement;
+use ScssPhp\ScssPhp\Visitor\StatementVisitor;
+use SourceSpan\FileSpan;
+
+/**
+ * A declaration (that is, a `name: value` pair).
+ *
+ * @extends ParentStatement<Statement[]|null>
+ *
+ * @internal
+ */
+final class Declaration extends ParentStatement
+{
+ private readonly Interpolation $name;
+
+ /**
+ * The value of this declaration.
+ *
+ * If {@see getChildren} is `null`, this is never `null`. Otherwise, it may or may
+ * not be `null`.
+ */
+ private readonly ?Expression $value;
+
+ private readonly FileSpan $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
+ {
+ return new self($name, $value, $span);
+ }
+
+ /**
+ * @param Statement[] $children
+ */
+ public static function nested(Interpolation $name, array $children, FileSpan $span, ?Expression $value = null): self
+ {
+ return new self($name, $value, $span, $children);
+ }
+
+ 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 str_starts_with($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/vendor/scssphp/scssphp/src/Ast/Sass/Statement/EachRule.php b/vendor/scssphp/scssphp/src/Ast/Sass/Statement/EachRule.php
new file mode 100644
index 000000000..b25193505
--- /dev/null
+++ b/vendor/scssphp/scssphp/src/Ast/Sass/Statement/EachRule.php
@@ -0,0 +1,79 @@
+<?php
+
+/**
+ * SCSSPHP
+ *
+ * @copyright 2012-2020 Leaf Corcoran
+ *
+ * @license http://opensource.org/licenses/MIT MIT
+ *
+ * @link http://scssphp.github.io/scssphp
+ */
+
+namespace ScssPhp\ScssPhp\Ast\Sass\Statement;
+
+use ScssPhp\ScssPhp\Ast\Sass\Expression;
+use ScssPhp\ScssPhp\Ast\Sass\Statement;
+use ScssPhp\ScssPhp\Visitor\StatementVisitor;
+use SourceSpan\FileSpan;
+
+/**
+ * An `@each` rule.
+ *
+ * This iterates over values in a list or map.
+ *
+ * @extends ParentStatement<Statement[]>
+ *
+ * @internal
+ */
+final class EachRule extends ParentStatement
+{
+ /**
+ * @var list<string>
+ */
+ private readonly array $variables;
+
+ private readonly Expression $list;
+
+ private readonly FileSpan $span;
+
+ /**
+ * @param list<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 list<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(fn($variable) => '$' . $variable, $this->variables)) . ' in ' . $this->list . ' {' . implode(' ', $this->getChildren()) . '}';
+ }
+}
diff --git a/vendor/scssphp/scssphp/src/Ast/Sass/Statement/ElseClause.php b/vendor/scssphp/scssphp/src/Ast/Sass/Statement/ElseClause.php
new file mode 100644
index 000000000..8ced96ede
--- /dev/null
+++ b/vendor/scssphp/scssphp/src/Ast/Sass/Statement/ElseClause.php
@@ -0,0 +1,26 @@
+<?php
+
+/**
+ * SCSSPHP
+ *
+ * @copyright 2012-2020 Leaf Corcoran
+ *
+ * @license http://opensource.org/licenses/MIT MIT
+ *
+ * @link http://scssphp.github.io/scssphp
+ */
+
+namespace ScssPhp\ScssPhp\Ast\Sass\Statement;
+
+/**
+ * An `@else` clause in an `@if` rule.
+ *
+ * @internal
+ */
+final class ElseClause extends IfRuleClause
+{
+ public function __toString(): string
+ {
+ return '@else {' . implode(' ', $this->getChildren()) . '}';
+ }
+}
diff --git a/vendor/scssphp/scssphp/src/Ast/Sass/Statement/ErrorRule.php b/vendor/scssphp/scssphp/src/Ast/Sass/Statement/ErrorRule.php
new file mode 100644
index 000000000..1a9203a71
--- /dev/null
+++ b/vendor/scssphp/scssphp/src/Ast/Sass/Statement/ErrorRule.php
@@ -0,0 +1,58 @@
+<?php
+
+/**
+ * SCSSPHP
+ *
+ * @copyright 2012-2020 Leaf Corcoran
+ *
+ * @license http://opensource.org/licenses/MIT MIT
+ *
+ * @link http://scssphp.github.io/scssphp
+ */
+
+namespace ScssPhp\ScssPhp\Ast\Sass\Statement;
+
+use ScssPhp\ScssPhp\Ast\Sass\Expression;
+use ScssPhp\ScssPhp\Ast\Sass\Statement;
+use ScssPhp\ScssPhp\Visitor\StatementVisitor;
+use SourceSpan\FileSpan;
+
+/**
+ * A `@error` rule.
+ *
+ * This emits an error and stops execution.
+ *
+ * @internal
+ */
+final class ErrorRule implements Statement
+{
+ private readonly Expression $expression;
+
+ private readonly FileSpan $span;
+
+ public function __construct(Expression $expression, FileSpan $span)
+ {
+ $this->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/vendor/scssphp/scssphp/src/Ast/Sass/Statement/ExtendRule.php b/vendor/scssphp/scssphp/src/Ast/Sass/Statement/ExtendRule.php
new file mode 100644
index 000000000..4f2e874f8
--- /dev/null
+++ b/vendor/scssphp/scssphp/src/Ast/Sass/Statement/ExtendRule.php
@@ -0,0 +1,72 @@
+<?php
+
+/**
+ * SCSSPHP
+ *
+ * @copyright 2012-2020 Leaf Corcoran
+ *
+ * @license http://opensource.org/licenses/MIT MIT
+ *
+ * @link http://scssphp.github.io/scssphp
+ */
+
+namespace ScssPhp\ScssPhp\Ast\Sass\Statement;
+
+use ScssPhp\ScssPhp\Ast\Sass\Interpolation;
+use ScssPhp\ScssPhp\Ast\Sass\Statement;
+use ScssPhp\ScssPhp\Visitor\StatementVisitor;
+use SourceSpan\FileSpan;
+
+/**
+ * An `@extend` rule.
+ *
+ * This gives one selector all the styling of another.
+ *
+ * @internal
+ */
+final class ExtendRule implements Statement
+{
+ private readonly Interpolation $selector;
+
+ private readonly FileSpan $span;
+
+ private readonly bool $optional;
+
+ public function __construct(Interpolation $selector, FileSpan $span, bool $optional = false)
+ {
+ $this->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/vendor/scssphp/scssphp/src/Ast/Sass/Statement/ForRule.php b/vendor/scssphp/scssphp/src/Ast/Sass/Statement/ForRule.php
new file mode 100644
index 000000000..3c4526e04
--- /dev/null
+++ b/vendor/scssphp/scssphp/src/Ast/Sass/Statement/ForRule.php
@@ -0,0 +1,91 @@
+<?php
+
+/**
+ * SCSSPHP
+ *
+ * @copyright 2012-2020 Leaf Corcoran
+ *
+ * @license http://opensource.org/licenses/MIT MIT
+ *
+ * @link http://scssphp.github.io/scssphp
+ */
+
+namespace ScssPhp\ScssPhp\Ast\Sass\Statement;
+
+use ScssPhp\ScssPhp\Ast\Sass\Expression;
+use ScssPhp\ScssPhp\Ast\Sass\Statement;
+use ScssPhp\ScssPhp\Visitor\StatementVisitor;
+use SourceSpan\FileSpan;
+
+/**
+ * A `@for` rule.
+ *
+ * This iterates a set number of times.
+ *
+ * @extends ParentStatement<Statement[]>
+ *
+ * @internal
+ */
+final class ForRule extends ParentStatement
+{
+ private readonly string $variable;
+
+ private readonly Expression $from;
+
+ private readonly Expression $to;
+
+ private readonly bool $exclusive;
+
+ private readonly FileSpan $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/vendor/scssphp/scssphp/src/Ast/Sass/Statement/FunctionRule.php b/vendor/scssphp/scssphp/src/Ast/Sass/Statement/FunctionRule.php
new file mode 100644
index 000000000..e451300a4
--- /dev/null
+++ b/vendor/scssphp/scssphp/src/Ast/Sass/Statement/FunctionRule.php
@@ -0,0 +1,43 @@
+<?php
+
+/**
+ * SCSSPHP
+ *
+ * @copyright 2012-2020 Leaf Corcoran
+ *
+ * @license http://opensource.org/licenses/MIT MIT
+ *
+ * @link http://scssphp.github.io/scssphp
+ */
+
+namespace ScssPhp\ScssPhp\Ast\Sass\Statement;
+
+use ScssPhp\ScssPhp\Ast\Sass\SassDeclaration;
+use ScssPhp\ScssPhp\Util\SpanUtil;
+use ScssPhp\ScssPhp\Visitor\StatementVisitor;
+use SourceSpan\FileSpan;
+
+/**
+ * A function declaration.
+ *
+ * This declares a function that's invoked using normal CSS function syntax.
+ *
+ * @internal
+ */
+final class FunctionRule extends CallableDeclaration implements SassDeclaration
+{
+ public function getNameSpan(): FileSpan
+ {
+ return SpanUtil::initialIdentifier(SpanUtil::withoutInitialAtRule($this->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/vendor/scssphp/scssphp/src/Ast/Sass/Statement/HasContentVisitor.php b/vendor/scssphp/scssphp/src/Ast/Sass/Statement/HasContentVisitor.php
new file mode 100644
index 000000000..0537265f3
--- /dev/null
+++ b/vendor/scssphp/scssphp/src/Ast/Sass/Statement/HasContentVisitor.php
@@ -0,0 +1,31 @@
+<?php
+
+/**
+ * SCSSPHP
+ *
+ * @copyright 2012-2020 Leaf Corcoran
+ *
+ * @license http://opensource.org/licenses/MIT MIT
+ *
+ * @link http://scssphp.github.io/scssphp
+ */
+
+namespace ScssPhp\ScssPhp\Ast\Sass\Statement;
+
+use ScssPhp\ScssPhp\Visitor\StatementSearchVisitor;
+
+/**
+ * A visitor for determining whether a {@see MixinRule} recursively contains a
+ * {@see ContentRule}.
+ *
+ * @internal
+ *
+ * @extends StatementSearchVisitor<bool>
+ */
+final class HasContentVisitor extends StatementSearchVisitor
+{
+ public function visitContentRule(ContentRule $node): bool
+ {
+ return true;
+ }
+}
diff --git a/vendor/scssphp/scssphp/src/Ast/Sass/Statement/IfClause.php b/vendor/scssphp/scssphp/src/Ast/Sass/Statement/IfClause.php
new file mode 100644
index 000000000..12ae5467c
--- /dev/null
+++ b/vendor/scssphp/scssphp/src/Ast/Sass/Statement/IfClause.php
@@ -0,0 +1,40 @@
+<?php
+
+/**
+ * SCSSPHP
+ *
+ * @copyright 2012-2020 Leaf Corcoran
+ *
+ * @license http://opensource.org/licenses/MIT MIT
+ *
+ * @link http://scssphp.github.io/scssphp
+ */
+
+namespace ScssPhp\ScssPhp\Ast\Sass\Statement;
+
+use ScssPhp\ScssPhp\Ast\Sass\Expression;
+use ScssPhp\ScssPhp\Ast\Sass\Statement;
+
+/**
+ * An `@if` or `@else if` clause in an `@if` rule.
+ *
+ * @internal
+ */
+final class IfClause extends IfRuleClause
+{
+ private readonly Expression $expression;
+
+ /**
+ * @param Statement[] $children
+ */
+ public function __construct(Expression $expression, array $children)
+ {
+ $this->expression = $expression;
+ parent::__construct($children);
+ }
+
+ public function getExpression(): Expression
+ {
+ return $this->expression;
+ }
+}
diff --git a/vendor/scssphp/scssphp/src/Ast/Sass/Statement/IfRule.php b/vendor/scssphp/scssphp/src/Ast/Sass/Statement/IfRule.php
new file mode 100644
index 000000000..ce000a80c
--- /dev/null
+++ b/vendor/scssphp/scssphp/src/Ast/Sass/Statement/IfRule.php
@@ -0,0 +1,95 @@
+<?php
+
+/**
+ * SCSSPHP
+ *
+ * @copyright 2012-2020 Leaf Corcoran
+ *
+ * @license http://opensource.org/licenses/MIT MIT
+ *
+ * @link http://scssphp.github.io/scssphp
+ */
+
+namespace ScssPhp\ScssPhp\Ast\Sass\Statement;
+
+use ScssPhp\ScssPhp\Ast\Sass\Statement;
+use ScssPhp\ScssPhp\Visitor\StatementVisitor;
+use SourceSpan\FileSpan;
+
+/**
+ * An `@if` rule.
+ *
+ * This conditionally executes a block of code.
+ *
+ * @internal
+ */
+final class IfRule implements Statement
+{
+ /**
+ * @var list<IfClause>
+ */
+ private readonly array $clauses;
+
+ private readonly ?ElseClause $lastClause;
+
+ private readonly FileSpan $span;
+
+ /**
+ * @param list<IfClause> $clauses
+ */
+ public function __construct(array $clauses, FileSpan $span, ?ElseClause $lastClause = null)
+ {
+ $this->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 list<IfClause>
+ */
+ public function getClauses(): array
+ {
+ return $this->clauses;
+ }
+
+ /**
+ * The final, unconditional `@else` clause.
+ *
+ * This is `null` if there is no unconditional `@else`.
+ */
+ 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/vendor/scssphp/scssphp/src/Ast/Sass/Statement/IfRuleClause.php b/vendor/scssphp/scssphp/src/Ast/Sass/Statement/IfRuleClause.php
new file mode 100644
index 000000000..66a38a27a
--- /dev/null
+++ b/vendor/scssphp/scssphp/src/Ast/Sass/Statement/IfRuleClause.php
@@ -0,0 +1,64 @@
+<?php
+
+/**
+ * SCSSPHP
+ *
+ * @copyright 2012-2020 Leaf Corcoran
+ *
+ * @license http://opensource.org/licenses/MIT MIT
+ *
+ * @link http://scssphp.github.io/scssphp
+ */
+
+namespace ScssPhp\ScssPhp\Ast\Sass\Statement;
+
+use ScssPhp\ScssPhp\Ast\Sass\Import\DynamicImport;
+use ScssPhp\ScssPhp\Ast\Sass\Statement;
+use ScssPhp\ScssPhp\Util\IterableUtil;
+
+/**
+ * The superclass of `@if` and `@else` clauses.
+ *
+ * @internal
+ */
+abstract class IfRuleClause
+{
+ /**
+ * @var Statement[]
+ */
+ private readonly array $children;
+
+ private readonly bool $declarations;
+
+ /**
+ * @param Statement[] $children
+ */
+ public function __construct(array $children)
+ {
+ $this->children = $children;
+ $this->declarations = IterableUtil::any($children, function (Statement $child) {
+ if ($child instanceof VariableDeclaration || $child instanceof FunctionRule || $child instanceof MixinRule) {
+ return true;
+ }
+
+ if ($child instanceof ImportRule) {
+ return IterableUtil::any($child->getImports(), fn ($import) => $import instanceof DynamicImport);
+ }
+
+ return false;
+ });
+ }
+
+ /**
+ * @return Statement[]
+ */
+ final public function getChildren(): array
+ {
+ return $this->children;
+ }
+
+ final public function hasDeclarations(): bool
+ {
+ return $this->declarations;
+ }
+}
diff --git a/vendor/scssphp/scssphp/src/Ast/Sass/Statement/ImportRule.php b/vendor/scssphp/scssphp/src/Ast/Sass/Statement/ImportRule.php
new file mode 100644
index 000000000..08af18e9b
--- /dev/null
+++ b/vendor/scssphp/scssphp/src/Ast/Sass/Statement/ImportRule.php
@@ -0,0 +1,65 @@
+<?php
+
+/**
+ * SCSSPHP
+ *
+ * @copyright 2012-2020 Leaf Corcoran
+ *
+ * @license http://opensource.org/licenses/MIT MIT
+ *
+ * @link http://scssphp.github.io/scssphp
+ */
+
+namespace ScssPhp\ScssPhp\Ast\Sass\Statement;
+
+use ScssPhp\ScssPhp\Ast\Sass\Import;
+use ScssPhp\ScssPhp\Ast\Sass\Statement;
+use ScssPhp\ScssPhp\Visitor\StatementVisitor;
+use SourceSpan\FileSpan;
+
+/**
+ * An `@import` rule.
+ *
+ * @internal
+ */
+final class ImportRule implements Statement
+{
+ /**
+ * @var list<Import>
+ */
+ private readonly array $imports;
+
+ private readonly FileSpan $span;
+
+ /**
+ * @param list<Import> $imports
+ */
+ public function __construct(array $imports, FileSpan $span)
+ {
+ $this->imports = $imports;
+ $this->span = $span;
+ }
+
+ /**
+ * @return list<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/vendor/scssphp/scssphp/src/Ast/Sass/Statement/IncludeRule.php b/vendor/scssphp/scssphp/src/Ast/Sass/Statement/IncludeRule.php
new file mode 100644
index 000000000..5e42d0d35
--- /dev/null
+++ b/vendor/scssphp/scssphp/src/Ast/Sass/Statement/IncludeRule.php
@@ -0,0 +1,141 @@
+<?php
+
+/**
+ * SCSSPHP
+ *
+ * @copyright 2012-2020 Leaf Corcoran
+ *
+ * @license http://opensource.org/licenses/MIT MIT
+ *
+ * @link http://scssphp.github.io/scssphp
+ */
+
+namespace ScssPhp\ScssPhp\Ast\Sass\Statement;
+
+use ScssPhp\ScssPhp\Ast\Sass\ArgumentInvocation;
+use ScssPhp\ScssPhp\Ast\Sass\CallableInvocation;
+use ScssPhp\ScssPhp\Ast\Sass\SassReference;
+use ScssPhp\ScssPhp\Ast\Sass\Statement;
+use ScssPhp\ScssPhp\Util\SpanUtil;
+use ScssPhp\ScssPhp\Visitor\StatementVisitor;
+use SourceSpan\FileSpan;
+
+/**
+ * A mixin invocation.
+ *
+ * @internal
+ */
+final class IncludeRule implements Statement, CallableInvocation, SassReference
+{
+ private readonly ?string $namespace;
+
+ private readonly string $name;
+
+ private readonly string $originalName;
+
+ private readonly ArgumentInvocation $arguments;
+
+ private readonly ?ContentBlock $content;
+
+ private readonly FileSpan $span;
+
+ public function __construct(string $originalName, ArgumentInvocation $arguments, FileSpan $span, ?string $namespace = null, ?ContentBlock $content = null)
+ {
+ $this->originalName = $originalName;
+ $this->name = str_replace('_', '-', $originalName);
+ $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;
+ }
+
+ /**
+ * The original name of the mixin being invoked, without underscores
+ * converted to hyphens.
+ */
+ public function getOriginalName(): string
+ {
+ return $this->originalName;
+ }
+
+ 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/vendor/scssphp/scssphp/src/Ast/Sass/Statement/LoudComment.php b/vendor/scssphp/scssphp/src/Ast/Sass/Statement/LoudComment.php
new file mode 100644
index 000000000..523f0e374
--- /dev/null
+++ b/vendor/scssphp/scssphp/src/Ast/Sass/Statement/LoudComment.php
@@ -0,0 +1,53 @@
+<?php
+
+/**
+ * SCSSPHP
+ *
+ * @copyright 2012-2020 Leaf Corcoran
+ *
+ * @license http://opensource.org/licenses/MIT MIT
+ *
+ * @link http://scssphp.github.io/scssphp
+ */
+
+namespace ScssPhp\ScssPhp\Ast\Sass\Statement;
+
+use ScssPhp\ScssPhp\Ast\Sass\Interpolation;
+use ScssPhp\ScssPhp\Ast\Sass\Statement;
+use ScssPhp\ScssPhp\Visitor\StatementVisitor;
+use SourceSpan\FileSpan;
+
+/**
+ * A loud CSS-style comment.
+ *
+ * @internal
+ */
+final class LoudComment implements Statement
+{
+ private readonly Interpolation $text;
+
+ public function __construct(Interpolation $text)
+ {
+ $this->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/vendor/scssphp/scssphp/src/Ast/Sass/Statement/MediaRule.php b/vendor/scssphp/scssphp/src/Ast/Sass/Statement/MediaRule.php
new file mode 100644
index 000000000..a1960d6b2
--- /dev/null
+++ b/vendor/scssphp/scssphp/src/Ast/Sass/Statement/MediaRule.php
@@ -0,0 +1,67 @@
+<?php
+
+/**
+ * SCSSPHP
+ *
+ * @copyright 2012-2020 Leaf Corcoran
+ *
+ * @license http://opensource.org/licenses/MIT MIT
+ *
+ * @link http://scssphp.github.io/scssphp
+ */
+
+namespace ScssPhp\ScssPhp\Ast\Sass\Statement;
+
+use ScssPhp\ScssPhp\Ast\Sass\Interpolation;
+use ScssPhp\ScssPhp\Ast\Sass\Statement;
+use ScssPhp\ScssPhp\Visitor\StatementVisitor;
+use SourceSpan\FileSpan;
+
+/**
+ * A `@media` rule.
+ *
+ * @extends ParentStatement<Statement[]>
+ *
+ * @internal
+ */
+final class MediaRule extends ParentStatement
+{
+ private readonly Interpolation $query;
+
+ private readonly FileSpan $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/vendor/scssphp/scssphp/src/Ast/Sass/Statement/MixinRule.php b/vendor/scssphp/scssphp/src/Ast/Sass/Statement/MixinRule.php
new file mode 100644
index 000000000..768faf517
--- /dev/null
+++ b/vendor/scssphp/scssphp/src/Ast/Sass/Statement/MixinRule.php
@@ -0,0 +1,79 @@
+<?php
+
+/**
+ * SCSSPHP
+ *
+ * @copyright 2012-2020 Leaf Corcoran
+ *
+ * @license http://opensource.org/licenses/MIT MIT
+ *
+ * @link http://scssphp.github.io/scssphp
+ */
+
+namespace ScssPhp\ScssPhp\Ast\Sass\Statement;
+
+use ScssPhp\ScssPhp\Ast\Sass\ArgumentDeclaration;
+use ScssPhp\ScssPhp\Ast\Sass\SassDeclaration;
+use ScssPhp\ScssPhp\Ast\Sass\Statement;
+use ScssPhp\ScssPhp\Util\SpanUtil;
+use ScssPhp\ScssPhp\Visitor\StatementVisitor;
+use SourceSpan\FileSpan;
+
+/**
+ * A mixin declaration.
+ *
+ * This declares a mixin that's invoked using `@include`.
+ *
+ * @internal
+ */
+final class MixinRule extends CallableDeclaration implements SassDeclaration
+{
+ /**
+ * Whether the mixin contains a `@content` rule.
+ */
+ private ?bool $content = null;
+
+ /**
+ * @param Statement[] $children
+ */
+ public function __construct(string $name, ArgumentDeclaration $arguments, FileSpan $span, array $children, ?SilentComment $comment = null)
+ {
+ parent::__construct($name, $arguments, $span, $children, $comment);
+ }
+
+ public function hasContent(): bool
+ {
+ if (!isset($this->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/vendor/scssphp/scssphp/src/Ast/Sass/Statement/ParentStatement.php b/vendor/scssphp/scssphp/src/Ast/Sass/Statement/ParentStatement.php
new file mode 100644
index 000000000..232debeef
--- /dev/null
+++ b/vendor/scssphp/scssphp/src/Ast/Sass/Statement/ParentStatement.php
@@ -0,0 +1,81 @@
+<?php
+
+/**
+ * SCSSPHP
+ *
+ * @copyright 2012-2020 Leaf Corcoran
+ *
+ * @license http://opensource.org/licenses/MIT MIT
+ *
+ * @link http://scssphp.github.io/scssphp
+ */
+
+namespace ScssPhp\ScssPhp\Ast\Sass\Statement;
+
+use ScssPhp\ScssPhp\Ast\Sass\Import\DynamicImport;
+use ScssPhp\ScssPhp\Ast\Sass\Statement;
+
+/**
+ * A {@see Statement} that can have child statements.
+ *
+ * This has a generic parameter so that its subclasses can choose whether or
+ * not their children lists are nullable.
+ *
+ * @template T
+ * @psalm-template T of (Statement[]|null)
+ *
+ * @internal
+ */
+abstract class ParentStatement implements Statement
+{
+ /**
+ * @var T
+ */
+ private readonly ?array $children;
+
+ private readonly bool $declarations;
+
+ /**
+ * @param T $children
+ */
+ public function __construct(?array $children)
+ {
+ $this->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(): ?array
+ {
+ return $this->children;
+ }
+
+ final public function hasDeclarations(): bool
+ {
+ return $this->declarations;
+ }
+}
diff --git a/vendor/scssphp/scssphp/src/Ast/Sass/Statement/ReturnRule.php b/vendor/scssphp/scssphp/src/Ast/Sass/Statement/ReturnRule.php
new file mode 100644
index 000000000..f9c304748
--- /dev/null
+++ b/vendor/scssphp/scssphp/src/Ast/Sass/Statement/ReturnRule.php
@@ -0,0 +1,58 @@
+<?php
+
+/**
+ * SCSSPHP
+ *
+ * @copyright 2012-2020 Leaf Corcoran
+ *
+ * @license http://opensource.org/licenses/MIT MIT
+ *
+ * @link http://scssphp.github.io/scssphp
+ */
+
+namespace ScssPhp\ScssPhp\Ast\Sass\Statement;
+
+use ScssPhp\ScssPhp\Ast\Sass\Expression;
+use ScssPhp\ScssPhp\Ast\Sass\Statement;
+use ScssPhp\ScssPhp\Visitor\StatementVisitor;
+use SourceSpan\FileSpan;
+
+/**
+ * A `@return` rule.
+ *
+ * This exits from the current function body with a return value.
+ *
+ * @internal
+ */
+final class ReturnRule implements Statement
+{
+ private readonly Expression $expression;
+
+ private readonly FileSpan $span;
+
+ public function __construct(Expression $expression, FileSpan $span)
+ {
+ $this->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/vendor/scssphp/scssphp/src/Ast/Sass/Statement/SilentComment.php b/vendor/scssphp/scssphp/src/Ast/Sass/Statement/SilentComment.php
new file mode 100644
index 000000000..021cf7d99
--- /dev/null
+++ b/vendor/scssphp/scssphp/src/Ast/Sass/Statement/SilentComment.php
@@ -0,0 +1,55 @@
+<?php
+
+/**
+ * SCSSPHP
+ *
+ * @copyright 2012-2020 Leaf Corcoran
+ *
+ * @license http://opensource.org/licenses/MIT MIT
+ *
+ * @link http://scssphp.github.io/scssphp
+ */
+
+namespace ScssPhp\ScssPhp\Ast\Sass\Statement;
+
+use ScssPhp\ScssPhp\Ast\Sass\Statement;
+use ScssPhp\ScssPhp\Visitor\StatementVisitor;
+use SourceSpan\FileSpan;
+
+/**
+ * A silent Sass-style comment.
+ *
+ * @internal
+ */
+final class SilentComment implements Statement
+{
+ private readonly string $text;
+
+ private readonly FileSpan $span;
+
+ public function __construct(string $text, FileSpan $span)
+ {
+ $this->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/vendor/scssphp/scssphp/src/Ast/Sass/Statement/StyleRule.php b/vendor/scssphp/scssphp/src/Ast/Sass/Statement/StyleRule.php
new file mode 100644
index 000000000..eeb1d5755
--- /dev/null
+++ b/vendor/scssphp/scssphp/src/Ast/Sass/Statement/StyleRule.php
@@ -0,0 +1,69 @@
+<?php
+
+/**
+ * SCSSPHP
+ *
+ * @copyright 2012-2020 Leaf Corcoran
+ *
+ * @license http://opensource.org/licenses/MIT MIT
+ *
+ * @link http://scssphp.github.io/scssphp
+ */
+
+namespace ScssPhp\ScssPhp\Ast\Sass\Statement;
+
+use ScssPhp\ScssPhp\Ast\Sass\Interpolation;
+use ScssPhp\ScssPhp\Ast\Sass\Statement;
+use ScssPhp\ScssPhp\Visitor\StatementVisitor;
+use SourceSpan\FileSpan;
+
+/**
+ * A style rule.
+ *
+ * This applies style declarations to elements that match a given selector.
+ *
+ * @extends ParentStatement<Statement[]>
+ *
+ * @internal
+ */
+final class StyleRule extends ParentStatement
+{
+ private readonly Interpolation $selector;
+
+ private readonly FileSpan $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/vendor/scssphp/scssphp/src/Ast/Sass/Statement/Stylesheet.php b/vendor/scssphp/scssphp/src/Ast/Sass/Statement/Stylesheet.php
new file mode 100644
index 000000000..74d8981fb
--- /dev/null
+++ b/vendor/scssphp/scssphp/src/Ast/Sass/Statement/Stylesheet.php
@@ -0,0 +1,106 @@
+<?php
+
+/**
+ * SCSSPHP
+ *
+ * @copyright 2012-2020 Leaf Corcoran
+ *
+ * @license http://opensource.org/licenses/MIT MIT
+ *
+ * @link http://scssphp.github.io/scssphp
+ */
+
+namespace ScssPhp\ScssPhp\Ast\Sass\Statement;
+
+use League\Uri\Contracts\UriInterface;
+use ScssPhp\ScssPhp\Ast\Sass\Statement;
+use ScssPhp\ScssPhp\Exception\SassFormatException;
+use ScssPhp\ScssPhp\Logger\LoggerInterface;
+use ScssPhp\ScssPhp\Parser\CssParser;
+use ScssPhp\ScssPhp\Parser\SassParser;
+use ScssPhp\ScssPhp\Parser\ScssParser;
+use ScssPhp\ScssPhp\Syntax;
+use ScssPhp\ScssPhp\Visitor\StatementVisitor;
+use SourceSpan\FileSpan;
+
+/**
+ * A Sass stylesheet.
+ *
+ * This is the root Sass node. It contains top-level statements.
+ *
+ * @extends ParentStatement<Statement[]>
+ *
+ * @internal
+ */
+final class Stylesheet extends ParentStatement
+{
+ private readonly bool $plainCss;
+
+ private readonly FileSpan $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);
+ }
+
+ /**
+ * @throws SassFormatException when parsing fails
+ */
+ public static function parse(string $contents, Syntax $syntax, ?LoggerInterface $logger = null, ?UriInterface $sourceUrl = null): self
+ {
+ return match ($syntax) {
+ Syntax::SASS => self::parseSass($contents, $logger, $sourceUrl),
+ Syntax::SCSS => self::parseScss($contents, $logger, $sourceUrl),
+ Syntax::CSS => self::parseCss($contents, $logger, $sourceUrl),
+ };
+ }
+
+ /**
+ * @throws SassFormatException when parsing fails
+ */
+ public static function parseSass(string $contents, ?LoggerInterface $logger = null, ?UriInterface $sourceUrl = null): self
+ {
+ return (new SassParser($contents, $logger, $sourceUrl))->parse();
+ }
+
+ /**
+ * @throws SassFormatException when parsing fails
+ */
+ public static function parseScss(string $contents, ?LoggerInterface $logger = null, ?UriInterface $sourceUrl = null): self
+ {
+ return (new ScssParser($contents, $logger, $sourceUrl))->parse();
+ }
+
+ /**
+ * @throws SassFormatException when parsing fails
+ */
+ public static function parseCss(string $contents, ?LoggerInterface $logger = null, ?UriInterface $sourceUrl = null): self
+ {
+ return (new CssParser($contents, $logger, $sourceUrl))->parse();
+ }
+
+ public function __toString(): string
+ {
+ return implode(' ', $this->getChildren());
+ }
+}
diff --git a/vendor/scssphp/scssphp/src/Ast/Sass/Statement/SupportsRule.php b/vendor/scssphp/scssphp/src/Ast/Sass/Statement/SupportsRule.php
new file mode 100644
index 000000000..5fc447a37
--- /dev/null
+++ b/vendor/scssphp/scssphp/src/Ast/Sass/Statement/SupportsRule.php
@@ -0,0 +1,62 @@
+<?php
+
+/**
+ * SCSSPHP
+ *
+ * @copyright 2012-2020 Leaf Corcoran
+ *
+ * @license http://opensource.org/licenses/MIT MIT
+ *
+ * @link http://scssphp.github.io/scssphp
+ */
+
+namespace ScssPhp\ScssPhp\Ast\Sass\Statement;
+
+use ScssPhp\ScssPhp\Ast\Sass\Statement;
+use ScssPhp\ScssPhp\Ast\Sass\SupportsCondition;
+use ScssPhp\ScssPhp\Visitor\StatementVisitor;
+use SourceSpan\FileSpan;
+
+/**
+ * A `@supports` rule.
+ *
+ * @extends ParentStatement<Statement[]>
+ *
+ * @internal
+ */
+final class SupportsRule extends ParentStatement
+{
+ private readonly SupportsCondition $condition;
+
+ private readonly FileSpan $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/vendor/scssphp/scssphp/src/Ast/Sass/Statement/VariableDeclaration.php b/vendor/scssphp/scssphp/src/Ast/Sass/Statement/VariableDeclaration.php
new file mode 100644
index 000000000..f414f0eb0
--- /dev/null
+++ b/vendor/scssphp/scssphp/src/Ast/Sass/Statement/VariableDeclaration.php
@@ -0,0 +1,146 @@
+<?php
+
+/**
+ * SCSSPHP
+ *
+ * @copyright 2012-2020 Leaf Corcoran
+ *
+ * @license http://opensource.org/licenses/MIT MIT
+ *
+ * @link http://scssphp.github.io/scssphp
+ */
+
+namespace ScssPhp\ScssPhp\Ast\Sass\Statement;
+
+use ScssPhp\ScssPhp\Ast\Sass\Expression;
+use ScssPhp\ScssPhp\Ast\Sass\SassDeclaration;
+use ScssPhp\ScssPhp\Ast\Sass\Statement;
+use ScssPhp\ScssPhp\Util;
+use ScssPhp\ScssPhp\Util\SpanUtil;
+use ScssPhp\ScssPhp\Visitor\StatementVisitor;
+use SourceSpan\FileSpan;
+
+/**
+ * A variable declaration.
+ *
+ * This defines or sets a variable.
+ *
+ * @internal
+ */
+final class VariableDeclaration implements Statement, SassDeclaration
+{
+ private readonly ?string $namespace;
+
+ private readonly string $name;
+
+ private readonly ?SilentComment $comment;
+
+ private readonly Expression $expression;
+
+ private readonly bool $guarded;
+
+ private readonly bool $global;
+
+ private readonly FileSpan $span;
+
+ public function __construct(string $name, Expression $expression, FileSpan $span, ?string $namespace = null, bool $guarded = false, bool $global = false, ?SilentComment $comment = null)
+ {
+ $this->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/vendor/scssphp/scssphp/src/Ast/Sass/Statement/WarnRule.php b/vendor/scssphp/scssphp/src/Ast/Sass/Statement/WarnRule.php
new file mode 100644
index 000000000..8acfbe77a
--- /dev/null
+++ b/vendor/scssphp/scssphp/src/Ast/Sass/Statement/WarnRule.php
@@ -0,0 +1,58 @@
+<?php
+
+/**
+ * SCSSPHP
+ *
+ * @copyright 2012-2020 Leaf Corcoran
+ *
+ * @license http://opensource.org/licenses/MIT MIT
+ *
+ * @link http://scssphp.github.io/scssphp
+ */
+
+namespace ScssPhp\ScssPhp\Ast\Sass\Statement;
+
+use ScssPhp\ScssPhp\Ast\Sass\Expression;
+use ScssPhp\ScssPhp\Ast\Sass\Statement;
+use ScssPhp\ScssPhp\Visitor\StatementVisitor;
+use SourceSpan\FileSpan;
+
+/**
+ * A `@warn` rule.
+ *
+ * This prints a Sass value—usually a string—to warn the user of something.
+ *
+ * @internal
+ */
+final class WarnRule implements Statement
+{
+ private readonly Expression $expression;
+
+ private readonly FileSpan $span;
+
+ public function __construct(Expression $expression, FileSpan $span)
+ {
+ $this->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/vendor/scssphp/scssphp/src/Ast/Sass/Statement/WhileRule.php b/vendor/scssphp/scssphp/src/Ast/Sass/Statement/WhileRule.php
new file mode 100644
index 000000000..facb8f5cd
--- /dev/null
+++ b/vendor/scssphp/scssphp/src/Ast/Sass/Statement/WhileRule.php
@@ -0,0 +1,65 @@
+<?php
+
+/**
+ * SCSSPHP
+ *
+ * @copyright 2012-2020 Leaf Corcoran
+ *
+ * @license http://opensource.org/licenses/MIT MIT
+ *
+ * @link http://scssphp.github.io/scssphp
+ */
+
+namespace ScssPhp\ScssPhp\Ast\Sass\Statement;
+
+use ScssPhp\ScssPhp\Ast\Sass\Expression;
+use ScssPhp\ScssPhp\Ast\Sass\Statement;
+use ScssPhp\ScssPhp\Visitor\StatementVisitor;
+use SourceSpan\FileSpan;
+
+/**
+ * A `@while` rule.
+ *
+ * This repeatedly executes a block of code as long as a statement evaluates to
+ * `true`.
+ *
+ * @extends ParentStatement<Statement[]>
+ *
+ * @internal
+ */
+final class WhileRule extends ParentStatement
+{
+ private readonly Expression $condition;
+
+ private readonly FileSpan $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/vendor/scssphp/scssphp/src/Ast/Sass/SupportsCondition.php b/vendor/scssphp/scssphp/src/Ast/Sass/SupportsCondition.php
new file mode 100644
index 000000000..b30c21035
--- /dev/null
+++ b/vendor/scssphp/scssphp/src/Ast/Sass/SupportsCondition.php
@@ -0,0 +1,22 @@
+<?php
+
+/**
+ * SCSSPHP
+ *
+ * @copyright 2012-2020 Leaf Corcoran
+ *
+ * @license http://opensource.org/licenses/MIT MIT
+ *
+ * @link http://scssphp.github.io/scssphp
+ */
+
+namespace ScssPhp\ScssPhp\Ast\Sass;
+
+/**
+ * An interface for defining the condition a `@supports` rule selects.
+ *
+ * @internal
+ */
+interface SupportsCondition extends SassNode
+{
+}
diff --git a/vendor/scssphp/scssphp/src/Ast/Sass/SupportsCondition/SupportsAnything.php b/vendor/scssphp/scssphp/src/Ast/Sass/SupportsCondition/SupportsAnything.php
new file mode 100644
index 000000000..6098a3d6f
--- /dev/null
+++ b/vendor/scssphp/scssphp/src/Ast/Sass/SupportsCondition/SupportsAnything.php
@@ -0,0 +1,54 @@
+<?php
+
+/**
+ * SCSSPHP
+ *
+ * @copyright 2012-2020 Leaf Corcoran
+ *
+ * @license http://opensource.org/licenses/MIT MIT
+ *
+ * @link http://scssphp.github.io/scssphp
+ */
+
+namespace ScssPhp\ScssPhp\Ast\Sass\SupportsCondition;
+
+use ScssPhp\ScssPhp\Ast\Sass\Interpolation;
+use ScssPhp\ScssPhp\Ast\Sass\SupportsCondition;
+use SourceSpan\FileSpan;
+
+/**
+ * A supports condition that represents the forwards-compatible
+ * `<general-enclosed>` production.
+ *
+ * @internal
+ */
+final class SupportsAnything implements SupportsCondition
+{
+ /**
+ * The contents of the condition.
+ */
+ private readonly Interpolation $contents;
+
+ private readonly FileSpan $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/vendor/scssphp/scssphp/src/Ast/Sass/SupportsCondition/SupportsDeclaration.php b/vendor/scssphp/scssphp/src/Ast/Sass/SupportsCondition/SupportsDeclaration.php
new file mode 100644
index 000000000..94e767378
--- /dev/null
+++ b/vendor/scssphp/scssphp/src/Ast/Sass/SupportsCondition/SupportsDeclaration.php
@@ -0,0 +1,80 @@
+<?php
+
+/**
+ * SCSSPHP
+ *
+ * @copyright 2012-2020 Leaf Corcoran
+ *
+ * @license http://opensource.org/licenses/MIT MIT
+ *
+ * @link http://scssphp.github.io/scssphp
+ */
+
+namespace ScssPhp\ScssPhp\Ast\Sass\SupportsCondition;
+
+use ScssPhp\ScssPhp\Ast\Sass\Expression;
+use ScssPhp\ScssPhp\Ast\Sass\Expression\StringExpression;
+use ScssPhp\ScssPhp\Ast\Sass\SupportsCondition;
+use SourceSpan\FileSpan;
+
+/**
+ * A condition that selects for browsers where a given declaration is
+ * supported.
+ *
+ * @internal
+ */
+final class SupportsDeclaration implements SupportsCondition
+{
+ /**
+ * The name of the declaration being tested.
+ */
+ private readonly Expression $name;
+
+ /**
+ * The value of the declaration being tested.
+ */
+ private readonly Expression $value;
+
+ private readonly FileSpan $span;
+
+ public function __construct(Expression $name, Expression $value, FileSpan $span)
+ {
+ $this->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() && str_starts_with($this->name->getText()->getInitialPlain(), '--');
+ }
+
+ public function __toString(): string
+ {
+ return "($this->name: $this->value)";
+ }
+}
diff --git a/vendor/scssphp/scssphp/src/Ast/Sass/SupportsCondition/SupportsFunction.php b/vendor/scssphp/scssphp/src/Ast/Sass/SupportsCondition/SupportsFunction.php
new file mode 100644
index 000000000..c610e68b9
--- /dev/null
+++ b/vendor/scssphp/scssphp/src/Ast/Sass/SupportsCondition/SupportsFunction.php
@@ -0,0 +1,64 @@
+<?php
+
+/**
+ * SCSSPHP
+ *
+ * @copyright 2012-2020 Leaf Corcoran
+ *
+ * @license http://opensource.org/licenses/MIT MIT
+ *
+ * @link http://scssphp.github.io/scssphp
+ */
+
+namespace ScssPhp\ScssPhp\Ast\Sass\SupportsCondition;
+
+use ScssPhp\ScssPhp\Ast\Sass\Interpolation;
+use ScssPhp\ScssPhp\Ast\Sass\SupportsCondition;
+use SourceSpan\FileSpan;
+
+/**
+ * A function-syntax condition.
+ *
+ * @internal
+ */
+final class SupportsFunction implements SupportsCondition
+{
+ /**
+ * The name of the function.
+ */
+ private readonly Interpolation $name;
+
+ /**
+ * The arguments of the function.
+ */
+ private readonly Interpolation $arguments;
+
+ private readonly FileSpan $span;
+
+ public function __construct(Interpolation $name, Interpolation $arguments, FileSpan $span)
+ {
+ $this->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/vendor/scssphp/scssphp/src/Ast/Sass/SupportsCondition/SupportsInterpolation.php b/vendor/scssphp/scssphp/src/Ast/Sass/SupportsCondition/SupportsInterpolation.php
new file mode 100644
index 000000000..8b6fa0b91
--- /dev/null
+++ b/vendor/scssphp/scssphp/src/Ast/Sass/SupportsCondition/SupportsInterpolation.php
@@ -0,0 +1,53 @@
+<?php
+
+/**
+ * SCSSPHP
+ *
+ * @copyright 2012-2020 Leaf Corcoran
+ *
+ * @license http://opensource.org/licenses/MIT MIT
+ *
+ * @link http://scssphp.github.io/scssphp
+ */
+
+namespace ScssPhp\ScssPhp\Ast\Sass\SupportsCondition;
+
+use ScssPhp\ScssPhp\Ast\Sass\Expression;
+use ScssPhp\ScssPhp\Ast\Sass\SupportsCondition;
+use SourceSpan\FileSpan;
+
+/**
+ * An interpolated condition.
+ *
+ * @internal
+ */
+final class SupportsInterpolation implements SupportsCondition
+{
+ /**
+ * The expression in the interpolation.
+ */
+ private readonly Expression $expression;
+
+ private readonly FileSpan $span;
+
+ public function __construct(Expression $expression, FileSpan $span)
+ {
+ $this->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/vendor/scssphp/scssphp/src/Ast/Sass/SupportsCondition/SupportsNegation.php b/vendor/scssphp/scssphp/src/Ast/Sass/SupportsCondition/SupportsNegation.php
new file mode 100644
index 000000000..7c5a78636
--- /dev/null
+++ b/vendor/scssphp/scssphp/src/Ast/Sass/SupportsCondition/SupportsNegation.php
@@ -0,0 +1,56 @@
+<?php
+
+/**
+ * SCSSPHP
+ *
+ * @copyright 2012-2020 Leaf Corcoran
+ *
+ * @license http://opensource.org/licenses/MIT MIT
+ *
+ * @link http://scssphp.github.io/scssphp
+ */
+
+namespace ScssPhp\ScssPhp\Ast\Sass\SupportsCondition;
+
+use ScssPhp\ScssPhp\Ast\Sass\SupportsCondition;
+use SourceSpan\FileSpan;
+
+/**
+ * A negated condition.
+ *
+ * @internal
+ */
+final class SupportsNegation implements SupportsCondition
+{
+ /**
+ * The condition that's been negated.
+ */
+ private readonly SupportsCondition $condition;
+
+ private readonly FileSpan $span;
+
+ public function __construct(SupportsCondition $condition, FileSpan $span)
+ {
+ $this->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/vendor/scssphp/scssphp/src/Ast/Sass/SupportsCondition/SupportsOperation.php b/vendor/scssphp/scssphp/src/Ast/Sass/SupportsCondition/SupportsOperation.php
new file mode 100644
index 000000000..0faa0b238
--- /dev/null
+++ b/vendor/scssphp/scssphp/src/Ast/Sass/SupportsCondition/SupportsOperation.php
@@ -0,0 +1,80 @@
+<?php
+
+/**
+ * SCSSPHP
+ *
+ * @copyright 2012-2020 Leaf Corcoran
+ *
+ * @license http://opensource.org/licenses/MIT MIT
+ *
+ * @link http://scssphp.github.io/scssphp
+ */
+
+namespace ScssPhp\ScssPhp\Ast\Sass\SupportsCondition;
+
+use ScssPhp\ScssPhp\Ast\Sass\SupportsCondition;
+use SourceSpan\FileSpan;
+
+/**
+ * An operation defining the relationship between two conditions.
+ *
+ * @internal
+ */
+final class SupportsOperation implements SupportsCondition
+{
+ /**
+ * The left-hand operand.
+ */
+ private readonly SupportsCondition $left;
+
+ /**
+ * The right-hand operand.
+ */
+ private readonly SupportsCondition $right;
+
+ private readonly string $operator;
+
+ private readonly FileSpan $span;
+
+ public function __construct(SupportsCondition $left, SupportsCondition $right, string $operator, FileSpan $span)
+ {
+ $this->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/vendor/scssphp/scssphp/src/Ast/Selector/AttributeOperator.php b/vendor/scssphp/scssphp/src/Ast/Selector/AttributeOperator.php
new file mode 100644
index 000000000..8bdffebd4
--- /dev/null
+++ b/vendor/scssphp/scssphp/src/Ast/Selector/AttributeOperator.php
@@ -0,0 +1,65 @@
+<?php
+
+/**
+ * SCSSPHP
+ *
+ * @copyright 2012-2020 Leaf Corcoran
+ *
+ * @license http://opensource.org/licenses/MIT MIT
+ *
+ * @link http://scssphp.github.io/scssphp
+ */
+
+namespace ScssPhp\ScssPhp\Ast\Selector;
+
+/**
+ * An operator that defines the semantics of an {@see AttributeSelector}.
+ *
+ * @internal
+ */
+enum AttributeOperator
+{
+ /**
+ * The attribute value exactly equals the given value.
+ */
+ case EQUAL;
+
+ /**
+ * The attribute value is a whitespace-separated list of words, one of which
+ * is the given value.
+ */
+ case INCLUDE;
+
+ /**
+ * The attribute value is either exactly the given value, or starts with the
+ * given value followed by a dash.
+ */
+ case DASH;
+
+ /**
+ * The attribute value begins with the given value.
+ */
+ case PREFIX;
+
+ /**
+ * The attribute value ends with the given value.
+ */
+ case SUFFIX;
+
+ /**
+ * The attribute value contains the given value.
+ */
+ case SUBSTRING;
+
+ public function getText(): string
+ {
+ return match ($this) {
+ self::EQUAL => '=',
+ self::INCLUDE => '~=',
+ self::DASH => '|=',
+ self::PREFIX => '^=',
+ self::SUFFIX => '$=',
+ self::SUBSTRING => '*=',
+ };
+ }
+}
diff --git a/vendor/scssphp/scssphp/src/Ast/Selector/AttributeSelector.php b/vendor/scssphp/scssphp/src/Ast/Selector/AttributeSelector.php
new file mode 100644
index 000000000..e3371fcc1
--- /dev/null
+++ b/vendor/scssphp/scssphp/src/Ast/Selector/AttributeSelector.php
@@ -0,0 +1,123 @@
+<?php
+
+/**
+ * SCSSPHP
+ *
+ * @copyright 2012-2020 Leaf Corcoran
+ *
+ * @license http://opensource.org/licenses/MIT MIT
+ *
+ * @link http://scssphp.github.io/scssphp
+ */
+
+namespace ScssPhp\ScssPhp\Ast\Selector;
+
+use ScssPhp\ScssPhp\Visitor\SelectorVisitor;
+use SourceSpan\FileSpan;
+
+/**
+ * An attribute selector.
+ *
+ * This selects for elements with the given attribute, and optionally with a
+ * value matching certain conditions as well.
+ *
+ * @internal
+ */
+final class AttributeSelector extends SimpleSelector
+{
+ /**
+ * The name of the attribute being selected for.
+ */
+ private readonly QualifiedName $name;
+
+ /**
+ * The operator that defines the semantics of {@see value}.
+ *
+ * If this is `null`, this matches any element with the given property,
+ * regardless of this value. It's `null` if and only if {@see value} is `null`.
+ */
+ private readonly ?AttributeOperator $op;
+
+ /**
+ * An assertion about the value of {@see name}.
+ *
+ * The precise semantics of this string are defined by {@see op}.
+ *
+ * If this is `null`, this matches any element with the given property,
+ * regardless of this value. It's `null` if and only if {@see op} is `null`.
+ */
+ private readonly ?string $value;
+
+ /**
+ * The modifier which indicates how the attribute selector should be
+ * processed.
+ *
+ * See for example [case-sensitivity][] modifiers.
+ *
+ * [case-sensitivity]: https://www.w3.org/TR/selectors-4/#attribute-case
+ *
+ * If {@see op} is `null`, this is always `null` as well.
+ */
+ private readonly ?string $modifier;
+
+ /**
+ * Creates an attribute selector that matches any element with a property of
+ * the given name.
+ */
+ public static function create(QualifiedName $name, FileSpan $span): AttributeSelector
+ {
+ return new AttributeSelector($name, $span, null, null, null);
+ }
+
+ /**
+ * Creates an attribute selector that matches an element with a property
+ * named $name, whose value matches $value based on the semantics of $op.
+ */
+ public static function withOperator(QualifiedName $name, ?AttributeOperator $op, ?string $value, FileSpan $span, ?string $modifier = null): AttributeSelector
+ {
+ return new AttributeSelector($name, $span, $op, $value, $modifier);
+ }
+
+ private function __construct(QualifiedName $name, FileSpan $span, ?AttributeOperator $op, ?string $value, ?string $modifier)
+ {
+ $this->name = $name;
+ $this->op = $op;
+ $this->value = $value;
+ $this->modifier = $modifier;
+ parent::__construct($span);
+ }
+
+ public function getName(): QualifiedName
+ {
+ return $this->name;
+ }
+
+ public function getOp(): ?AttributeOperator
+ {
+ 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/vendor/scssphp/scssphp/src/Ast/Selector/ClassSelector.php b/vendor/scssphp/scssphp/src/Ast/Selector/ClassSelector.php
new file mode 100644
index 000000000..47d547952
--- /dev/null
+++ b/vendor/scssphp/scssphp/src/Ast/Selector/ClassSelector.php
@@ -0,0 +1,58 @@
+<?php
+
+/**
+ * SCSSPHP
+ *
+ * @copyright 2012-2020 Leaf Corcoran
+ *
+ * @license http://opensource.org/licenses/MIT MIT
+ *
+ * @link http://scssphp.github.io/scssphp
+ */
+
+namespace ScssPhp\ScssPhp\Ast\Selector;
+
+use ScssPhp\ScssPhp\Visitor\SelectorVisitor;
+use SourceSpan\FileSpan;
+
+/**
+ * A class selector.
+ *
+ * This selects elements whose `class` attribute contains an identifier with
+ * the given name.
+ *
+ * @internal
+ */
+final class ClassSelector extends SimpleSelector
+{
+ /**
+ * The class name this selects for.
+ */
+ private readonly string $name;
+
+ public function __construct(string $name, FileSpan $span)
+ {
+ $this->name = $name;
+ parent::__construct($span);
+ }
+
+ 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, $this->getSpan());
+ }
+}
diff --git a/vendor/scssphp/scssphp/src/Ast/Selector/Combinator.php b/vendor/scssphp/scssphp/src/Ast/Selector/Combinator.php
new file mode 100644
index 000000000..5de38ae80
--- /dev/null
+++ b/vendor/scssphp/scssphp/src/Ast/Selector/Combinator.php
@@ -0,0 +1,49 @@
+<?php
+
+/**
+ * SCSSPHP
+ *
+ * @copyright 2012-2020 Leaf Corcoran
+ *
+ * @license http://opensource.org/licenses/MIT MIT
+ *
+ * @link http://scssphp.github.io/scssphp
+ */
+
+namespace ScssPhp\ScssPhp\Ast\Selector;
+
+/**
+ * A combinator that defines the relationship between selectors in a
+ * {@see ComplexSelector}.
+ *
+ * @internal
+ */
+enum Combinator
+{
+ /**
+ * Matches the right-hand selector if it's immediately adjacent to the
+ * left-hand selector in the DOM tree.
+ */
+ case NEXT_SIBLING;
+
+ /**
+ * Matches the right-hand selector if it's a direct child of the left-hand
+ * selector in the DOM tree.
+ */
+ case CHILD;
+
+ /**
+ * Matches the right-hand selector if it comes after the left-hand selector
+ * in the DOM tree.
+ */
+ case FOLLOWING_SIBLING;
+
+ public function getText(): string
+ {
+ return match ($this) {
+ self::NEXT_SIBLING => '+',
+ self::CHILD => '>',
+ self::FOLLOWING_SIBLING => '~',
+ };
+ }
+}
diff --git a/vendor/scssphp/scssphp/src/Ast/Selector/ComplexSelector.php b/vendor/scssphp/scssphp/src/Ast/Selector/ComplexSelector.php
new file mode 100644
index 000000000..2ccaa6508
--- /dev/null
+++ b/vendor/scssphp/scssphp/src/Ast/Selector/ComplexSelector.php
@@ -0,0 +1,271 @@
+<?php
+
+/**
+ * SCSSPHP
+ *
+ * @copyright 2012-2020 Leaf Corcoran
+ *
+ * @license http://opensource.org/licenses/MIT MIT
+ *
+ * @link http://scssphp.github.io/scssphp
+ */
+
+namespace ScssPhp\ScssPhp\Ast\Selector;
+
+use League\Uri\Contracts\UriInterface;
+use ScssPhp\ScssPhp\Ast\Css\CssValue;
+use ScssPhp\ScssPhp\Exception\SassFormatException;
+use ScssPhp\ScssPhp\Extend\ExtendUtil;
+use ScssPhp\ScssPhp\Logger\LoggerInterface;
+use ScssPhp\ScssPhp\Parser\SelectorParser;
+use ScssPhp\ScssPhp\Util\EquatableUtil;
+use ScssPhp\ScssPhp\Util\ListUtil;
+use ScssPhp\ScssPhp\Visitor\SelectorVisitor;
+use SourceSpan\FileSpan;
+
+/**
+ * A complex selector.
+ *
+ * A complex selector is composed of {@see CompoundSelector}s separated by
+ * {@see Combinator}s. It selects elements based on their parent selectors.
+ *
+ * @internal
+ */
+final class ComplexSelector extends Selector
+{
+ /**
+ * This selector's leading combinators.
+ *
+ * If this is empty, that indicates that it has no leading combinator. If
+ * it's more than one element, that means it's invalid CSS; however, we still
+ * support this for backwards-compatibility purposes.
+ *
+ * @var list<CssValue<Combinator>>
+ */
+ private readonly array $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<ComplexSelectorComponent>
+ */
+ private readonly array $components;
+
+ /**
+ * Whether a line break should be emitted *before* this selector.
+ */
+ private readonly bool $lineBreak;
+
+ private ?int $specificity = null;
+
+ /**
+ * @param list<CssValue<Combinator>> $leadingCombinators
+ * @param list<ComplexSelectorComponent> $components
+ */
+ public function __construct(array $leadingCombinators, array $components, FileSpan $span, 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;
+ parent::__construct($span);
+ }
+
+ /**
+ * 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, ?UriInterface $url = null, bool $allowParent = true): ComplexSelector
+ {
+ return (new SelectorParser($contents, $logger, $url, $allowParent))->parseComplexSelector();
+ }
+
+ /**
+ * @return list<CssValue<Combinator>>
+ */
+ public function getLeadingCombinators(): array
+ {
+ return $this->leadingCombinators;
+ }
+
+ /**
+ * @return list<ComplexSelectorComponent>
+ */
+ 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 && EquatableUtil::listEquals($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<CssValue<Combinator>> $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->getSpan(), $this->lineBreak || $forceLineBreak);
+ }
+
+ return new ComplexSelector(
+ $this->leadingCombinators,
+ array_merge(
+ ListUtil::exceptLast($this->components),
+ [ListUtil::last($this->components)->withAdditionalCombinators($combinators)]
+ ),
+ $this->getSpan(),
+ $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.
+ */
+ public function withAdditionalComponent(ComplexSelectorComponent $component, FileSpan $span, bool $forceLineBreak = false): ComplexSelector
+ {
+ return new ComplexSelector($this->leadingCombinators, array_merge($this->components, [$component]), $span, $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.
+ */
+ public function concatenate(ComplexSelector $child, FileSpan $span, bool $forceLineBreak = false): ComplexSelector
+ {
+ if (\count($child->leadingCombinators) === 0) {
+ return new ComplexSelector(
+ $this->leadingCombinators,
+ array_merge($this->components, $child->components),
+ $span,
+ $this->lineBreak || $child->lineBreak || $forceLineBreak
+ );
+ }
+
+ if (\count($this->components) === 0) {
+ return new ComplexSelector(
+ array_merge($this->leadingCombinators, $child->leadingCombinators),
+ $child->components,
+ $span,
+ $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
+ ),
+ $span,
+ $this->lineBreak || $child->lineBreak || $forceLineBreak
+ );
+ }
+}
diff --git a/vendor/scssphp/scssphp/src/Ast/Selector/ComplexSelectorComponent.php b/vendor/scssphp/scssphp/src/Ast/Selector/ComplexSelectorComponent.php
new file mode 100644
index 000000000..dd9ed0566
--- /dev/null
+++ b/vendor/scssphp/scssphp/src/Ast/Selector/ComplexSelectorComponent.php
@@ -0,0 +1,99 @@
+<?php
+
+/**
+ * SCSSPHP
+ *
+ * @copyright 2012-2020 Leaf Corcoran
+ *
+ * @license http://opensource.org/licenses/MIT MIT
+ *
+ * @link http://scssphp.github.io/scssphp
+ */
+
+namespace ScssPhp\ScssPhp\Ast\Selector;
+
+use ScssPhp\ScssPhp\Ast\Css\CssValue;
+use ScssPhp\ScssPhp\Util\Equatable;
+use ScssPhp\ScssPhp\Util\EquatableUtil;
+use SourceSpan\FileSpan;
+
+/**
+ * A component of a {@see ComplexSelector}.
+ *
+ * This a {@see CompoundSelector} with one or more trailing {@see Combinator}s.
+ *
+ * @internal
+ */
+final class ComplexSelectorComponent implements Equatable
+{
+ /**
+ * This component's compound selector.
+ */
+ private readonly CompoundSelector $selector;
+
+ /**
+ * This selector's combinators.
+ *
+ * If this is empty, that indicates that it has an implicit descendent
+ * combinator. If it's more than one element, that means it's invalid CSS;
+ * however, we still support this for backwards-compatibility purposes.
+ *
+ * @var list<CssValue<Combinator>>
+ */
+ private readonly array $combinators;
+
+ private readonly FileSpan $span;
+
+ /**
+ * @param list<CssValue<Combinator>> $combinators
+ */
+ public function __construct(CompoundSelector $selector, array $combinators, FileSpan $span)
+ {
+ $this->selector = $selector;
+ $this->combinators = $combinators;
+ $this->span = $span;
+ }
+
+ public function getSelector(): CompoundSelector
+ {
+ return $this->selector;
+ }
+
+ public function getSpan(): FileSpan
+ {
+ return $this->span;
+ }
+
+ /**
+ * @return list<CssValue<Combinator>>
+ */
+ public function getCombinators(): array
+ {
+ return $this->combinators;
+ }
+
+ public function equals(object $other): bool
+ {
+ return $other instanceof ComplexSelectorComponent && $this->selector->equals($other->selector) && EquatableUtil::listEquals($this->combinators, $other->combinators);
+ }
+
+ /**
+ * Returns a copy of $this with $combinators added to the end of
+ * `$this->combinators`.
+ *
+ * @param list<CssValue<Combinator>> $combinators
+ */
+ public function withAdditionalCombinators(array $combinators): ComplexSelectorComponent
+ {
+ if ($combinators === []) {
+ return $this;
+ }
+
+ return new ComplexSelectorComponent($this->selector, array_merge($this->combinators, $combinators), $this->span);
+ }
+
+ public function __toString(): string
+ {
+ return $this->selector . implode('', array_map(fn ($combinator) => ' ' . $combinator, $this->combinators));
+ }
+}
diff --git a/vendor/scssphp/scssphp/src/Ast/Selector/CompoundSelector.php b/vendor/scssphp/scssphp/src/Ast/Selector/CompoundSelector.php
new file mode 100644
index 000000000..ec7fc8847
--- /dev/null
+++ b/vendor/scssphp/scssphp/src/Ast/Selector/CompoundSelector.php
@@ -0,0 +1,156 @@
+<?php
+
+/**
+ * SCSSPHP
+ *
+ * @copyright 2012-2020 Leaf Corcoran
+ *
+ * @license http://opensource.org/licenses/MIT MIT
+ *
+ * @link http://scssphp.github.io/scssphp
+ */
+
+namespace ScssPhp\ScssPhp\Ast\Selector;
+
+use League\Uri\Contracts\UriInterface;
+use ScssPhp\ScssPhp\Exception\SassFormatException;
+use ScssPhp\ScssPhp\Extend\ExtendUtil;
+use ScssPhp\ScssPhp\Logger\LoggerInterface;
+use ScssPhp\ScssPhp\Parser\SelectorParser;
+use ScssPhp\ScssPhp\Util\EquatableUtil;
+use ScssPhp\ScssPhp\Util\IterableUtil;
+use ScssPhp\ScssPhp\Visitor\SelectorVisitor;
+use SourceSpan\FileSpan;
+
+/**
+ * A compound selector.
+ *
+ * A compound selector is composed of {@see SimpleSelector}s. It matches an element
+ * that matches all of the component simple selectors.
+ *
+ * @internal
+ */
+final class CompoundSelector extends Selector
+{
+ /**
+ * The components of this selector.
+ *
+ * This is never empty.
+ *
+ * @var list<SimpleSelector>
+ */
+ private readonly array $components;
+
+ private ?int $specificity = null;
+
+ private ?bool $complicatedSuperselectorSemantics = null;
+
+ /**
+ * 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, ?UriInterface $url = null, bool $allowParent = true): CompoundSelector
+ {
+ return (new SelectorParser($contents, $logger, $url, $allowParent))->parseCompoundSelector();
+ }
+
+ /**
+ * @param list<SimpleSelector> $components
+ */
+ public function __construct(array $components, FileSpan $span)
+ {
+ if ($components === []) {
+ throw new \InvalidArgumentException('components may not be empty.');
+ }
+
+ $this->components = $components;
+ parent::__construct($span);
+ }
+
+ /**
+ * @return list<SimpleSelector>
+ */
+ 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;
+ }
+
+ /**
+ * Whether any simple selector in this contains a selector that requires
+ * complex non-local reasoning to determine whether it's a super- or
+ * sub-selector.
+ *
+ * This includes both pseudo-elements and pseudo-selectors that take
+ * selectors as arguments.
+ *
+ * @internal
+ */
+ public function hasComplicatedSuperselectorSemantics(): bool
+ {
+ return $this->complicatedSuperselectorSemantics ??= IterableUtil::any($this->components, fn (SimpleSelector $component) => $component->hasComplicatedSuperselectorSemantics());
+ }
+
+ 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/vendor/scssphp/scssphp/src/Ast/Selector/IDSelector.php b/vendor/scssphp/scssphp/src/Ast/Selector/IDSelector.php
new file mode 100644
index 000000000..528fd46a1
--- /dev/null
+++ b/vendor/scssphp/scssphp/src/Ast/Selector/IDSelector.php
@@ -0,0 +1,74 @@
+<?php
+
+/**
+ * SCSSPHP
+ *
+ * @copyright 2012-2020 Leaf Corcoran
+ *
+ * @license http://opensource.org/licenses/MIT MIT
+ *
+ * @link http://scssphp.github.io/scssphp
+ */
+
+namespace ScssPhp\ScssPhp\Ast\Selector;
+
+use ScssPhp\ScssPhp\Visitor\SelectorVisitor;
+use SourceSpan\FileSpan;
+
+/**
+ * An ID selector.
+ *
+ * This selects elements whose `id` attribute exactly matches the given name.
+ *
+ * @internal
+ */
+final class IDSelector extends SimpleSelector
+{
+ /**
+ * The ID name this selects for.
+ */
+ private readonly string $name;
+
+ public function __construct(string $name, FileSpan $span)
+ {
+ $this->name = $name;
+ parent::__construct($span);
+ }
+
+ 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, $this->getSpan());
+ }
+
+ 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/vendor/scssphp/scssphp/src/Ast/Selector/IsBogusVisitor.php b/vendor/scssphp/scssphp/src/Ast/Selector/IsBogusVisitor.php
new file mode 100644
index 000000000..28c84bace
--- /dev/null
+++ b/vendor/scssphp/scssphp/src/Ast/Selector/IsBogusVisitor.php
@@ -0,0 +1,64 @@
+<?php
+
+/**
+ * SCSSPHP
+ *
+ * @copyright 2012-2020 Leaf Corcoran
+ *
+ * @license http://opensource.org/licenses/MIT MIT
+ *
+ * @link http://scssphp.github.io/scssphp
+ */
+
+namespace ScssPhp\ScssPhp\Ast\Selector;
+
+use ScssPhp\ScssPhp\Visitor\AnySelectorVisitor;
+
+/**
+ * The visitor used to implement {@see Selector::isBogus}.
+ *
+ * @internal
+ */
+final class IsBogusVisitor extends AnySelectorVisitor
+{
+ /**
+ * Whether to consider selectors with leading combinators as bogus.
+ */
+ private readonly bool $includeLeadingCombinator;
+
+ public function __construct(bool $includeLeadingCombinator)
+ {
+ $this->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/vendor/scssphp/scssphp/src/Ast/Selector/IsInvisibleVisitor.php b/vendor/scssphp/scssphp/src/Ast/Selector/IsInvisibleVisitor.php
new file mode 100644
index 000000000..027d6cb90
--- /dev/null
+++ b/vendor/scssphp/scssphp/src/Ast/Selector/IsInvisibleVisitor.php
@@ -0,0 +1,69 @@
+<?php
+
+/**
+ * SCSSPHP
+ *
+ * @copyright 2012-2020 Leaf Corcoran
+ *
+ * @license http://opensource.org/licenses/MIT MIT
+ *
+ * @link http://scssphp.github.io/scssphp
+ */
+
+namespace ScssPhp\ScssPhp\Ast\Selector;
+
+use ScssPhp\ScssPhp\Visitor\AnySelectorVisitor;
+
+/**
+ * The visitor used to implement {@see Selector::isInvisible}.
+ *
+ * @internal
+ */
+final class IsInvisibleVisitor extends AnySelectorVisitor
+{
+ /**
+ * Whether to consider selectors with bogus combinators invisible.
+ */
+ private readonly bool $includeBogus;
+
+ public function __construct(bool $includeBogus)
+ {
+ $this->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/vendor/scssphp/scssphp/src/Ast/Selector/IsUselessVisitor.php b/vendor/scssphp/scssphp/src/Ast/Selector/IsUselessVisitor.php
new file mode 100644
index 000000000..ff8a6da12
--- /dev/null
+++ b/vendor/scssphp/scssphp/src/Ast/Selector/IsUselessVisitor.php
@@ -0,0 +1,43 @@
+<?php
+
+/**
+ * SCSSPHP
+ *
+ * @copyright 2012-2020 Leaf Corcoran
+ *
+ * @license http://opensource.org/licenses/MIT MIT
+ *
+ * @link http://scssphp.github.io/scssphp
+ */
+
+namespace ScssPhp\ScssPhp\Ast\Selector;
+
+use ScssPhp\ScssPhp\Visitor\AnySelectorVisitor;
+
+/**
+ * The visitor used to implement {@see Selector::isUseless}.
+ *
+ * @internal
+ */
+final class IsUselessVisitor extends AnySelectorVisitor
+{
+ public function visitComplexSelector(ComplexSelector $complex): bool
+ {
+ if (\count($complex->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/vendor/scssphp/scssphp/src/Ast/Selector/ParentSelector.php b/vendor/scssphp/scssphp/src/Ast/Selector/ParentSelector.php
new file mode 100644
index 000000000..6f03e4f26
--- /dev/null
+++ b/vendor/scssphp/scssphp/src/Ast/Selector/ParentSelector.php
@@ -0,0 +1,62 @@
+<?php
+
+/**
+ * SCSSPHP
+ *
+ * @copyright 2012-2020 Leaf Corcoran
+ *
+ * @license http://opensource.org/licenses/MIT MIT
+ *
+ * @link http://scssphp.github.io/scssphp
+ */
+
+namespace ScssPhp\ScssPhp\Ast\Selector;
+
+use ScssPhp\ScssPhp\Visitor\SelectorVisitor;
+use SourceSpan\FileSpan;
+
+/**
+ * A selector that matches the parent in the Sass stylesheet.
+ *
+ * This is not a plain CSS selector—it should be removed before emitting a CSS
+ * document.
+ *
+ * @internal
+ */
+final class ParentSelector extends SimpleSelector
+{
+ /**
+ * The suffix that will be added to the parent selector after it's been
+ * resolved.
+ *
+ * This is assumed to be a valid identifier suffix. It may be `null`,
+ * indicating that the parent selector will not be modified.
+ */
+ private readonly ?string $suffix;
+
+ public function __construct(FileSpan $span, ?string $suffix = null)
+ {
+ $this->suffix = $suffix;
+ parent::__construct($span);
+ }
+
+ 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/vendor/scssphp/scssphp/src/Ast/Selector/ParentSelectorVisitor.php b/vendor/scssphp/scssphp/src/Ast/Selector/ParentSelectorVisitor.php
new file mode 100644
index 000000000..293d3aefc
--- /dev/null
+++ b/vendor/scssphp/scssphp/src/Ast/Selector/ParentSelectorVisitor.php
@@ -0,0 +1,20 @@
+<?php
+
+namespace ScssPhp\ScssPhp\Ast\Selector;
+
+use ScssPhp\ScssPhp\Visitor\SelectorSearchVisitor;
+
+/**
+ * A visitor for finding the first {@see ParentSelector} in a given selector.
+ *
+ * @template-extends SelectorSearchVisitor<ParentSelector>
+ *
+ * @internal
+ */
+final class ParentSelectorVisitor extends SelectorSearchVisitor
+{
+ public function visitParentSelector(ParentSelector $selector): ParentSelector
+ {
+ return $selector;
+ }
+}
diff --git a/vendor/scssphp/scssphp/src/Ast/Selector/PlaceholderSelector.php b/vendor/scssphp/scssphp/src/Ast/Selector/PlaceholderSelector.php
new file mode 100644
index 000000000..75030cea4
--- /dev/null
+++ b/vendor/scssphp/scssphp/src/Ast/Selector/PlaceholderSelector.php
@@ -0,0 +1,69 @@
+<?php
+
+/**
+ * SCSSPHP
+ *
+ * @copyright 2012-2020 Leaf Corcoran
+ *
+ * @license http://opensource.org/licenses/MIT MIT
+ *
+ * @link http://scssphp.github.io/scssphp
+ */
+
+namespace ScssPhp\ScssPhp\Ast\Selector;
+
+use ScssPhp\ScssPhp\Util\Character;
+use ScssPhp\ScssPhp\Visitor\SelectorVisitor;
+use SourceSpan\FileSpan;
+
+/**
+ * A placeholder selector.
+ *
+ * This doesn't match any elements. It's intended to be extended using
+ * `@extend`. It's not a plain CSS selector—it should be removed before
+ * emitting a CSS document.
+ *
+ * @internal
+ */
+final class PlaceholderSelector extends SimpleSelector
+{
+ /**
+ * The name of the placeholder.
+ */
+ private readonly string $name;
+
+ public function __construct(string $name, FileSpan $span)
+ {
+ $this->name = $name;
+ parent::__construct($span);
+ }
+
+ 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, $this->getSpan());
+ }
+
+ public function equals(object $other): bool
+ {
+ return $other instanceof PlaceholderSelector && $other->name === $this->name;
+ }
+}
diff --git a/vendor/scssphp/scssphp/src/Ast/Selector/PseudoSelector.php b/vendor/scssphp/scssphp/src/Ast/Selector/PseudoSelector.php
new file mode 100644
index 000000000..249b2c4a3
--- /dev/null
+++ b/vendor/scssphp/scssphp/src/Ast/Selector/PseudoSelector.php
@@ -0,0 +1,347 @@
+<?php
+
+/**
+ * SCSSPHP
+ *
+ * @copyright 2012-2020 Leaf Corcoran
+ *
+ * @license http://opensource.org/licenses/MIT MIT
+ *
+ * @link http://scssphp.github.io/scssphp
+ */
+
+namespace ScssPhp\ScssPhp\Ast\Selector;
+
+use ScssPhp\ScssPhp\Util;
+use ScssPhp\ScssPhp\Util\EquatableUtil;
+use ScssPhp\ScssPhp\Visitor\SelectorVisitor;
+use SourceSpan\FileSpan;
+
+/**
+ * A pseudo-class or pseudo-element selector.
+ *
+ * The semantics of a specific pseudo selector depends on its name. Some
+ * selectors take arguments, including other selectors. Sass manually encodes
+ * logic for each pseudo selector that takes a selector as an argument, to
+ * ensure that extension and other selector operations work properly.
+ *
+ * @internal
+ */
+final class PseudoSelector extends SimpleSelector
+{
+ /**
+ * The name of this selector.
+ */
+ private readonly string $name;
+
+ /**
+ * Like {@see name}, but without any vendor prefixes.
+ */
+ private readonly string $normalizedName;
+
+ private readonly bool $isClass;
+
+ private readonly bool $isSyntacticClass;
+
+ /**
+ * The non-selector argument passed to this selector.
+ *
+ * This is `null` if there's no argument. If {@see argument} and {@see selector} are
+ * both non-`null`, the selector follows the argument.
+ */
+ private readonly ?string $argument;
+
+ /**
+ * The selector argument passed to this selector.
+ *
+ * This is `null` if there's no selector. If {@see argument} and {@see selector} are
+ * both non-`null`, the selector follows the argument.
+ */
+ private readonly ?SelectorList $selector;
+
+ private ?int $specificity = null;
+
+ public function __construct(string $name, FileSpan $span, bool $element = false, ?string $argument = null, ?SelectorList $selector = null)
+ {
+ $this->name = $name;
+ $this->isClass = !$element && !self::isFakePseudoElement($name);
+ $this->isSyntacticClass = !$element;
+ $this->argument = $argument;
+ $this->selector = $selector;
+ $this->normalizedName = Util::unvendor($name);
+ parent::__construct($span);
+ }
+
+ /**
+ * 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;
+ }
+
+ /**
+ * @internal
+ */
+ public function hasComplicatedSuperselectorSemantics(): bool
+ {
+ return $this->isElement() || $this->selector !== null;
+ }
+
+ 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->getSpan(), $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->getSpan(), $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::iterableContains($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], $this->getSpan()))->isSuperselector(new CompoundSelector([$other], $this->getSpan()));
+ }
+
+ 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/vendor/scssphp/scssphp/src/Ast/Selector/QualifiedName.php b/vendor/scssphp/scssphp/src/Ast/Selector/QualifiedName.php
new file mode 100644
index 000000000..ac30123e2
--- /dev/null
+++ b/vendor/scssphp/scssphp/src/Ast/Selector/QualifiedName.php
@@ -0,0 +1,65 @@
+<?php
+
+/**
+ * SCSSPHP
+ *
+ * @copyright 2012-2020 Leaf Corcoran
+ *
+ * @license http://opensource.org/licenses/MIT MIT
+ *
+ * @link http://scssphp.github.io/scssphp
+ */
+
+namespace ScssPhp\ScssPhp\Ast\Selector;
+
+use ScssPhp\ScssPhp\Util\Equatable;
+
+/**
+ * A [qualified name][].
+ *
+ * [qualified name]: https://www.w3.org/TR/css3-namespace/#css-qnames
+ *
+ * @internal
+ */
+final class QualifiedName implements Equatable
+{
+ /**
+ * The identifier name.
+ */
+ private readonly string $name;
+
+ /**
+ * The namespace name.
+ *
+ * If this is `null`, {@see name} belongs to the default namespace. If it's the
+ * empty string, {@see name} belongs to no namespace. If it's `*`, {@see name} belongs
+ * to any namespace. Otherwise, {@see name} belongs to the given namespace.
+ */
+ private readonly ?string $namespace;
+
+ public function __construct(string $name, ?string $namespace = null)
+ {
+ $this->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/vendor/scssphp/scssphp/src/Ast/Selector/Selector.php b/vendor/scssphp/scssphp/src/Ast/Selector/Selector.php
new file mode 100644
index 000000000..138766d30
--- /dev/null
+++ b/vendor/scssphp/scssphp/src/Ast/Selector/Selector.php
@@ -0,0 +1,127 @@
+<?php
+
+/**
+ * SCSSPHP
+ *
+ * @copyright 2012-2020 Leaf Corcoran
+ *
+ * @license http://opensource.org/licenses/MIT MIT
+ *
+ * @link http://scssphp.github.io/scssphp
+ */
+
+namespace ScssPhp\ScssPhp\Ast\Selector;
+
+use ScssPhp\ScssPhp\Ast\AstNode;
+use ScssPhp\ScssPhp\Deprecation;
+use ScssPhp\ScssPhp\Exception\SassException;
+use ScssPhp\ScssPhp\Serializer\Serializer;
+use ScssPhp\ScssPhp\Util\Equatable;
+use ScssPhp\ScssPhp\Visitor\SelectorVisitor;
+use ScssPhp\ScssPhp\Warn;
+use SourceSpan\FileSpan;
+
+/**
+ * A node in the abstract syntax tree for a selector.
+ *
+ * This selector tree is mostly plain CSS, but also may contain a
+ * {@see ParentSelector} or a {@see PlaceholderSelector}.
+ *
+ * Selectors have structural equality semantics.
+ *
+ * @internal
+ */
+abstract class Selector implements AstNode, Equatable
+{
+ private readonly FileSpan $span;
+
+ public function __construct(FileSpan $span)
+ {
+ $this->span = $span;
+ }
+
+ public function getSpan(): FileSpan
+ {
+ return $this->span;
+ }
+
+ /**
+ * Whether this selector, and complex selectors containing it, should not be
+ * emitted.
+ */
+ public function isInvisible(): bool
+ {
+ return $this->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 SassException} in a future major version.
+ */
+ public function assertNotBogus(?string $name = null): void
+ {
+ if (!$this->isBogus()) {
+ return;
+ }
+
+ Warn::forDeprecation(($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", Deprecation::bogusCombinators);
+ }
+
+ /**
+ * Calls the appropriate visit method on $visitor.
+ *
+ * @template T
+ *
+ * @param SelectorVisitor<T> $visitor
+ *
+ * @return T
+ *
+ * @internal
+ */
+ abstract public function accept(SelectorVisitor $visitor);
+
+ final public function __toString(): string
+ {
+ return Serializer::serializeSelector($this, true);
+ }
+}
diff --git a/vendor/scssphp/scssphp/src/Ast/Selector/SelectorList.php b/vendor/scssphp/scssphp/src/Ast/Selector/SelectorList.php
new file mode 100644
index 000000000..f08fd3e39
--- /dev/null
+++ b/vendor/scssphp/scssphp/src/Ast/Selector/SelectorList.php
@@ -0,0 +1,355 @@
+<?php
+
+/**
+ * SCSSPHP
+ *
+ * @copyright 2012-2020 Leaf Corcoran
+ *
+ * @license http://opensource.org/licenses/MIT MIT
+ *
+ * @link http://scssphp.github.io/scssphp
+ */
+
+namespace ScssPhp\ScssPhp\Ast\Selector;
+
+use League\Uri\Contracts\UriInterface;
+use ScssPhp\ScssPhp\Ast\Css\CssValue;
+use ScssPhp\ScssPhp\Exception\SassFormatException;
+use ScssPhp\ScssPhp\Exception\SassScriptException;
+use ScssPhp\ScssPhp\Exception\SimpleSassException;
+use ScssPhp\ScssPhp\Extend\ExtendUtil;
+use ScssPhp\ScssPhp\Logger\LoggerInterface;
+use ScssPhp\ScssPhp\Parser\InterpolationMap;
+use ScssPhp\ScssPhp\Parser\SelectorParser;
+use ScssPhp\ScssPhp\Util\EquatableUtil;
+use ScssPhp\ScssPhp\Util\ListUtil;
+use ScssPhp\ScssPhp\Value\ListSeparator;
+use ScssPhp\ScssPhp\Value\SassList;
+use ScssPhp\ScssPhp\Value\SassString;
+use ScssPhp\ScssPhp\Visitor\SelectorVisitor;
+use SourceSpan\FileSpan;
+
+/**
+ * A selector list.
+ *
+ * A selector list is composed of {@see ComplexSelector}s. It matches any element
+ * that matches any of the component selectors.
+ *
+ * @internal
+ */
+final class SelectorList extends Selector
+{
+ /**
+ * The components of this selector.
+ *
+ * This is never empty.
+ *
+ * @var non-empty-list<ComplexSelector>
+ */
+ private readonly array $components;
+
+ /**
+ * Parses a selector list from $contents.
+ *
+ * If passed, $url is the name of the file from which $contents comes. If
+ * $allowParent is false, this doesn't allow {@see ParentSelector}s. If
+ * $plainCss is true, this parses the selector as plain CSS rather than
+ * unresolved Sass.
+ *
+ * If passed, $interpolationMap maps the text of $contents back to the
+ * original location of the selector in the source file.
+ *
+ * @throws SassFormatException if parsing fails.
+ */
+ public static function parse(string $contents, ?LoggerInterface $logger = null, ?InterpolationMap $interpolationMap = null, ?UriInterface $url = null, bool $allowParent = true, bool $plainCss = false): SelectorList
+ {
+ return (new SelectorParser($contents, $logger, $url, $allowParent, $interpolationMap, $plainCss))->parse();
+ }
+
+ /**
+ * @param list<ComplexSelector> $components
+ */
+ public function __construct(array $components, FileSpan $span)
+ {
+ if ($components === []) {
+ throw new \InvalidArgumentException('components may not be empty.');
+ }
+
+ $this->components = $components;
+ parent::__construct($span);
+ }
+
+ /**
+ * @return non-empty-list<ComplexSelector>
+ */
+ 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], $complex1->getSpan());
+
+ if ($unified === null) {
+ continue;
+ }
+
+ foreach ($unified as $complex) {
+ $contents[] = $complex;
+ }
+ }
+ }
+
+ return \count($contents) === 0 ? null : new SelectorList($contents, $this->getSpan());
+ }
+
+ /**
+ * Returns a new selector list that represents $this nested within $parent.
+ *
+ * By default, this replaces {@see ParentSelector}s in $this with $parent. If
+ * $preserveParentSelectors is true, this instead preserves those selectors
+ * as parent selectors.
+ *
+ * If $implicitParent is true, this prepends $parent to any
+ * {@see ComplexSelector}s in this that don't contain explicit {@see ParentSelector}s,
+ * or to _all_ {@see ComplexSelector}s if $preserveParentSelectors is true.
+ *
+ * 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 or if $preserveParentSelectors is true. Otherwise, this
+ * throws a {@see SassScriptException}.
+ */
+ public function nestWithin(?SelectorList $parent, bool $implicitParent = true, bool $preserveParentSelectors = false): SelectorList
+ {
+ if ($parent === null) {
+ if ($preserveParentSelectors) {
+ return $this;
+ }
+
+ $parentSelector = $this->accept(new ParentSelectorVisitor());
+ if ($parentSelector === null) {
+ return $this;
+ }
+
+ throw new SimpleSassException('Top-level selectors may not contain the parent selector "&".', $parentSelector->getSpan());
+ }
+
+ return new SelectorList(ListUtil::flattenVertically(array_map(function (ComplexSelector $complex) use ($parent, $implicitParent, $preserveParentSelectors) {
+ if ($preserveParentSelectors || !self::containsParentSelector($complex)) {
+ if (!$implicitParent) {
+ return [$complex];
+ }
+
+ return array_map(fn(ComplexSelector $parentComplex) => $parentComplex->concatenate($complex, $complex->getSpan()), $parent->getComponents());
+ }
+
+ /** @var list<ComplexSelector> $newComplexes */
+ $newComplexes = [];
+
+ foreach ($complex->getComponents() as $component) {
+ $resolved = self::nestWithinCompound($component, $parent);
+ if ($resolved === null) {
+ if (\count($newComplexes) === 0) {
+ $newComplexes[] = new ComplexSelector($complex->getLeadingCombinators(), [$component], $complex->getSpan(), false);
+ } else {
+ $newComplexes = array_map(fn ($newComplex) => $newComplex->withAdditionalComponent($component, $complex->getSpan()), $newComplexes);
+ }
+ } elseif (\count($newComplexes) === 0) {
+ if (\count($complex->getLeadingCombinators()) === 0) {
+ $newComplexes = $resolved;
+ } else {
+ $newComplexes = array_map(fn (ComplexSelector $resolvedComplex) => new ComplexSelector(
+ array_merge($complex->getLeadingCombinators(), $resolvedComplex->getLeadingCombinators()),
+ $resolvedComplex->getComponents(),
+ $complex->getSpan(),
+ $resolvedComplex->getLineBreak()
+ ), $resolved);
+ }
+ } else {
+ $previousComplexes = $newComplexes;
+ $newComplexes = [];
+
+ foreach ($previousComplexes as $newComplex) {
+ foreach ($resolved as $resolvedComplex) {
+ $newComplexes[] = $newComplex->concatenate($resolvedComplex, $newComplex->getSpan());
+ }
+ }
+ }
+ }
+
+ return $newComplexes;
+ }, $this->components)), $this->getSpan());
+ }
+
+ /**
+ * 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);
+ }
+
+ /**
+ * 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<ComplexSelector>|null
+ */
+ private static function nestWithinCompound(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 && self::containsParentSelector($selector)) {
+ $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 (!self::containsParentSelector($selector)) {
+ return $simple;
+ }
+
+ return $simple->withSelector($selector->nestWithin($parent, false));
+ }, $simples);
+ } else {
+ $resolvedSimples = $simples;
+ }
+
+ $parentSelector = $simples[0];
+
+ if (!$parentSelector instanceof ParentSelector) {
+ return [
+ new ComplexSelector([], [
+ new ComplexSelectorComponent(
+ new CompoundSelector($resolvedSimples, $component->getSelector()->getSpan()),
+ $component->getCombinators(),
+ $component->getSpan()
+ ),
+ ], $component->getSpan()),
+ ];
+ }
+
+ 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 SimpleSassException("Parent \"$complex\" is incompatible with this selector.", $parentSelector->getSpan());
+ }
+
+ $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)
+ ), $component->getSelector()->getSpan());
+ } else {
+ $last = new CompoundSelector(array_merge($lastSimples, array_slice($resolvedSimples, 1)), $component->getSelector()->getSpan());
+ }
+
+ $components = ListUtil::exceptLast($complex->getComponents());
+ $components[] = new ComplexSelectorComponent($last, $component->getCombinators(), $component->getSpan());
+
+ return new ComplexSelector($complex->getLeadingCombinators(), $components, $component->getSpan(), $complex->getLineBreak());
+ }, $parent->getComponents());
+ }
+
+ /**
+ * Returns a copy of `this` with $combinators added to the end of each
+ * complex selector in {@see components}].
+ *
+ * @param list<CssValue<Combinator>> $combinators
+ */
+ public function withAdditionalCombinators(array $combinators): SelectorList
+ {
+ if ($combinators === []) {
+ return $this;
+ }
+
+ return new SelectorList(array_map(fn(ComplexSelector $complex) => $complex->withAdditionalCombinators($combinators), $this->components), $this->getSpan());
+ }
+
+ /**
+ * Returns whether $selector recursively contains a parent selector.
+ */
+ private static function containsParentSelector(Selector $selector): bool
+ {
+ return $selector->accept(new ParentSelectorVisitor()) !== null;
+ }
+}
diff --git a/vendor/scssphp/scssphp/src/Ast/Selector/SimpleSelector.php b/vendor/scssphp/scssphp/src/Ast/Selector/SimpleSelector.php
new file mode 100644
index 000000000..e329593ea
--- /dev/null
+++ b/vendor/scssphp/scssphp/src/Ast/Selector/SimpleSelector.php
@@ -0,0 +1,179 @@
+<?php
+
+/**
+ * SCSSPHP
+ *
+ * @copyright 2012-2020 Leaf Corcoran
+ *
+ * @license http://opensource.org/licenses/MIT MIT
+ *
+ * @link http://scssphp.github.io/scssphp
+ */
+
+namespace ScssPhp\ScssPhp\Ast\Selector;
+
+use League\Uri\Contracts\UriInterface;
+use ScssPhp\ScssPhp\Exception\MultiSpanSassException;
+use ScssPhp\ScssPhp\Exception\SassException;
+use ScssPhp\ScssPhp\Exception\SassFormatException;
+use ScssPhp\ScssPhp\Logger\LoggerInterface;
+use ScssPhp\ScssPhp\Parser\SelectorParser;
+use ScssPhp\ScssPhp\Util\EquatableUtil;
+use ScssPhp\ScssPhp\Util\ListUtil;
+
+/**
+ * An abstract superclass for simple selectors.
+ *
+ * @internal
+ */
+abstract class SimpleSelector extends Selector
+{
+ /**
+ * Names of pseudo-classes that take selectors as arguments, and that are
+ * subselectors of their arguments.
+ *
+ * For example, `.foo` is a superselector of `:matches(.foo)`.
+ */
+ private const SUBSELECTOR_PSEUDOS = [
+ 'is',
+ 'matches',
+ 'where',
+ 'any',
+ 'nth-child',
+ 'nth-last-child',
+ ];
+
+ /**
+ * Parses a simple 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, ?UriInterface $url = null, bool $allowParent = true): SimpleSelector
+ {
+ return (new SelectorParser($contents, $logger, $url, $allowParent))->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;
+ }
+
+ /**
+ * Whether this requires complex non-local reasoning to determine whether
+ * it's a super- or sub-selector.
+ *
+ * This includes both pseudo-elements and pseudo-selectors that take
+ * selectors as arguments.
+ *
+ * @internal
+ */
+ public function hasComplicatedSuperselectorSemantics(): bool
+ {
+ return false;
+ }
+
+ /**
+ * 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 an exception.
+ *
+ * @throws SassException
+ */
+ public function addSuffix(string $suffix): SimpleSelector
+ {
+ throw new MultiSpanSassException("Invalid parent selector \"$this\"", $this->getSpan(), 'outer selector', []);
+ }
+
+ /**
+ * 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<SimpleSelector> $compound
+ *
+ * @return list<SimpleSelector>|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::iterableContains($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/vendor/scssphp/scssphp/src/Ast/Selector/TypeSelector.php b/vendor/scssphp/scssphp/src/Ast/Selector/TypeSelector.php
new file mode 100644
index 000000000..c7e86ff39
--- /dev/null
+++ b/vendor/scssphp/scssphp/src/Ast/Selector/TypeSelector.php
@@ -0,0 +1,87 @@
+<?php
+
+/**
+ * SCSSPHP
+ *
+ * @copyright 2012-2020 Leaf Corcoran
+ *
+ * @license http://opensource.org/licenses/MIT MIT
+ *
+ * @link http://scssphp.github.io/scssphp
+ */
+
+namespace ScssPhp\ScssPhp\Ast\Selector;
+
+use ScssPhp\ScssPhp\Extend\ExtendUtil;
+use ScssPhp\ScssPhp\Visitor\SelectorVisitor;
+use SourceSpan\FileSpan;
+
+/**
+ * A type selector.
+ *
+ * This selects elements whose name equals the given name.
+ *
+ * @internal
+ */
+final class TypeSelector extends SimpleSelector
+{
+ /**
+ * The element name being selected.
+ */
+ private readonly QualifiedName $name;
+
+ public function __construct(QualifiedName $name, FileSpan $span)
+ {
+ $this->name = $name;
+ parent::__construct($span);
+ }
+
+ 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()), $this->getSpan());
+ }
+
+ 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/vendor/scssphp/scssphp/src/Ast/Selector/UniversalSelector.php b/vendor/scssphp/scssphp/src/Ast/Selector/UniversalSelector.php
new file mode 100644
index 000000000..321560549
--- /dev/null
+++ b/vendor/scssphp/scssphp/src/Ast/Selector/UniversalSelector.php
@@ -0,0 +1,110 @@
+<?php
+
+/**
+ * SCSSPHP
+ *
+ * @copyright 2012-2020 Leaf Corcoran
+ *
+ * @license http://opensource.org/licenses/MIT MIT
+ *
+ * @link http://scssphp.github.io/scssphp
+ */
+
+namespace ScssPhp\ScssPhp\Ast\Selector;
+
+use ScssPhp\ScssPhp\Extend\ExtendUtil;
+use ScssPhp\ScssPhp\Visitor\SelectorVisitor;
+use SourceSpan\FileSpan;
+
+/**
+ * Matches any element in the given namespace.
+ *
+ * @internal
+ */
+final class UniversalSelector extends SimpleSelector
+{
+ /**
+ * The selector namespace.
+ *
+ * If this is `null`, this matches all elements in the default namespace. If
+ * it's the empty string, this matches all elements that aren't in any
+ * namespace. If it's `*`, this matches all elements in any namespace.
+ * Otherwise, it matches all elements in the given namespace.
+ */
+ private readonly ?string $namespace;
+
+ public function __construct(FileSpan $span, ?string $namespace = null)
+ {
+ $this->namespace = $namespace;
+ parent::__construct($span);
+ }
+
+ 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/vendor/scssphp/scssphp/src/Base/Range.php b/vendor/scssphp/scssphp/src/Base/Range.php
deleted file mode 100644
index 31d5ec565..000000000
--- a/vendor/scssphp/scssphp/src/Base/Range.php
+++ /dev/null
@@ -1,57 +0,0 @@
-<?php
-
-/**
- * SCSSPHP
- *
- * @copyright 2015-2020 Leaf Corcoran
- *
- * @license http://opensource.org/licenses/MIT MIT
- *
- * @link http://scssphp.github.io/scssphp
- */
-
-namespace ScssPhp\ScssPhp\Base;
-
-/**
- * Range
- *
- * @author Anthon Pang <anthon.pang@gmail.com>
- *
- * @internal
- */
-class Range
-{
- /**
- * @var float|int
- */
- public $first;
-
- /**
- * @var float|int
- */
- public $last;
-
- /**
- * Initialize range
- *
- * @param int|float $first
- * @param int|float $last
- */
- public function __construct($first, $last)
- {
- $this->first = $first;
- $this->last = $last;
- }
-
- /**
- * Test for inclusion in range
- *
- * @param int|float $value
- *
- * @return bool
- */
- public function includes($value)
- {
- return $value >= $this->first && $value <= $this->last;
- }
-}
diff --git a/vendor/scssphp/scssphp/src/Block.php b/vendor/scssphp/scssphp/src/Block.php
deleted file mode 100644
index 96668dc66..000000000
--- a/vendor/scssphp/scssphp/src/Block.php
+++ /dev/null
@@ -1,73 +0,0 @@
-<?php
-
-/**
- * SCSSPHP
- *
- * @copyright 2012-2020 Leaf Corcoran
- *
- * @license http://opensource.org/licenses/MIT MIT
- *
- * @link http://scssphp.github.io/scssphp
- */
-
-namespace ScssPhp\ScssPhp;
-
-/**
- * Block
- *
- * @author Anthon Pang <anthon.pang@gmail.com>
- *
- * @internal
- */
-class Block
-{
- /**
- * @var string|null
- */
- public $type;
-
- /**
- * @var Block|null
- */
- public $parent;
-
- /**
- * @var string
- */
- public $sourceName;
-
- /**
- * @var int
- */
- public $sourceIndex;
-
- /**
- * @var int
- */
- public $sourceLine;
-
- /**
- * @var int
- */
- public $sourceColumn;
-
- /**
- * @var array|null
- */
- public $selectors;
-
- /**
- * @var array
- */
- public $comments;
-
- /**
- * @var array
- */
- public $children;
-
- /**
- * @var Block|null
- */
- public $selfParent;
-}
diff --git a/vendor/scssphp/scssphp/src/Block/AtRootBlock.php b/vendor/scssphp/scssphp/src/Block/AtRootBlock.php
deleted file mode 100644
index 41842c269..000000000
--- a/vendor/scssphp/scssphp/src/Block/AtRootBlock.php
+++ /dev/null
@@ -1,37 +0,0 @@
-<?php
-
-/**
- * SCSSPHP
- *
- * @copyright 2012-2020 Leaf Corcoran
- *
- * @license http://opensource.org/licenses/MIT MIT
- *
- * @link http://scssphp.github.io/scssphp
- */
-
-namespace ScssPhp\ScssPhp\Block;
-
-use ScssPhp\ScssPhp\Block;
-use ScssPhp\ScssPhp\Type;
-
-/**
- * @internal
- */
-class AtRootBlock extends Block
-{
- /**
- * @var array|null
- */
- public $selector;
-
- /**
- * @var array|null
- */
- public $with;
-
- public function __construct()
- {
- $this->type = Type::T_AT_ROOT;
- }
-}
diff --git a/vendor/scssphp/scssphp/src/Block/CallableBlock.php b/vendor/scssphp/scssphp/src/Block/CallableBlock.php
deleted file mode 100644
index 9b32d8ce7..000000000
--- a/vendor/scssphp/scssphp/src/Block/CallableBlock.php
+++ /dev/null
@@ -1,46 +0,0 @@
-<?php
-
-/**
- * SCSSPHP
- *
- * @copyright 2012-2020 Leaf Corcoran
- *
- * @license http://opensource.org/licenses/MIT MIT
- *
- * @link http://scssphp.github.io/scssphp
- */
-
-namespace ScssPhp\ScssPhp\Block;
-
-use ScssPhp\ScssPhp\Block;
-use ScssPhp\ScssPhp\Compiler\Environment;
-use ScssPhp\ScssPhp\Node\Number;
-
-/**
- * @internal
- */
-class CallableBlock extends Block
-{
- /**
- * @var string
- */
- public $name;
-
- /**
- * @var list<array{string, array|Number|null, bool}>|null
- */
- public $args;
-
- /**
- * @var Environment|null
- */
- public $parentEnv;
-
- /**
- * @param string $type
- */
- public function __construct($type)
- {
- $this->type = $type;
- }
-}
diff --git a/vendor/scssphp/scssphp/src/Block/ContentBlock.php b/vendor/scssphp/scssphp/src/Block/ContentBlock.php
deleted file mode 100644
index 870849800..000000000
--- a/vendor/scssphp/scssphp/src/Block/ContentBlock.php
+++ /dev/null
@@ -1,38 +0,0 @@
-<?php
-
-/**
- * SCSSPHP
- *
- * @copyright 2012-2020 Leaf Corcoran
- *
- * @license http://opensource.org/licenses/MIT MIT
- *
- * @link http://scssphp.github.io/scssphp
- */
-
-namespace ScssPhp\ScssPhp\Block;
-
-use ScssPhp\ScssPhp\Block;
-use ScssPhp\ScssPhp\Compiler\Environment;
-use ScssPhp\ScssPhp\Type;
-
-/**
- * @internal
- */
-class ContentBlock extends Block
-{
- /**
- * @var array|null
- */
- public $child;
-
- /**
- * @var Environment|null
- */
- public $scope;
-
- public function __construct()
- {
- $this->type = Type::T_INCLUDE;
- }
-}
diff --git a/vendor/scssphp/scssphp/src/Block/DirectiveBlock.php b/vendor/scssphp/scssphp/src/Block/DirectiveBlock.php
deleted file mode 100644
index b1d3d1a81..000000000
--- a/vendor/scssphp/scssphp/src/Block/DirectiveBlock.php
+++ /dev/null
@@ -1,37 +0,0 @@
-<?php
-
-/**
- * SCSSPHP
- *
- * @copyright 2012-2020 Leaf Corcoran
- *
- * @license http://opensource.org/licenses/MIT MIT
- *
- * @link http://scssphp.github.io/scssphp
- */
-
-namespace ScssPhp\ScssPhp\Block;
-
-use ScssPhp\ScssPhp\Block;
-use ScssPhp\ScssPhp\Type;
-
-/**
- * @internal
- */
-class DirectiveBlock extends Block
-{
- /**
- * @var string|array
- */
- public $name;
-
- /**
- * @var string|array|null
- */
- public $value;
-
- public function __construct()
- {
- $this->type = Type::T_DIRECTIVE;
- }
-}
diff --git a/vendor/scssphp/scssphp/src/Block/EachBlock.php b/vendor/scssphp/scssphp/src/Block/EachBlock.php
deleted file mode 100644
index b3289579d..000000000
--- a/vendor/scssphp/scssphp/src/Block/EachBlock.php
+++ /dev/null
@@ -1,37 +0,0 @@
-<?php
-
-/**
- * SCSSPHP
- *
- * @copyright 2012-2020 Leaf Corcoran
- *
- * @license http://opensource.org/licenses/MIT MIT
- *
- * @link http://scssphp.github.io/scssphp
- */
-
-namespace ScssPhp\ScssPhp\Block;
-
-use ScssPhp\ScssPhp\Block;
-use ScssPhp\ScssPhp\Type;
-
-/**
- * @internal
- */
-class EachBlock extends Block
-{
- /**
- * @var string[]
- */
- public $vars = [];
-
- /**
- * @var array
- */
- public $list;
-
- public function __construct()
- {
- $this->type = Type::T_EACH;
- }
-}
diff --git a/vendor/scssphp/scssphp/src/Block/ElseBlock.php b/vendor/scssphp/scssphp/src/Block/ElseBlock.php
deleted file mode 100644
index 6abb4d775..000000000
--- a/vendor/scssphp/scssphp/src/Block/ElseBlock.php
+++ /dev/null
@@ -1,27 +0,0 @@
-<?php
-
-/**
- * SCSSPHP
- *
- * @copyright 2012-2020 Leaf Corcoran
- *
- * @license http://opensource.org/licenses/MIT MIT
- *
- * @link http://scssphp.github.io/scssphp
- */
-
-namespace ScssPhp\ScssPhp\Block;
-
-use ScssPhp\ScssPhp\Block;
-use ScssPhp\ScssPhp\Type;
-
-/**
- * @internal
- */
-class ElseBlock extends Block
-{
- public function __construct()
- {
- $this->type = Type::T_ELSE;
- }
-}
diff --git a/vendor/scssphp/scssphp/src/Block/ElseifBlock.php b/vendor/scssphp/scssphp/src/Block/ElseifBlock.php
deleted file mode 100644
index 4622bca79..000000000
--- a/vendor/scssphp/scssphp/src/Block/ElseifBlock.php
+++ /dev/null
@@ -1,32 +0,0 @@
-<?php
-
-/**
- * SCSSPHP
- *
- * @copyright 2012-2020 Leaf Corcoran
- *
- * @license http://opensource.org/licenses/MIT MIT
- *
- * @link http://scssphp.github.io/scssphp
- */
-
-namespace ScssPhp\ScssPhp\Block;
-
-use ScssPhp\ScssPhp\Block;
-use ScssPhp\ScssPhp\Type;
-
-/**
- * @internal
- */
-class ElseifBlock extends Block
-{
- /**
- * @var array
- */
- public $cond;
-
- public function __construct()
- {
- $this->type = Type::T_ELSEIF;
- }
-}
diff --git a/vendor/scssphp/scssphp/src/Block/ForBlock.php b/vendor/scssphp/scssphp/src/Block/ForBlock.php
deleted file mode 100644
index a9cf6733b..000000000
--- a/vendor/scssphp/scssphp/src/Block/ForBlock.php
+++ /dev/null
@@ -1,47 +0,0 @@
-<?php
-
-/**
- * SCSSPHP
- *
- * @copyright 2012-2020 Leaf Corcoran
- *
- * @license http://opensource.org/licenses/MIT MIT
- *
- * @link http://scssphp.github.io/scssphp
- */
-
-namespace ScssPhp\ScssPhp\Block;
-
-use ScssPhp\ScssPhp\Block;
-use ScssPhp\ScssPhp\Type;
-
-/**
- * @internal
- */
-class ForBlock extends Block
-{
- /**
- * @var string
- */
- public $var;
-
- /**
- * @var array
- */
- public $start;
-
- /**
- * @var array
- */
- public $end;
-
- /**
- * @var bool
- */
- public $until;
-
- public function __construct()
- {
- $this->type = Type::T_FOR;
- }
-}
diff --git a/vendor/scssphp/scssphp/src/Block/IfBlock.php b/vendor/scssphp/scssphp/src/Block/IfBlock.php
deleted file mode 100644
index 9f21bf88a..000000000
--- a/vendor/scssphp/scssphp/src/Block/IfBlock.php
+++ /dev/null
@@ -1,37 +0,0 @@
-<?php
-
-/**
- * SCSSPHP
- *
- * @copyright 2012-2020 Leaf Corcoran
- *
- * @license http://opensource.org/licenses/MIT MIT
- *
- * @link http://scssphp.github.io/scssphp
- */
-
-namespace ScssPhp\ScssPhp\Block;
-
-use ScssPhp\ScssPhp\Block;
-use ScssPhp\ScssPhp\Type;
-
-/**
- * @internal
- */
-class IfBlock extends Block
-{
- /**
- * @var array
- */
- public $cond;
-
- /**
- * @var array<ElseifBlock|ElseBlock>
- */
- public $cases = [];
-
- public function __construct()
- {
- $this->type = Type::T_IF;
- }
-}
diff --git a/vendor/scssphp/scssphp/src/Block/MediaBlock.php b/vendor/scssphp/scssphp/src/Block/MediaBlock.php
deleted file mode 100644
index c49ee1b2b..000000000
--- a/vendor/scssphp/scssphp/src/Block/MediaBlock.php
+++ /dev/null
@@ -1,37 +0,0 @@
-<?php
-
-/**
- * SCSSPHP
- *
- * @copyright 2012-2020 Leaf Corcoran
- *
- * @license http://opensource.org/licenses/MIT MIT
- *
- * @link http://scssphp.github.io/scssphp
- */
-
-namespace ScssPhp\ScssPhp\Block;
-
-use ScssPhp\ScssPhp\Block;
-use ScssPhp\ScssPhp\Type;
-
-/**
- * @internal
- */
-class MediaBlock extends Block
-{
- /**
- * @var string|array|null
- */
- public $value;
-
- /**
- * @var array|null
- */
- public $queryList;
-
- public function __construct()
- {
- $this->type = Type::T_MEDIA;
- }
-}
diff --git a/vendor/scssphp/scssphp/src/Block/NestedPropertyBlock.php b/vendor/scssphp/scssphp/src/Block/NestedPropertyBlock.php
deleted file mode 100644
index 1ea4a6c8a..000000000
--- a/vendor/scssphp/scssphp/src/Block/NestedPropertyBlock.php
+++ /dev/null
@@ -1,37 +0,0 @@
-<?php
-
-/**
- * SCSSPHP
- *
- * @copyright 2012-2020 Leaf Corcoran
- *
- * @license http://opensource.org/licenses/MIT MIT
- *
- * @link http://scssphp.github.io/scssphp
- */
-
-namespace ScssPhp\ScssPhp\Block;
-
-use ScssPhp\ScssPhp\Block;
-use ScssPhp\ScssPhp\Type;
-
-/**
- * @internal
- */
-class NestedPropertyBlock extends Block
-{
- /**
- * @var bool
- */
- public $hasValue;
-
- /**
- * @var array
- */
- public $prefix;
-
- public function __construct()
- {
- $this->type = Type::T_NESTED_PROPERTY;
- }
-}
diff --git a/vendor/scssphp/scssphp/src/Block/WhileBlock.php b/vendor/scssphp/scssphp/src/Block/WhileBlock.php
deleted file mode 100644
index ac18d4e02..000000000
--- a/vendor/scssphp/scssphp/src/Block/WhileBlock.php
+++ /dev/null
@@ -1,32 +0,0 @@
-<?php
-
-/**
- * SCSSPHP
- *
- * @copyright 2012-2020 Leaf Corcoran
- *
- * @license http://opensource.org/licenses/MIT MIT
- *
- * @link http://scssphp.github.io/scssphp
- */
-
-namespace ScssPhp\ScssPhp\Block;
-
-use ScssPhp\ScssPhp\Block;
-use ScssPhp\ScssPhp\Type;
-
-/**
- * @internal
- */
-class WhileBlock extends Block
-{
- /**
- * @var array
- */
- public $cond;
-
- public function __construct()
- {
- $this->type = Type::T_WHILE;
- }
-}
diff --git a/vendor/scssphp/scssphp/src/Cache.php b/vendor/scssphp/scssphp/src/Cache.php
deleted file mode 100644
index 9731c60a7..000000000
--- a/vendor/scssphp/scssphp/src/Cache.php
+++ /dev/null
@@ -1,272 +0,0 @@
-<?php
-
-/**
- * SCSSPHP
- *
- * @copyright 2012-2020 Leaf Corcoran
- *
- * @license http://opensource.org/licenses/MIT MIT
- *
- * @link http://scssphp.github.io/scssphp
- */
-
-namespace ScssPhp\ScssPhp;
-
-use Exception;
-use ScssPhp\ScssPhp\Version;
-
-/**
- * The scss cache manager.
- *
- * In short:
- *
- * allow to put in cache/get from cache a generic result from a known operation on a generic dataset,
- * taking in account options that affects the result
- *
- * The cache manager is agnostic about data format and only the operation is expected to be described by string
- */
-
-/**
- * SCSS cache
- *
- * @author Cedric Morin <cedric@yterium.com>
- *
- * @internal
- */
-class Cache
-{
- const CACHE_VERSION = 1;
-
- /**
- * directory used for storing data
- *
- * @var string|false
- */
- public static $cacheDir = false;
-
- /**
- * prefix for the storing data
- *
- * @var string
- */
- public static $prefix = 'scssphp_';
-
- /**
- * force a refresh : 'once' for refreshing the first hit on a cache only, true to never use the cache in this hit
- *
- * @var bool|string
- */
- public static $forceRefresh = false;
-
- /**
- * specifies the number of seconds after which data cached will be seen as 'garbage' and potentially cleaned up
- *
- * @var int
- */
- public static $gcLifetime = 604800;
-
- /**
- * array of already refreshed cache if $forceRefresh==='once'
- *
- * @var array<string, bool>
- */
- protected static $refreshed = [];
-
- /**
- * Constructor
- *
- * @param array $options
- *
- * @phpstan-param array{cacheDir?: string, prefix?: string, forceRefresh?: string} $options
- */
- public function __construct($options)
- {
- // check $cacheDir
- if (isset($options['cacheDir'])) {
- self::$cacheDir = $options['cacheDir'];
- }
-
- if (empty(self::$cacheDir)) {
- throw new Exception('cacheDir not set');
- }
-
- if (isset($options['prefix'])) {
- self::$prefix = $options['prefix'];
- }
-
- if (empty(self::$prefix)) {
- throw new Exception('prefix not set');
- }
-
- if (isset($options['forceRefresh'])) {
- self::$forceRefresh = $options['forceRefresh'];
- }
-
- self::checkCacheDir();
- }
-
- /**
- * Get the cached result of $operation on $what,
- * which is known as dependant from the content of $options
- *
- * @param string $operation parse, compile...
- * @param mixed $what content key (e.g., filename to be treated)
- * @param array $options any option that affect the operation result on the content
- * @param int|null $lastModified last modified timestamp
- *
- * @return mixed
- *
- * @throws \Exception
- */
- public function getCache($operation, $what, $options = [], $lastModified = null)
- {
- $fileCache = self::$cacheDir . self::cacheName($operation, $what, $options);
-
- if (
- ((self::$forceRefresh === false) || (self::$forceRefresh === 'once' &&
- isset(self::$refreshed[$fileCache]))) && file_exists($fileCache)
- ) {
- $cacheTime = filemtime($fileCache);
-
- if (
- (\is_null($lastModified) || $cacheTime > $lastModified) &&
- $cacheTime + self::$gcLifetime > time()
- ) {
- $c = file_get_contents($fileCache);
- $c = unserialize($c);
-
- if (\is_array($c) && isset($c['value'])) {
- return $c['value'];
- }
- }
- }
-
- return null;
- }
-
- /**
- * Put in cache the result of $operation on $what,
- * which is known as dependant from the content of $options
- *
- * @param string $operation
- * @param mixed $what
- * @param mixed $value
- * @param array $options
- *
- * @return void
- */
- public function setCache($operation, $what, $value, $options = [])
- {
- $fileCache = self::$cacheDir . self::cacheName($operation, $what, $options);
-
- $c = ['value' => $value];
- $c = serialize($c);
-
- file_put_contents($fileCache, $c);
-
- if (self::$forceRefresh === 'once') {
- self::$refreshed[$fileCache] = true;
- }
- }
-
- /**
- * Get the cache name for the caching of $operation on $what,
- * which is known as dependant from the content of $options
- *
- * @param string $operation
- * @param mixed $what
- * @param array $options
- *
- * @return string
- */
- private static function cacheName($operation, $what, $options = [])
- {
- $t = [
- 'version' => self::CACHE_VERSION,
- 'scssphpVersion' => Version::VERSION,
- 'operation' => $operation,
- 'what' => $what,
- 'options' => $options
- ];
-
- $t = self::$prefix
- . sha1(json_encode($t))
- . ".$operation"
- . ".scsscache";
-
- return $t;
- }
-
- /**
- * Check that the cache dir exists and is writeable
- *
- * @return void
- *
- * @throws \Exception
- */
- public static function checkCacheDir()
- {
- self::$cacheDir = str_replace('\\', '/', self::$cacheDir);
- self::$cacheDir = rtrim(self::$cacheDir, '/') . '/';
-
- if (! is_dir(self::$cacheDir)) {
- throw new Exception('Cache directory doesn\'t exist: ' . self::$cacheDir);
- }
-
- if (! is_writable(self::$cacheDir)) {
- throw new Exception('Cache directory isn\'t writable: ' . self::$cacheDir);
- }
- }
-
- /**
- * Delete unused cached files
- *
- * @return void
- */
- public static function cleanCache()
- {
- static $clean = false;
-
- if ($clean || empty(self::$cacheDir)) {
- return;
- }
-
- $clean = true;
-
- // only remove files with extensions created by SCSSPHP Cache
- // css files removed based on the list files
- $removeTypes = ['scsscache' => 1];
-
- $files = scandir(self::$cacheDir);
-
- if (! $files) {
- return;
- }
-
- $checkTime = time() - self::$gcLifetime;
-
- foreach ($files as $file) {
- // don't delete if the file wasn't created with SCSSPHP Cache
- if (strpos($file, self::$prefix) !== 0) {
- continue;
- }
-
- $parts = explode('.', $file);
- $type = array_pop($parts);
-
- if (! isset($removeTypes[$type])) {
- continue;
- }
-
- $fullPath = self::$cacheDir . $file;
- $mtime = filemtime($fullPath);
-
- // don't delete if it's a relatively new file
- if ($mtime > $checkTime) {
- continue;
- }
-
- unlink($fullPath);
- }
- }
-}
diff --git a/vendor/scssphp/scssphp/src/Collection/Map.php b/vendor/scssphp/scssphp/src/Collection/Map.php
new file mode 100644
index 000000000..7e7c00702
--- /dev/null
+++ b/vendor/scssphp/scssphp/src/Collection/Map.php
@@ -0,0 +1,193 @@
+<?php
+
+/**
+ * SCSSPHP
+ *
+ * @copyright 2012-2020 Leaf Corcoran
+ *
+ * @license http://opensource.org/licenses/MIT MIT
+ *
+ * @link http://scssphp.github.io/scssphp
+ */
+
+namespace ScssPhp\ScssPhp\Collection;
+
+use ScssPhp\ScssPhp\Value\Value;
+
+/**
+ * A map using Sass values as keys based on Value::equals.
+ *
+ * The map can be either modifiable or unmodifiable. For unmodifiable
+ * maps, all mutators will throw a LogicException.
+ *
+ * Iteration preserves the order in which keys have been inserted.
+ *
+ * @template T
+ * @template-implements \IteratorAggregate<Value, T>
+ */
+final class Map implements \Countable, \IteratorAggregate
+{
+ private bool $modifiable = true;
+
+ // TODO implement a better internal storage to allow reading keys in O(1).
+
+ /**
+ * @var array<int, array{Value, T}>
+ */
+ private array $pairs = [];
+
+ /**
+ * Returns a modifiable version of the Map.
+ *
+ * @template V
+ * @param Map<V> $map
+ *
+ * @return Map<V>
+ */
+ 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<V> $map
+ *
+ * @return Map<V>
+ */
+ 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;
+ }
+
+ public function containsKey(Value $key): bool
+ {
+ return $this->get($key) !== 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;
+ }
+
+ /**
+ * @return list<Value>
+ */
+ public function keys(): array
+ {
+ $keys = [];
+
+ foreach ($this->pairs as $pair) {
+ $keys[] = $pair[0];
+ }
+
+ return $keys;
+ }
+
+ /**
+ * @return list<T>
+ */
+ public function values(): array
+ {
+ $values = [];
+
+ foreach ($this->pairs as $pair) {
+ $values[] = $pair[1];
+ }
+
+ return $values;
+ }
+
+ 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/vendor/scssphp/scssphp/src/Colors.php b/vendor/scssphp/scssphp/src/Colors.php
index 2df39992b..a72a7bbed 100644
--- a/vendor/scssphp/scssphp/src/Colors.php
+++ b/vendor/scssphp/scssphp/src/Colors.php
@@ -12,6 +12,8 @@
namespace ScssPhp\ScssPhp;
+use ScssPhp\ScssPhp\Value\SassColor;
+
/**
* CSS Colors
*
@@ -19,7 +21,7 @@ namespace ScssPhp\ScssPhp;
*
* @internal
*/
-class Colors
+final class Colors
{
/**
* CSS Colors
@@ -28,7 +30,7 @@ class Colors
*
* @var array<string, string>
*/
- protected static $cssColors = [
+ private const CSS_COLORS = [
'aliceblue' => '240,248,255',
'antiquewhite' => '250,235,215',
'aqua' => '0,255,255',
@@ -180,6 +182,17 @@ 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] ?? 1.0);
+ }
+
/**
* Convert named color in a [r,g,b[,a]] array
*
@@ -187,38 +200,25 @@ class Colors
*
* @return int[]|null
*/
- public static function colorNameToRGBa($colorName)
+ private static function colorNameToRGBa(string $colorName): ?array
{
- if (\is_string($colorName) && isset(static::$cssColors[$colorName])) {
- $rgba = explode(',', static::$cssColors[$colorName]);
+ if (isset(self::CSS_COLORS[$colorName])) {
+ $rgba = explode(',', self::CSS_COLORS[$colorName]);
// only case with opacity is transparent, with opacity=0, so we can intval on opacity also
- $rgba = array_map('intval', $rgba);
-
- return $rgba;
+ return array_map('intval', $rgba);
}
return null;
}
/**
- * Reverse conversion : from RGBA to a color name if possible
- *
- * @param int $r
- * @param int $g
- * @param int $b
- * @param int|float $a
- *
- * @return string|null
+ * Reverse conversion: from RGBA to a color name if possible
*/
- public static function RGBaToColorName($r, $g, $b, $a = 1)
+ public static function RGBaToColorName(int $r, int $g, int $b, float $a): ?string
{
static $reverseColorTable = null;
- if (! is_numeric($r) || ! is_numeric($g) || ! is_numeric($b) || ! is_numeric($a)) {
- return null;
- }
-
if ($a < 1) {
return null;
}
@@ -226,11 +226,11 @@ class Colors
if (\is_null($reverseColorTable)) {
$reverseColorTable = [];
- foreach (static::$cssColors as $name => $rgb_str) {
+ foreach (self::CSS_COLORS as $name => $rgb_str) {
$rgb_str = explode(',', $rgb_str);
if (
- \count($rgb_str) == 3 &&
+ \count($rgb_str) === 3 &&
! isset($reverseColorTable[\intval($rgb_str[0])][\intval($rgb_str[1])][\intval($rgb_str[2])])
) {
$reverseColorTable[\intval($rgb_str[0])][\intval($rgb_str[1])][\intval($rgb_str[2])] = $name;
@@ -238,8 +238,8 @@ class Colors
}
}
- if (isset($reverseColorTable[\intval($r)][\intval($g)][\intval($b)])) {
- return $reverseColorTable[\intval($r)][\intval($g)][\intval($b)];
+ if (isset($reverseColorTable[$r][$g][$b])) {
+ return $reverseColorTable[$r][$g][$b];
}
return null;
diff --git a/vendor/scssphp/scssphp/src/CompilationResult.php b/vendor/scssphp/scssphp/src/CompilationResult.php
index 36adb0da4..aa7faae7b 100644
--- a/vendor/scssphp/scssphp/src/CompilationResult.php
+++ b/vendor/scssphp/scssphp/src/CompilationResult.php
@@ -12,57 +12,44 @@
namespace ScssPhp\ScssPhp;
-class CompilationResult
+final class CompilationResult
{
- /**
- * @var string
- */
- private $css;
+ private string $css;
- /**
- * @var string|null
- */
- private $sourceMap;
+ private ?string $sourceMap;
/**
- * @var string[]
+ * @var list<string>
*/
- private $includedFiles;
+ private array $includedFiles;
/**
- * @param string $css
- * @param string|null $sourceMap
- * @param string[] $includedFiles
+ * @param list<string> $includedFiles
*/
- public function __construct($css, $sourceMap, array $includedFiles)
+ public function __construct(string $css, ?string $sourceMap, array $includedFiles)
{
$this->css = $css;
$this->sourceMap = $sourceMap;
$this->includedFiles = $includedFiles;
}
- /**
- * @return string
- */
- public function getCss()
+ public function getCss(): string
{
return $this->css;
}
/**
- * @return string[]
+ * @return list<string>
*/
- public function getIncludedFiles()
+ public function getIncludedFiles(): array
{
return $this->includedFiles;
}
/**
* The sourceMap content, if it was generated
- *
- * @return null|string
*/
- public function getSourceMap()
+ public function getSourceMap(): ?string
{
return $this->sourceMap;
}
diff --git a/vendor/scssphp/scssphp/src/Compiler.php b/vendor/scssphp/scssphp/src/Compiler.php
index d4e7c6896..4527f9c17 100644
--- a/vendor/scssphp/scssphp/src/Compiler.php
+++ b/vendor/scssphp/scssphp/src/Compiler.php
@@ -12,5398 +12,140 @@
namespace ScssPhp\ScssPhp;
-use ScssPhp\ScssPhp\Base\Range;
-use ScssPhp\ScssPhp\Block\AtRootBlock;
-use ScssPhp\ScssPhp\Block\CallableBlock;
-use ScssPhp\ScssPhp\Block\DirectiveBlock;
-use ScssPhp\ScssPhp\Block\EachBlock;
-use ScssPhp\ScssPhp\Block\ElseBlock;
-use ScssPhp\ScssPhp\Block\ElseifBlock;
-use ScssPhp\ScssPhp\Block\ForBlock;
-use ScssPhp\ScssPhp\Block\IfBlock;
-use ScssPhp\ScssPhp\Block\MediaBlock;
-use ScssPhp\ScssPhp\Block\NestedPropertyBlock;
-use ScssPhp\ScssPhp\Block\WhileBlock;
-use ScssPhp\ScssPhp\Compiler\CachedResult;
-use ScssPhp\ScssPhp\Compiler\Environment;
-use ScssPhp\ScssPhp\Exception\CompilerException;
-use ScssPhp\ScssPhp\Exception\ParserException;
+use League\Uri\Contracts\UriInterface;
+use League\Uri\Uri;
+use ScssPhp\ScssPhp\Ast\Css\CssParentNode;
+use ScssPhp\ScssPhp\Ast\Sass\Statement\Stylesheet;
+use ScssPhp\ScssPhp\Collection\Map;
+use ScssPhp\ScssPhp\Compiler\LegacyValueVisitor;
+use ScssPhp\ScssPhp\Evaluation\EvaluateVisitor;
use ScssPhp\ScssPhp\Exception\SassException;
use ScssPhp\ScssPhp\Exception\SassScriptException;
-use ScssPhp\ScssPhp\Formatter\Compressed;
-use ScssPhp\ScssPhp\Formatter\Expanded;
-use ScssPhp\ScssPhp\Formatter\OutputBlock;
+use ScssPhp\ScssPhp\Function\FunctionRegistry;
+use ScssPhp\ScssPhp\Importer\FilesystemImporter;
+use ScssPhp\ScssPhp\Importer\ImportCache;
+use ScssPhp\ScssPhp\Importer\Importer;
+use ScssPhp\ScssPhp\Importer\LegacyCallbackImporter;
+use ScssPhp\ScssPhp\Importer\NoOpImporter;
+use ScssPhp\ScssPhp\Logger\DeprecationProcessingLogger;
use ScssPhp\ScssPhp\Logger\LoggerInterface;
use ScssPhp\ScssPhp\Logger\StreamLogger;
use ScssPhp\ScssPhp\Node\Number;
-use ScssPhp\ScssPhp\SourceMap\SourceMapGenerator;
+use ScssPhp\ScssPhp\SassCallable\BuiltInCallable;
+use ScssPhp\ScssPhp\Serializer\Serializer;
use ScssPhp\ScssPhp\Util\Path;
-
-/**
- * The scss compiler and parser.
- *
- * Converting SCSS to CSS is a three stage process. The incoming file is parsed
- * by `Parser` into a syntax tree, then it is compiled into another tree
- * representing the CSS structure by `Compiler`. The CSS tree is fed into a
- * formatter, like `Formatter` which then outputs CSS as a string.
- *
- * During the first compile, all values are *reduced*, which means that their
- * types are brought to the lowest form before being dump as strings. This
- * handles math equations, variable dereferences, and the like.
- *
- * The `compile` function of `Compiler` is the entry point.
- *
- * In summary:
- *
- * The `Compiler` class creates an instance of the parser, feeds it SCSS code,
- * then transforms the resulting tree to a CSS tree. This class also holds the
- * evaluation context, such as all available mixins and variables at any given
- * time.
- *
- * The `Parser` class is only concerned with parsing its input.
- *
- * The `Formatter` takes a CSS tree, and dumps it to a formatted string,
- * handling things like indentation.
- */
-
-/**
- * SCSS compiler
- *
- * @author Leaf Corcoran <leafot@gmail.com>
- *
- * @final Extending the Compiler is deprecated
- */
-class Compiler
+use ScssPhp\ScssPhp\Value\ListSeparator;
+use ScssPhp\ScssPhp\Value\SassArgumentList;
+use ScssPhp\ScssPhp\Value\SassBoolean;
+use ScssPhp\ScssPhp\Value\SassColor;
+use ScssPhp\ScssPhp\Value\SassList;
+use ScssPhp\ScssPhp\Value\SassMap;
+use ScssPhp\ScssPhp\Value\SassNull;
+use ScssPhp\ScssPhp\Value\SassNumber;
+use ScssPhp\ScssPhp\Value\SassString;
+use ScssPhp\ScssPhp\Value\Value;
+use ScssPhp\ScssPhp\Visitor\CssVisitor;
+
+final 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;
- /**
- * @var array<string, string>
- */
- protected static $operatorNames = [
- '+' => 'add',
- '-' => 'sub',
- '*' => 'mul',
- '/' => 'div',
- '%' => 'mod',
-
- '==' => 'eq',
- '!=' => 'neq',
- '<' => 'lt',
- '>' => 'gt',
-
- '<=' => 'lte',
- '>=' => 'gte',
- ];
-
- /**
- * @var array<string, string>
- */
- protected static $namespaces = [
- 'special' => '%',
- 'mixin' => '@',
- 'function' => '^',
- ];
-
public static $true = [Type::T_KEYWORD, 'true'];
public static $false = [Type::T_KEYWORD, 'false'];
- /** @deprecated */
- public static $NaN = [Type::T_KEYWORD, 'NaN'];
- /** @deprecated */
- public static $Infinity = [Type::T_KEYWORD, 'Infinity'];
public static $null = [Type::T_NULL];
- public static $nullString = [Type::T_STRING, '', []];
- public static $defaultValue = [Type::T_KEYWORD, ''];
- public static $selfSelector = [Type::T_SELF];
public static $emptyList = [Type::T_LIST, '', []];
public static $emptyMap = [Type::T_MAP, [], []];
public static $emptyString = [Type::T_STRING, '"', []];
- public static $with = [Type::T_KEYWORD, 'with'];
- public static $without = [Type::T_KEYWORD, 'without'];
- private static $emptyArgumentList = [Type::T_LIST, '', [], []];
-
- /**
- * @var array<int, string|callable>
- */
- protected $importPaths = [];
- /**
- * @var array<string, Block>
- */
- protected $importCache = [];
-
- /**
- * @var string[]
- */
- protected $importedFiles = [];
/**
- * @var array
- * @phpstan-var array<string, array{0: callable, 1: string[]|null}>
- */
- protected $userFunctions = [];
- /**
- * @var array<string, mixed>
+ * @var list<Importer>
*/
- protected $registeredVars = [];
- /**
- * @var array<string, bool>
- */
- protected $registeredFeatures = [
- 'extend-selector-pseudoclass' => false,
- 'at-error' => true,
- 'units-level-3' => true,
- 'global-variable-shadowing' => false,
- ];
+ private array $importers = [];
/**
- * @var string|null
- */
- protected $encoding = null;
- /**
- * @var null
- * @deprecated
+ * @var array<int, string|callable(string): (string|null)>
*/
- protected $lineNumberStyle = null;
+ private array $importPaths = [];
/**
- * @var int|SourceMapGenerator
- * @phpstan-var self::SOURCE_MAP_*|SourceMapGenerator
+ * @var array<string, array{0: callable, 1: string[]}>
*/
- protected $sourceMap = self::SOURCE_MAP_NONE;
+ private array $userFunctions = [];
/**
- * @var array
- * @phpstan-var array{sourceRoot?: string, sourceMapFilename?: string|null, sourceMapURL?: string|null, sourceMapWriteTo?: string|null, outputSourceFiles?: bool, sourceMapRootpath?: string, sourceMapBasepath?: string}
+ * @var array<string, Value>
*/
- protected $sourceMapOptions = [];
+ private array $registeredVars = [];
/**
- * @var bool
+ * @var self::SOURCE_MAP_*
*/
- private $charset = true;
+ private int $sourceMap = self::SOURCE_MAP_NONE;
/**
- * @var Formatter
+ * @var array{sourceRoot?: string, sourceMapFilename?: string|null, sourceMapURL?: string|null, outputSourceFiles?: bool, sourceMapRootpath?: string, sourceMapBasepath?: string}
*/
- protected $formatter;
+ private array $sourceMapOptions = [];
- /**
- * @var string
- * @phpstan-var class-string<Formatter>
- */
- private $configuredFormatter = Expanded::class;
+ private bool $charset = true;
- /**
- * @var Environment
- */
- protected $rootEnv;
- /**
- * @var OutputBlock|null
- */
- protected $rootBlock;
+ private bool $quietDeps = false;
/**
- * @var \ScssPhp\ScssPhp\Compiler\Environment
- */
- protected $env;
- /**
- * @var OutputBlock|null
- */
- protected $scope;
- /**
- * @var Environment|null
- */
- protected $storeEnv;
- /**
- * @var bool|null
+ * Deprecation warnings of these types will be ignored.
*
- * @deprecated
- */
- protected $charsetSeen;
- /**
- * @var array<int, string|null>
- */
- protected $sourceNames;
-
- /**
- * @var Cache|null
+ * @var Deprecation[]
*/
- protected $cache;
+ private array $silenceDeprecations = [];
/**
- * @var bool
- */
- protected $cacheCheckImportResolutions = false;
-
- /**
- * @var int
- */
- protected $indentLevel;
- /**
- * @var array[]
- */
- protected $extends;
- /**
- * @var array<string, int[]>
- */
- protected $extendsMap;
-
- /**
- * @var array<string, int>
- */
- protected $parsedFiles = [];
-
- /**
- * @var Parser|null
- */
- protected $parser;
- /**
- * @var int|null
- */
- protected $sourceIndex;
- /**
- * @var int|null
- */
- protected $sourceLine;
- /**
- * @var int|null
- */
- protected $sourceColumn;
- /**
- * @var bool|null
- */
- protected $shouldEvaluate;
- /**
- * @var null
- * @deprecated
- */
- protected $ignoreErrors;
- /**
- * @var bool
- */
- protected $ignoreCallStackMessage = false;
-
- /**
- * @var array[]
- */
- protected $callStack = [];
-
- /**
- * @var array
- * @phpstan-var list<array{currentDir: string|null, path: string, filePath: string}>
- */
- private $resolvedImports = [];
-
- /**
- * The directory of the currently processed file
+ * Deprecation warnings of one of these types will cause an error to be
+ * thrown.
*
- * @var string|null
+ * Future deprecations in this list will still cause an error even if they
+ * are not also in {@see $futureDeprecations}.
+ *
+ * @var Deprecation[]
*/
- private $currentDirectory;
+ private array $fatalDeprecations = [];
/**
- * The directory of the input file
+ * Future deprecations that the user has explicitly opted into.
*
- * @var string
+ * @var Deprecation[]
*/
- private $rootDirectory;
+ private array $futureDeprecations = [];
- /**
- * @var bool
- */
- private $legacyCwdImportPath = true;
+ private bool $verbose = false;
- /**
- * @var LoggerInterface
- */
- private $logger;
+ private OutputStyle $outputStyle = OutputStyle::EXPANDED;
- /**
- * @var array<string, bool>
- */
- private $warnedChildFunctions = [];
+ private LoggerInterface $logger;
- /**
- * Constructor
- *
- * @param array|null $cacheOptions
- * @phpstan-param array{cacheDir?: string, prefix?: string, forceRefresh?: string, checkImportResolutions?: bool}|null $cacheOptions
- */
- public function __construct($cacheOptions = null)
+ public function __construct()
{
- $this->sourceNames = [];
-
- if ($cacheOptions) {
- $this->cache = new Cache($cacheOptions);
- if (!empty($cacheOptions['checkImportResolutions'])) {
- $this->cacheCheckImportResolutions = true;
- }
- }
-
$this->logger = new StreamLogger(fopen('php://stderr', 'w'), true);
}
/**
- * Get compiler options
- *
- * @return array<string, mixed>
- *
- * @internal
- */
- public function getCompileOptions()
- {
- $options = [
- 'importPaths' => $this->importPaths,
- 'registeredVars' => $this->registeredVars,
- 'registeredFeatures' => $this->registeredFeatures,
- 'encoding' => $this->encoding,
- 'sourceMap' => serialize($this->sourceMap),
- 'sourceMapOptions' => $this->sourceMapOptions,
- 'formatter' => $this->configuredFormatter,
- 'legacyImportPath' => $this->legacyCwdImportPath,
- ];
-
- return $options;
- }
-
- /**
* Sets an alternative logger.
*
* Changing the logger in the middle of the compilation is not
* supported and will result in an undefined behavior.
- *
- * @param LoggerInterface $logger
- *
- * @return void
*/
- public function setLogger(LoggerInterface $logger)
+ public function setLogger(LoggerInterface $logger): void
{
$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 $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 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);
- $compileOptions = $this->getCompileOptions();
- $cachedResult = $this->cache->getCache('compile', $cacheKey, $compileOptions);
-
- if ($cachedResult instanceof CachedResult && $this->isFreshCachedResult($cachedResult)) {
- return $cachedResult->getResult();
- }
- }
-
- $this->indentLevel = -1;
- $this->extends = [];
- $this->extendsMap = [];
- $this->sourceIndex = null;
- $this->sourceLine = null;
- $this->sourceColumn = null;
- $this->env = null;
- $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)) {
- $path = realpath($path) ?: $path;
- $this->currentDirectory = dirname($path);
- $this->rootDirectory = $this->currentDirectory;
- } else {
- $this->currentDirectory = null;
- $this->rootDirectory = getcwd();
- }
-
- try {
- $this->parser = $this->parserFactory($path);
- $tree = $this->parser->parse($source);
- $this->parser = null;
-
- $this->formatter = new $this->configuredFormatter();
- $this->rootBlock = null;
- $this->rootEnv = $this->pushEnv($tree);
-
- $warnCallback = function ($message, $deprecation) {
- $this->logger->warn($message, $deprecation);
- };
- $previousWarnCallback = Warn::setCallback($warnCallback);
-
- try {
- $this->injectVariables($this->registeredVars);
- $this->compileRoot($tree);
- $this->popEnv();
- } finally {
- Warn::setCallback($previousWarnCallback);
- }
-
- $sourceMapGenerator = null;
-
- if ($this->sourceMap) {
- if (\is_object($this->sourceMap) && $this->sourceMap instanceof SourceMapGenerator) {
- $sourceMapGenerator = $this->sourceMap;
- $this->sourceMap = self::SOURCE_MAP_FILE;
- } elseif ($this->sourceMap !== self::SOURCE_MAP_NONE) {
- $sourceMapGenerator = new SourceMapGenerator($this->sourceMapOptions);
- }
- }
- assert($this->scope !== null);
-
- $out = $this->formatter->format($this->scope, $sourceMapGenerator);
-
- $prefix = '';
-
- if ($this->charset && strlen($out) !== Util::mbStrlen($out)) {
- $prefix = '@charset "UTF-8";' . "\n";
- $out = $prefix . $out;
- }
-
- $sourceMap = null;
-
- if (! empty($out) && $this->sourceMap !== self::SOURCE_MAP_NONE && $this->sourceMap) {
- assert($sourceMapGenerator !== null);
- $sourceMap = $sourceMapGenerator->generateJson($prefix);
- $sourceMapUrl = null;
-
- switch ($this->sourceMap) {
- case self::SOURCE_MAP_INLINE:
- $sourceMapUrl = sprintf('data:application/json,%s', Util::encodeURIComponent($sourceMap));
- break;
-
- case self::SOURCE_MAP_FILE:
- if (isset($this->sourceMapOptions['sourceMapURL'])) {
- $sourceMapUrl = $this->sourceMapOptions['sourceMapURL'];
- }
- break;
- }
-
- if ($sourceMapUrl !== null) {
- $out .= sprintf('/*# sourceMappingURL=%s */', $sourceMapUrl);
- }
- }
- } catch (SassScriptException $e) {
- throw new CompilerException($this->addLocationToMessage($e->getMessage()), 0, $e);
- }
-
- $includedFiles = [];
-
- foreach ($this->resolvedImports as $resolvedImport) {
- $includedFiles[$resolvedImport['filePath']] = $resolvedImport['filePath'];
- }
-
- $result = new CompilationResult($out, $sourceMap, array_values($includedFiles));
-
- if ($this->cache && isset($cacheKey) && isset($compileOptions)) {
- $this->cache->setCache('compile', $cacheKey, new CachedResult($result, $this->parsedFiles, $this->resolvedImports), $compileOptions);
- }
-
- // Reset state to free memory
- // TODO in 2.0, reset parsedFiles as well when the getter is removed.
- $this->resolvedImports = [];
- $this->importedFiles = [];
-
- return $result;
- }
-
- /**
- * @param CachedResult $result
- *
- * @return bool
- */
- private function isFreshCachedResult(CachedResult $result)
- {
- // check if any dependency file changed since the result was compiled
- foreach ($result->getParsedFiles() as $file => $mtime) {
- if (! is_file($file) || filemtime($file) !== $mtime) {
- return false;
- }
- }
-
- if ($this->cacheCheckImportResolutions) {
- $resolvedImports = [];
-
- foreach ($result->getResolvedImports() as $import) {
- $currentDir = $import['currentDir'];
- $path = $import['path'];
- // store the check across all the results in memory to avoid multiple findImport() on the same path
- // with same context.
- // this is happening in a same hit with multiple compilations (especially with big frameworks)
- if (empty($resolvedImports[$currentDir][$path])) {
- $resolvedImports[$currentDir][$path] = $this->findImport($path, $currentDir);
- }
-
- if ($resolvedImports[$currentDir][$path] !== $import['filePath']) {
- return false;
- }
- }
- }
-
- return true;
- }
-
- /**
- * Instantiate parser
- *
- * @param string|null $path
- *
- * @return \ScssPhp\ScssPhp\Parser
- */
- protected function parserFactory($path)
- {
- // https://sass-lang.com/documentation/at-rules/import
- // CSS files imported by Sass don’t allow any special Sass features.
- // In order to make sure authors don’t accidentally write Sass in their CSS,
- // all Sass features that aren’t also valid CSS will produce errors.
- // Otherwise, the CSS will be rendered as-is. It can even be extended!
- $cssOnly = false;
-
- if ($path !== null && substr($path, -4) === '.css') {
- $cssOnly = true;
- }
-
- $parser = new Parser($path, \count($this->sourceNames), $this->encoding, $this->cache, $cssOnly, $this->logger);
-
- $this->sourceNames[] = $path;
- $this->addParsedFile($path);
-
- return $parser;
- }
-
- /**
- * Is self extend?
- *
- * @param array $target
- * @param array $origin
- *
- * @return bool
- */
- protected function isSelfExtend($target, $origin)
- {
- foreach ($origin as $sel) {
- if (\in_array($target, $sel)) {
- return true;
- }
- }
-
- return false;
- }
-
- /**
- * Push extends
- *
- * @param string[] $target
- * @param array $origin
- * @param array|null $block
- *
- * @return void
- */
- protected function pushExtends($target, $origin, $block)
- {
- $i = \count($this->extends);
- $this->extends[] = [$target, $origin, $block];
-
- foreach ($target as $part) {
- if (isset($this->extendsMap[$part])) {
- $this->extendsMap[$part][] = $i;
- } else {
- $this->extendsMap[$part] = [$i];
- }
- }
- }
-
- /**
- * Make output block
- *
- * @param string|null $type
- * @param string[]|null $selectors
- *
- * @return \ScssPhp\ScssPhp\Formatter\OutputBlock
- */
- protected function makeOutputBlock($type, $selectors = null)
- {
- $out = new OutputBlock();
- $out->type = $type;
- $out->lines = [];
- $out->children = [];
- $out->parent = $this->scope;
- $out->selectors = $selectors;
- $out->depth = $this->env->depth;
-
- if ($this->env->block instanceof Block) {
- $out->sourceName = $this->env->block->sourceName;
- $out->sourceLine = $this->env->block->sourceLine;
- $out->sourceColumn = $this->env->block->sourceColumn;
- } else {
- $out->sourceName = isset($this->sourceNames[$this->sourceIndex]) ? $this->sourceNames[$this->sourceIndex] : '(stdin)';
- $out->sourceLine = $this->sourceLine;
- $out->sourceColumn = $this->sourceColumn;
- }
-
- return $out;
- }
-
- /**
- * Compile root
- *
- * @param \ScssPhp\ScssPhp\Block $rootBlock
- *
- * @return void
- */
- protected function compileRoot(Block $rootBlock)
- {
- $this->rootBlock = $this->scope = $this->makeOutputBlock(Type::T_ROOT);
-
- $this->compileChildrenNoReturn($rootBlock->children, $this->scope);
- assert($this->scope !== null);
- $this->flattenSelectors($this->scope);
- $this->missingSelectors();
- }
-
- /**
- * Report missing selectors
- *
- * @return void
- */
- protected function missingSelectors()
- {
- foreach ($this->extends as $extend) {
- if (isset($extend[3])) {
- continue;
- }
-
- list($target, $origin, $block) = $extend;
-
- // ignore if !optional
- if ($block[2]) {
- continue;
- }
-
- $target = implode(' ', $target);
- $origin = $this->collapseSelectors($origin);
-
- $this->sourceLine = $block[Parser::SOURCE_LINE];
- throw $this->error("\"$origin\" failed to @extend \"$target\". The selector \"$target\" was not found.");
- }
- }
-
- /**
- * Flatten selectors
- *
- * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $block
- * @param string $parentKey
- *
- * @return void
- */
- protected function flattenSelectors(OutputBlock $block, $parentKey = null)
- {
- if ($block->selectors) {
- $selectors = [];
-
- foreach ($block->selectors as $s) {
- $selectors[] = $s;
-
- if (! \is_array($s)) {
- continue;
- }
-
- // check extends
- if (! empty($this->extendsMap)) {
- $this->matchExtends($s, $selectors);
-
- // remove duplicates
- array_walk($selectors, function (&$value) {
- $value = serialize($value);
- });
-
- $selectors = array_unique($selectors);
-
- array_walk($selectors, function (&$value) {
- $value = unserialize($value);
- });
- }
- }
-
- $block->selectors = [];
- $placeholderSelector = false;
-
- foreach ($selectors as $selector) {
- if ($this->hasSelectorPlaceholder($selector)) {
- $placeholderSelector = true;
- continue;
- }
-
- $block->selectors[] = $this->compileSelector($selector);
- }
-
- if ($placeholderSelector && 0 === \count($block->selectors) && null !== $parentKey) {
- assert($block->parent !== null);
- unset($block->parent->children[$parentKey]);
-
- return;
- }
- }
-
- foreach ($block->children as $key => $child) {
- $this->flattenSelectors($child, $key);
- }
- }
-
- /**
- * Glue parts of :not( or :nth-child( ... that are in general split in selectors parts
- *
- * @param array $parts
- *
- * @return array
- */
- protected function glueFunctionSelectors($parts)
- {
- $new = [];
-
- foreach ($parts as $part) {
- if (\is_array($part)) {
- $part = $this->glueFunctionSelectors($part);
- $new[] = $part;
- } else {
- // a selector part finishing with a ) is the last part of a :not( or :nth-child(
- // and need to be joined to this
- if (
- \count($new) && \is_string($new[\count($new) - 1]) &&
- \strlen($part) && substr($part, -1) === ')' && strpos($part, '(') === false
- ) {
- while (\count($new) > 1 && substr($new[\count($new) - 1], -1) !== '(') {
- $part = array_pop($new) . $part;
- }
- $new[\count($new) - 1] .= $part;
- } else {
- $new[] = $part;
- }
- }
- }
-
- return $new;
- }
-
- /**
- * Match extends
- *
- * @param array $selector
- * @param array $out
- * @param int $from
- * @param bool $initial
- *
- * @return void
- */
- protected function matchExtends($selector, &$out, $from = 0, $initial = true)
- {
- static $partsPile = [];
- $selector = $this->glueFunctionSelectors($selector);
-
- if (\count($selector) == 1 && \in_array(reset($selector), $partsPile)) {
- return;
- }
-
- $outRecurs = [];
-
- foreach ($selector as $i => $part) {
- if ($i < $from) {
- continue;
- }
-
- // check that we are not building an infinite loop of extensions
- // if the new part is just including a previous part don't try to extend anymore
- if (\count($part) > 1) {
- foreach ($partsPile as $previousPart) {
- if (! \count(array_diff($previousPart, $part))) {
- continue 2;
- }
- }
- }
-
- $partsPile[] = $part;
-
- if ($this->matchExtendsSingle($part, $origin, $initial)) {
- $after = \array_slice($selector, $i + 1);
- $before = \array_slice($selector, 0, $i);
- list($before, $nonBreakableBefore) = $this->extractRelationshipFromFragment($before);
-
- foreach ($origin as $new) {
- $k = 0;
-
- // remove shared parts
- if (\count($new) > 1) {
- while ($k < $i && isset($new[$k]) && $selector[$k] === $new[$k]) {
- $k++;
- }
- }
-
- if (\count($nonBreakableBefore) && $k === \count($new)) {
- $k--;
- }
-
- $replacement = [];
- $tempReplacement = $k > 0 ? \array_slice($new, $k) : $new;
-
- for ($l = \count($tempReplacement) - 1; $l >= 0; $l--) {
- $slice = [];
-
- foreach ($tempReplacement[$l] as $chunk) {
- if (! \in_array($chunk, $slice)) {
- $slice[] = $chunk;
- }
- }
-
- array_unshift($replacement, $slice);
-
- if (! $this->isImmediateRelationshipCombinator(end($slice))) {
- break;
- }
- }
-
- $afterBefore = $l != 0 ? \array_slice($tempReplacement, 0, $l) : [];
-
- // Merge shared direct relationships.
- $mergedBefore = $this->mergeDirectRelationships($afterBefore, $nonBreakableBefore);
-
- $result = array_merge(
- $before,
- $mergedBefore,
- $replacement,
- $after
- );
-
- if ($result === $selector) {
- continue;
- }
-
- $this->pushOrMergeExtentedSelector($out, $result);
-
- // recursively check for more matches
- $startRecurseFrom = \count($before) + min(\count($nonBreakableBefore), \count($mergedBefore));
-
- if (\count($origin) > 1) {
- $this->matchExtends($result, $out, $startRecurseFrom, false);
- } else {
- $this->matchExtends($result, $outRecurs, $startRecurseFrom, false);
- }
-
- // selector sequence merging
- if (! empty($before) && \count($new) > 1) {
- $preSharedParts = $k > 0 ? \array_slice($before, 0, $k) : [];
- $postSharedParts = $k > 0 ? \array_slice($before, $k) : $before;
-
- list($betweenSharedParts, $nonBreakabl2) = $this->extractRelationshipFromFragment($afterBefore);
-
- $result2 = array_merge(
- $preSharedParts,
- $betweenSharedParts,
- $postSharedParts,
- $nonBreakabl2,
- $nonBreakableBefore,
- $replacement,
- $after
- );
-
- $this->pushOrMergeExtentedSelector($out, $result2);
- }
- }
- }
- array_pop($partsPile);
- }
-
- while (\count($outRecurs)) {
- $result = array_shift($outRecurs);
- $this->pushOrMergeExtentedSelector($out, $result);
- }
- }
-
- /**
- * Test a part for being a pseudo selector
- *
- * @param string $part
- * @param array $matches
- *
- * @return bool
- */
- protected function isPseudoSelector($part, &$matches)
- {
- if (
- strpos($part, ':') === 0 &&
- preg_match(",^::?([\w-]+)\((.+)\)$,", $part, $matches)
- ) {
- return true;
- }
-
- return false;
- }
-
- /**
- * Push extended selector except if
- * - this is a pseudo selector
- * - same as previous
- * - in a white list
- * in this case we merge the pseudo selector content
- *
- * @param array $out
- * @param array $extended
- *
- * @return void
- */
- protected function pushOrMergeExtentedSelector(&$out, $extended)
- {
- if (\count($out) && \count($extended) === 1 && \count(reset($extended)) === 1) {
- $single = reset($extended);
- $part = reset($single);
-
- if (
- $this->isPseudoSelector($part, $matchesExtended) &&
- \in_array($matchesExtended[1], [ 'slotted' ])
- ) {
- $prev = end($out);
- $prev = $this->glueFunctionSelectors($prev);
-
- if (\count($prev) === 1 && \count(reset($prev)) === 1) {
- $single = reset($prev);
- $part = reset($single);
-
- if (
- $this->isPseudoSelector($part, $matchesPrev) &&
- $matchesPrev[1] === $matchesExtended[1]
- ) {
- $extended = explode($matchesExtended[1] . '(', $matchesExtended[0], 2);
- $extended[1] = $matchesPrev[2] . ', ' . $extended[1];
- $extended = implode($matchesExtended[1] . '(', $extended);
- $extended = [ [ $extended ]];
- array_pop($out);
- }
- }
- }
- }
- $out[] = $extended;
- }
-
- /**
- * Match extends single
- *
- * @param array $rawSingle
- * @param array $outOrigin
- * @param bool $initial
- *
- * @return bool
- */
- protected function matchExtendsSingle($rawSingle, &$outOrigin, $initial = true)
- {
- $counts = [];
- $single = [];
-
- // simple usual cases, no need to do the whole trick
- if (\in_array($rawSingle, [['>'],['+'],['~']])) {
- return false;
- }
-
- foreach ($rawSingle as $part) {
- // matches Number
- if (! \is_string($part)) {
- return false;
- }
-
- if (! preg_match('/^[\[.:#%]/', $part) && \count($single)) {
- $single[\count($single) - 1] .= $part;
- } else {
- $single[] = $part;
- }
- }
-
- $extendingDecoratedTag = false;
-
- if (\count($single) > 1) {
- $matches = null;
- $extendingDecoratedTag = preg_match('/^[a-z0-9]+$/i', $single[0], $matches) ? $matches[0] : false;
- }
-
- $outOrigin = [];
- $found = false;
-
- foreach ($single as $k => $part) {
- if (isset($this->extendsMap[$part])) {
- foreach ($this->extendsMap[$part] as $idx) {
- $counts[$idx] = isset($counts[$idx]) ? $counts[$idx] + 1 : 1;
- }
- }
-
- if (
- $initial &&
- $this->isPseudoSelector($part, $matches) &&
- ! \in_array($matches[1], [ 'not' ])
- ) {
- $buffer = $matches[2];
- $parser = $this->parserFactory(__METHOD__);
-
- if ($parser->parseSelector($buffer, $subSelectors, false)) {
- foreach ($subSelectors as $ksub => $subSelector) {
- $subExtended = [];
- $this->matchExtends($subSelector, $subExtended, 0, false);
-
- if ($subExtended) {
- $subSelectorsExtended = $subSelectors;
- $subSelectorsExtended[$ksub] = $subExtended;
-
- foreach ($subSelectorsExtended as $ksse => $sse) {
- $subSelectorsExtended[$ksse] = $this->collapseSelectors($sse);
- }
-
- $subSelectorsExtended = implode(', ', $subSelectorsExtended);
- $singleExtended = $single;
- $singleExtended[$k] = str_replace('(' . $buffer . ')', "($subSelectorsExtended)", $part);
- $outOrigin[] = [ $singleExtended ];
- $found = true;
- }
- }
- }
- }
- }
-
- foreach ($counts as $idx => $count) {
- list($target, $origin, /* $block */) = $this->extends[$idx];
-
- $origin = $this->glueFunctionSelectors($origin);
-
- // check count
- if ($count !== \count($target)) {
- continue;
- }
-
- $this->extends[$idx][3] = true;
-
- $rem = array_diff($single, $target);
-
- foreach ($origin as $j => $new) {
- // prevent infinite loop when target extends itself
- if ($this->isSelfExtend($single, $origin) && ! $initial) {
- return false;
- }
-
- $replacement = end($new);
-
- // Extending a decorated tag with another tag is not possible.
- if (
- $extendingDecoratedTag && $replacement[0] != $extendingDecoratedTag &&
- preg_match('/^[a-z0-9]+$/i', $replacement[0])
- ) {
- unset($origin[$j]);
- continue;
- }
-
- $combined = $this->combineSelectorSingle($replacement, $rem);
-
- if (\count(array_diff($combined, $origin[$j][\count($origin[$j]) - 1]))) {
- $origin[$j][\count($origin[$j]) - 1] = $combined;
- }
- }
-
- $outOrigin = array_merge($outOrigin, $origin);
-
- $found = true;
- }
-
- return $found;
- }
-
- /**
- * Extract a relationship from the fragment.
- *
- * When extracting the last portion of a selector we will be left with a
- * fragment which may end with a direction relationship combinator. This
- * method will extract the relationship fragment and return it along side
- * the rest.
- *
- * @param array $fragment The selector fragment maybe ending with a direction relationship combinator.
- *
- * @return array The selector without the relationship fragment if any, the relationship fragment.
- */
- protected function extractRelationshipFromFragment(array $fragment)
- {
- $parents = [];
- $children = [];
-
- $j = $i = \count($fragment);
-
- for (;;) {
- $children = $j != $i ? \array_slice($fragment, $j, $i - $j) : [];
- $parents = \array_slice($fragment, 0, $j);
- $slice = end($parents);
-
- if (empty($slice) || ! $this->isImmediateRelationshipCombinator($slice[0])) {
- break;
- }
-
- $j -= 2;
- }
-
- return [$parents, $children];
- }
-
- /**
- * Combine selector single
- *
- * @param array $base
- * @param array $other
- *
- * @return array
- */
- protected function combineSelectorSingle($base, $other)
- {
- $tag = [];
- $out = [];
- $wasTag = false;
- $pseudo = [];
-
- while (\count($other) && strpos(end($other), ':') === 0) {
- array_unshift($pseudo, array_pop($other));
- }
-
- foreach ([array_reverse($base), array_reverse($other)] as $single) {
- $rang = count($single);
-
- foreach ($single as $part) {
- if (preg_match('/^[\[:]/', $part)) {
- $out[] = $part;
- $wasTag = false;
- } elseif (preg_match('/^[\.#]/', $part)) {
- array_unshift($out, $part);
- $wasTag = false;
- } elseif (preg_match('/^[^_-]/', $part) && $rang === 1) {
- $tag[] = $part;
- $wasTag = true;
- } elseif ($wasTag) {
- $tag[\count($tag) - 1] .= $part;
- } else {
- array_unshift($out, $part);
- }
- $rang--;
- }
- }
-
- if (\count($tag)) {
- array_unshift($out, $tag[0]);
- }
-
- while (\count($pseudo)) {
- $out[] = array_shift($pseudo);
- }
-
- return $out;
- }
-
- /**
- * Compile media
- *
- * @param \ScssPhp\ScssPhp\Block $media
- *
- * @return void
- */
- protected function compileMedia(Block $media)
- {
- assert($media instanceof MediaBlock);
- $this->pushEnv($media);
-
- $mediaQueries = $this->compileMediaQuery($this->multiplyMedia($this->env));
-
- if (! empty($mediaQueries)) {
- assert($this->scope !== null);
- $previousScope = $this->scope;
- $parentScope = $this->mediaParent($this->scope);
-
- foreach ($mediaQueries as $mediaQuery) {
- $this->scope = $this->makeOutputBlock(Type::T_MEDIA, [$mediaQuery]);
-
- $parentScope->children[] = $this->scope;
- $parentScope = $this->scope;
- }
-
- // top level properties in a media cause it to be wrapped
- $needsWrap = false;
-
- foreach ($media->children as $child) {
- $type = $child[0];
-
- if (
- $type !== Type::T_BLOCK &&
- $type !== Type::T_MEDIA &&
- $type !== Type::T_DIRECTIVE &&
- $type !== Type::T_IMPORT
- ) {
- $needsWrap = true;
- break;
- }
- }
-
- if ($needsWrap) {
- $wrapped = new Block();
- $wrapped->sourceName = $media->sourceName;
- $wrapped->sourceIndex = $media->sourceIndex;
- $wrapped->sourceLine = $media->sourceLine;
- $wrapped->sourceColumn = $media->sourceColumn;
- $wrapped->selectors = [];
- $wrapped->comments = [];
- $wrapped->parent = $media;
- $wrapped->children = $media->children;
-
- $media->children = [[Type::T_BLOCK, $wrapped]];
- }
-
- $this->compileChildrenNoReturn($media->children, $this->scope);
-
- $this->scope = $previousScope;
- }
-
- $this->popEnv();
- }
-
- /**
- * Media parent
- *
- * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $scope
- *
- * @return \ScssPhp\ScssPhp\Formatter\OutputBlock
- */
- protected function mediaParent(OutputBlock $scope)
- {
- while (! empty($scope->parent)) {
- if (! empty($scope->type) && $scope->type !== Type::T_MEDIA) {
- break;
- }
-
- $scope = $scope->parent;
- }
-
- return $scope;
- }
-
- /**
- * Compile directive
- *
- * @param DirectiveBlock|array $directive
- * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $out
- *
- * @return void
- */
- protected function compileDirective($directive, OutputBlock $out)
- {
- if (\is_array($directive)) {
- $directiveName = $this->compileDirectiveName($directive[0]);
- $s = '@' . $directiveName;
-
- if (! empty($directive[1])) {
- $s .= ' ' . $this->compileValue($directive[1]);
- }
- // sass-spec compliance on newline after directives, a bit tricky :/
- $appendNewLine = (! empty($directive[2]) || strpos($s, "\n")) ? "\n" : "";
- if (\is_array($directive[0]) && empty($directive[1])) {
- $appendNewLine = "\n";
- }
-
- if (empty($directive[3])) {
- $this->appendRootDirective($s . ';' . $appendNewLine, $out, [Type::T_COMMENT, Type::T_DIRECTIVE]);
- } else {
- $this->appendOutputLine($out, Type::T_DIRECTIVE, $s . ';');
- }
- } else {
- $directive->name = $this->compileDirectiveName($directive->name);
- $s = '@' . $directive->name;
-
- if (! empty($directive->value)) {
- $s .= ' ' . $this->compileValue($directive->value);
- }
-
- if ($directive->name === 'keyframes' || substr($directive->name, -10) === '-keyframes') {
- $this->compileKeyframeBlock($directive, [$s]);
- } else {
- $this->compileNestedBlock($directive, [$s]);
- }
- }
- }
-
- /**
- * directive names can include some interpolation
- *
- * @param string|array $directiveName
- * @return string
- * @throws CompilerException
- */
- protected function compileDirectiveName($directiveName)
- {
- if (is_string($directiveName)) {
- return $directiveName;
- }
-
- return $this->compileValue($directiveName);
- }
-
- /**
- * Compile at-root
- *
- * @param \ScssPhp\ScssPhp\Block $block
- *
- * @return void
- */
- protected function compileAtRoot(Block $block)
- {
- assert($block instanceof AtRootBlock);
- $env = $this->pushEnv($block);
- $envs = $this->compactEnv($env);
- list($with, $without) = $this->compileWith(isset($block->with) ? $block->with : null);
-
- // wrap inline selector
- if ($block->selector) {
- $wrapped = new Block();
- $wrapped->sourceName = $block->sourceName;
- $wrapped->sourceIndex = $block->sourceIndex;
- $wrapped->sourceLine = $block->sourceLine;
- $wrapped->sourceColumn = $block->sourceColumn;
- $wrapped->selectors = $block->selector;
- $wrapped->comments = [];
- $wrapped->parent = $block;
- $wrapped->children = $block->children;
- $wrapped->selfParent = $block->selfParent;
-
- $block->children = [[Type::T_BLOCK, $wrapped]];
- $block->selector = null;
- }
-
- $selfParent = $block->selfParent;
- assert($selfParent !== null, 'at-root blocks must have a selfParent set.');
-
- if (
- ! $selfParent->selectors &&
- isset($block->parent) &&
- isset($block->parent->selectors) && $block->parent->selectors
- ) {
- $selfParent = $block->parent;
- }
-
- $this->env = $this->filterWithWithout($envs, $with, $without);
-
- assert($this->scope !== null);
- $saveScope = $this->scope;
- $this->scope = $this->filterScopeWithWithout($saveScope, $with, $without);
-
- // propagate selfParent to the children where they still can be useful
- $this->compileChildrenNoReturn($block->children, $this->scope, $selfParent);
-
- assert($this->scope !== null);
- $this->completeScope($this->scope, $saveScope);
- $this->scope = $saveScope;
- $this->env = $this->extractEnv($envs);
-
- $this->popEnv();
- }
-
- /**
- * Filter at-root scope depending on with/without option
- *
- * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $scope
- * @param array $with
- * @param array $without
- *
- * @return OutputBlock
- */
- protected function filterScopeWithWithout($scope, $with, $without)
- {
- $filteredScopes = [];
- $childStash = [];
-
- if ($scope->type === Type::T_ROOT) {
- return $scope;
- }
- assert($this->rootBlock !== null);
-
- // start from the root
- while ($scope->parent && $scope->parent->type !== Type::T_ROOT) {
- array_unshift($childStash, $scope);
- \assert($scope->parent !== null);
- $scope = $scope->parent;
- }
-
- for (;;) {
- if (! $scope) {
- break;
- }
-
- if ($this->isWith($scope, $with, $without)) {
- $s = clone $scope;
- $s->children = [];
- $s->lines = [];
- $s->parent = null;
-
- if ($s->type !== Type::T_MEDIA && $s->type !== Type::T_DIRECTIVE) {
- $s->selectors = [];
- }
-
- $filteredScopes[] = $s;
- }
-
- if (\count($childStash)) {
- $scope = array_shift($childStash);
- } elseif ($scope->children) {
- $scope = end($scope->children);
- } else {
- $scope = null;
- }
- }
-
- if (! \count($filteredScopes)) {
- return $this->rootBlock;
- }
-
- $newScope = array_shift($filteredScopes);
- $newScope->parent = $this->rootBlock;
-
- $this->rootBlock->children[] = $newScope;
-
- $p = &$newScope;
-
- while (\count($filteredScopes)) {
- $s = array_shift($filteredScopes);
- $s->parent = $p;
- $p->children[] = $s;
- $newScope = &$p->children[0];
- $p = &$p->children[0];
- }
-
- return $newScope;
- }
-
- /**
- * 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 \ScssPhp\ScssPhp\Formatter\OutputBlock $scope
- * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $previousScope
- *
- * @return OutputBlock
- */
- protected function completeScope($scope, $previousScope)
- {
- if (! $scope->type && ! $scope->selectors && \count($scope->lines)) {
- $scope->selectors = $this->findScopeSelectors($previousScope, $scope->depth);
- }
-
- if ($scope->children) {
- foreach ($scope->children as $k => $c) {
- $scope->children[$k] = $this->completeScope($c, $previousScope);
- }
- }
-
- return $scope;
- }
-
- /**
- * Find a selector by the depth node in the scope
- *
- * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $scope
- * @param int $depth
- *
- * @return array
- */
- protected function findScopeSelectors($scope, $depth)
- {
- if ($scope->depth === $depth && $scope->selectors) {
- return $scope->selectors;
- }
-
- if ($scope->children) {
- foreach (array_reverse($scope->children) as $c) {
- if ($s = $this->findScopeSelectors($c, $depth)) {
- return $s;
- }
- }
- }
-
- return [];
- }
-
- /**
- * Compile @at-root's with: inclusion / without: exclusion into 2 lists uses to filter scope/env later
- *
- * @param array|null $withCondition
- *
- * @return array
- *
- * @phpstan-return array{array<string, bool>, array<string, bool>}
- */
- protected function compileWith($withCondition)
- {
- // just compile what we have in 2 lists
- $with = [];
- $without = ['rule' => true];
-
- if ($withCondition) {
- if ($withCondition[0] === Type::T_INTERPOLATE) {
- $w = $this->compileValue($withCondition);
-
- $buffer = "($w)";
- $parser = $this->parserFactory(__METHOD__);
-
- if ($parser->parseValue($buffer, $reParsedWith)) {
- $withCondition = $reParsedWith;
- }
- }
-
- $withConfig = $this->mapGet($withCondition, static::$with);
- if ($withConfig !== null) {
- $without = []; // cancel the default
- $list = $this->coerceList($withConfig);
-
- foreach ($list[2] as $item) {
- $keyword = $this->compileStringContent($this->coerceString($item));
-
- $with[$keyword] = true;
- }
- }
-
- $withoutConfig = $this->mapGet($withCondition, static::$without);
- if ($withoutConfig !== null) {
- $without = []; // cancel the default
- $list = $this->coerceList($withoutConfig);
-
- foreach ($list[2] as $item) {
- $keyword = $this->compileStringContent($this->coerceString($item));
-
- $without[$keyword] = true;
- }
- }
- }
-
- return [$with, $without];
- }
-
- /**
- * Filter env stack
- *
- * @param Environment[] $envs
- * @param array $with
- * @param array $without
- *
- * @return Environment
- *
- * @phpstan-param non-empty-array<Environment> $envs
- */
- protected function filterWithWithout($envs, $with, $without)
- {
- $filtered = [];
-
- foreach ($envs as $e) {
- if ($e->block && ! $this->isWith($e->block, $with, $without)) {
- $ec = clone $e;
- $ec->block = null;
- $ec->selectors = [];
-
- $filtered[] = $ec;
- } else {
- $filtered[] = $e;
- }
- }
-
- return $this->extractEnv($filtered);
- }
-
- /**
- * Filter WITH rules
- *
- * @param \ScssPhp\ScssPhp\Block|\ScssPhp\ScssPhp\Formatter\OutputBlock $block
- * @param array $with
- * @param array $without
- *
- * @return bool
- */
- protected function isWith($block, $with, $without)
- {
- if (isset($block->type)) {
- if ($block->type === Type::T_MEDIA) {
- return $this->testWithWithout('media', $with, $without);
- }
-
- if ($block->type === Type::T_DIRECTIVE) {
- assert($block instanceof DirectiveBlock || $block instanceof OutputBlock);
- if (isset($block->name)) {
- return $this->testWithWithout($this->compileDirectiveName($block->name), $with, $without);
- } elseif (isset($block->selectors) && preg_match(',@(\w+),ims', json_encode($block->selectors), $m)) {
- return $this->testWithWithout($m[1], $with, $without);
- } else {
- return $this->testWithWithout('???', $with, $without);
- }
- }
- } elseif (isset($block->selectors)) {
- // a selector starting with number is a keyframe rule
- if (\count($block->selectors)) {
- $s = reset($block->selectors);
-
- while (\is_array($s)) {
- $s = reset($s);
- }
-
- if (\is_object($s) && $s instanceof Number) {
- return $this->testWithWithout('keyframes', $with, $without);
- }
- }
-
- return $this->testWithWithout('rule', $with, $without);
- }
-
- return true;
- }
-
- /**
- * Test a single type of block against with/without lists
- *
- * @param string $what
- * @param array $with
- * @param array $without
- *
- * @return bool
- * true if the block should be kept, false to reject
- */
- protected function testWithWithout($what, $with, $without)
- {
- // if without, reject only if in the list (or 'all' is in the list)
- if (\count($without)) {
- return (isset($without[$what]) || isset($without['all'])) ? false : true;
- }
-
- // otherwise reject all what is not in the with list
- return (isset($with[$what]) || isset($with['all'])) ? true : false;
- }
-
-
- /**
- * Compile keyframe block
- *
- * @param \ScssPhp\ScssPhp\Block $block
- * @param string[] $selectors
- *
- * @return void
- */
- protected function compileKeyframeBlock(Block $block, $selectors)
- {
- $env = $this->pushEnv($block);
-
- $envs = $this->compactEnv($env);
-
- $this->env = $this->extractEnv(array_filter($envs, function (Environment $e) {
- return ! isset($e->block->selectors);
- }));
-
- $this->scope = $this->makeOutputBlock($block->type, $selectors);
- $this->scope->depth = 1;
- assert($this->scope->parent !== null);
- $this->scope->parent->children[] = $this->scope;
-
- $this->compileChildrenNoReturn($block->children, $this->scope);
-
- assert($this->scope !== null);
- $this->scope = $this->scope->parent;
- $this->env = $this->extractEnv($envs);
-
- $this->popEnv();
- }
-
- /**
- * Compile nested properties lines
- *
- * @param \ScssPhp\ScssPhp\Block $block
- * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $out
- *
- * @return void
- */
- protected function compileNestedPropertiesBlock(Block $block, OutputBlock $out)
- {
- assert($block instanceof NestedPropertyBlock);
- $prefix = $this->compileValue($block->prefix) . '-';
-
- $nested = $this->makeOutputBlock($block->type);
- $nested->parent = $out;
-
- if ($block->hasValue) {
- $nested->depth = $out->depth + 1;
- }
-
- $out->children[] = $nested;
-
- foreach ($block->children as $child) {
- switch ($child[0]) {
- case Type::T_ASSIGN:
- array_unshift($child[1][2], $prefix);
- break;
-
- case Type::T_NESTED_PROPERTY:
- assert($child[1] instanceof NestedPropertyBlock);
- array_unshift($child[1]->prefix[2], $prefix);
- break;
- }
-
- $this->compileChild($child, $nested);
- }
- }
-
- /**
- * Compile nested block
- *
- * @param \ScssPhp\ScssPhp\Block $block
- * @param string[] $selectors
- *
- * @return void
- */
- protected function compileNestedBlock(Block $block, $selectors)
- {
- $this->pushEnv($block);
-
- $this->scope = $this->makeOutputBlock($block->type, $selectors);
- assert($this->scope->parent !== null);
- $this->scope->parent->children[] = $this->scope;
-
- // wrap assign children in a block
- // except for @font-face
- if (!$block instanceof DirectiveBlock || $this->compileDirectiveName($block->name) !== 'font-face') {
- // need wrapping?
- $needWrapping = false;
-
- foreach ($block->children as $child) {
- if ($child[0] === Type::T_ASSIGN) {
- $needWrapping = true;
- break;
- }
- }
-
- if ($needWrapping) {
- $wrapped = new Block();
- $wrapped->sourceName = $block->sourceName;
- $wrapped->sourceIndex = $block->sourceIndex;
- $wrapped->sourceLine = $block->sourceLine;
- $wrapped->sourceColumn = $block->sourceColumn;
- $wrapped->selectors = [];
- $wrapped->comments = [];
- $wrapped->parent = $block;
- $wrapped->children = $block->children;
- $wrapped->selfParent = $block->selfParent;
-
- $block->children = [[Type::T_BLOCK, $wrapped]];
- }
- }
-
- $this->compileChildrenNoReturn($block->children, $this->scope);
-
- assert($this->scope !== null);
- $this->scope = $this->scope->parent;
-
- $this->popEnv();
- }
-
- /**
- * Recursively compiles a block.
- *
- * A block is analogous to a CSS block in most cases. A single SCSS document
- * is encapsulated in a block when parsed, but it does not have parent tags
- * so all of its children appear on the root level when compiled.
- *
- * Blocks are made up of selectors and children.
- *
- * The children of a block are just all the blocks that are defined within.
- *
- * Compiling the block involves pushing a fresh environment on the stack,
- * and iterating through the props, compiling each one.
- *
- * @see Compiler::compileChild()
- *
- * @param \ScssPhp\ScssPhp\Block $block
- *
- * @return void
- */
- protected function compileBlock(Block $block)
- {
- $env = $this->pushEnv($block);
- assert($block->selectors !== null);
- $env->selectors = $this->evalSelectors($block->selectors);
-
- $out = $this->makeOutputBlock(null);
-
- assert($this->scope !== null);
- $this->scope->children[] = $out;
-
- if (\count($block->children)) {
- $out->selectors = $this->multiplySelectors($env, $block->selfParent);
-
- // propagate selfParent to the children where they still can be useful
- $selfParentSelectors = null;
-
- if (isset($block->selfParent->selectors)) {
- $selfParentSelectors = $block->selfParent->selectors;
- $block->selfParent->selectors = $out->selectors;
- }
-
- $this->compileChildrenNoReturn($block->children, $out, $block->selfParent);
-
- // and revert for the following children of the same block
- if ($selfParentSelectors) {
- assert($block->selfParent !== null);
- $block->selfParent->selectors = $selfParentSelectors;
- }
- }
-
- $this->popEnv();
- }
-
-
- /**
- * Compile the value of a comment that can have interpolation
- *
- * @param array $value
- * @param bool $pushEnv
- *
- * @return string
- */
- protected function compileCommentValue($value, $pushEnv = false)
- {
- $c = $value[1];
-
- if (isset($value[2])) {
- if ($pushEnv) {
- $this->pushEnv();
- }
-
- 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();
- }
- }
-
- return $c;
- }
-
- /**
- * Compile root level comment
- *
- * @param array $block
- *
- * @return void
- */
- protected function compileComment($block)
- {
- $out = $this->makeOutputBlock(Type::T_COMMENT);
- $out->lines[] = $this->compileCommentValue($block, true);
-
- assert($this->scope !== null);
- $this->scope->children[] = $out;
- }
-
- /**
- * Evaluate selectors
- *
- * @param array $selectors
- *
- * @return array
- */
- protected function evalSelectors($selectors)
- {
- $this->shouldEvaluate = false;
-
- $evaluatedSelectors = [];
- foreach ($selectors as $selector) {
- $evaluatedSelectors[] = $this->evalSelector($selector);
- }
- $selectors = $evaluatedSelectors;
-
- // after evaluating interpolates, we might need a second pass
- if ($this->shouldEvaluate) {
- $selectors = $this->replaceSelfSelector($selectors, '&');
- $buffer = $this->collapseSelectors($selectors);
- $parser = $this->parserFactory(__METHOD__);
-
- try {
- $isValid = $parser->parseSelector($buffer, $newSelectors, true);
- } catch (ParserException $e) {
- throw $this->error($e->getMessage());
- }
-
- if ($isValid) {
- $selectors = array_map([$this, 'evalSelector'], $newSelectors);
- }
- }
-
- return $selectors;
- }
-
- /**
- * Evaluate selector
- *
- * @param array $selector
- *
- * @return array
- *
- * @phpstan-impure
- */
- protected function evalSelector($selector)
- {
- return array_map([$this, 'evalSelectorPart'], $selector);
- }
-
- /**
- * Evaluate selector part; replaces all the interpolates, stripping quotes
- *
- * @param array $part
- *
- * @return array
- *
- * @phpstan-impure
- */
- protected function evalSelectorPart($part)
- {
- foreach ($part as &$p) {
- if (\is_array($p) && ($p[0] === Type::T_INTERPOLATE || $p[0] === Type::T_STRING)) {
- $p = $this->compileValue($p);
-
- // force re-evaluation if self char or non standard char
- if (preg_match(',[^\w-],', $p)) {
- $this->shouldEvaluate = true;
- }
- } elseif (
- \is_string($p) && \strlen($p) >= 2 &&
- ($p[0] === '"' || $p[0] === "'") &&
- substr($p, -1) === $p[0]
- ) {
- $p = substr($p, 1, -1);
- }
- }
-
- return $this->flattenSelectorSingle($part);
- }
-
- /**
- * Collapse selectors
- *
- * @param array $selectors
- *
- * @return string
- */
- protected function collapseSelectors($selectors)
- {
- $parts = [];
-
- foreach ($selectors as $selector) {
- $output = [];
-
- foreach ($selector as $node) {
- $compound = '';
-
- if (!is_array($node)) {
- $output[] = $node;
- continue;
- }
-
- array_walk_recursive(
- $node,
- function ($value, $key) use (&$compound) {
- $compound .= $value;
- }
- );
-
- $output[] = $compound;
- }
-
- $parts[] = implode(' ', $output);
- }
-
- return implode(', ', $parts);
- }
-
- /**
- * Collapse selectors
- *
- * @param array $selectors
- *
- * @return array
- */
- private function collapseSelectorsAsList($selectors)
- {
- $parts = [];
-
- foreach ($selectors as $selector) {
- $output = [];
- $glueNext = false;
-
- foreach ($selector as $node) {
- $compound = '';
-
- 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)) {
- $output[\count($output) - 1] .= ' ' . $compound;
- } else {
- $output[] = $compound;
- }
-
- $glueNext = true;
- } elseif ($glueNext) {
- $output[\count($output) - 1] .= ' ' . $compound;
- $glueNext = false;
- } else {
- $output[] = $compound;
- }
- }
-
- foreach ($output as &$o) {
- $o = [Type::T_STRING, '', [$o]];
- }
-
- $parts[] = [Type::T_LIST, ' ', $output];
- }
-
- return [Type::T_LIST, ',', $parts];
- }
-
- /**
- * Parse down the selector and revert [self] to "&" before a reparsing
- *
- * @param array $selectors
- * @param string|null $replace
- *
- * @return array
- */
- protected function replaceSelfSelector($selectors, $replace = null)
- {
- foreach ($selectors as &$part) {
- if (\is_array($part)) {
- if ($part === [Type::T_SELF]) {
- if (\is_null($replace)) {
- $replace = $this->reduce([Type::T_SELF]);
- $replace = $this->compileValue($replace);
- }
- $part = $replace;
- } else {
- $part = $this->replaceSelfSelector($part, $replace);
- }
- }
- }
-
- return $selectors;
- }
-
- /**
- * Flatten selector single; joins together .classes and #ids
- *
- * @param array $single
- *
- * @return array
- */
- protected function flattenSelectorSingle($single)
- {
- $joined = [];
-
- foreach ($single as $part) {
- if (
- empty($joined) ||
- ! \is_string($part) ||
- preg_match('/[\[.:#%]/', $part)
- ) {
- $joined[] = $part;
- continue;
- }
-
- if (\is_array(end($joined))) {
- $joined[] = $part;
- } else {
- $joined[\count($joined) - 1] .= $part;
- }
- }
-
- return $joined;
- }
-
- /**
- * Compile selector to string; self(&) should have been replaced by now
- *
- * @param string|array $selector
- *
- * @return string
- */
- protected function compileSelector($selector)
- {
- if (! \is_array($selector)) {
- return $selector; // media and the like
- }
-
- return implode(
- ' ',
- array_map(
- [$this, 'compileSelectorPart'],
- $selector
- )
- );
- }
-
- /**
- * Compile selector part
- *
- * @param array $piece
- *
- * @return string
- */
- protected function compileSelectorPart($piece)
- {
- foreach ($piece as &$p) {
- if (! \is_array($p)) {
- continue;
- }
-
- switch ($p[0]) {
- case Type::T_SELF:
- $p = '&';
- break;
-
- default:
- $p = $this->compileValue($p);
- break;
- }
- }
-
- return implode($piece);
- }
-
- /**
- * Has selector placeholder?
- *
- * @param array $selector
- *
- * @return bool
- */
- protected function hasSelectorPlaceholder($selector)
- {
- if (! \is_array($selector)) {
- return false;
- }
-
- foreach ($selector as $parts) {
- foreach ($parts as $part) {
- if (\strlen($part) && '%' === $part[0]) {
- return true;
- }
- }
- }
-
- return false;
- }
-
- /**
- * @param string $name
- *
- * @return void
- */
- protected function pushCallStack($name = '')
- {
- $this->callStack[] = [
- 'n' => $name,
- Parser::SOURCE_INDEX => $this->sourceIndex,
- Parser::SOURCE_LINE => $this->sourceLine,
- Parser::SOURCE_COLUMN => $this->sourceColumn
- ];
-
- // infinite calling loop
- if (\count($this->callStack) > 25000) {
- // not displayed but you can var_dump it to deep debug
- $msg = $this->callStackMessage(true, 100);
- $msg = 'Infinite calling loop';
-
- throw $this->error($msg);
- }
- }
-
- /**
- * @return void
- */
- protected function popCallStack()
- {
- array_pop($this->callStack);
- }
-
- /**
- * Compile children and return result
- *
- * @param array $stms
- * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $out
- * @param string $traceName
- *
- * @return array|Number|null
- */
- protected function compileChildren($stms, OutputBlock $out, $traceName = '')
- {
- $this->pushCallStack($traceName);
-
- foreach ($stms as $stm) {
- $ret = $this->compileChild($stm, $out);
-
- if (isset($ret)) {
- $this->popCallStack();
-
- return $ret;
- }
- }
-
- $this->popCallStack();
-
- return null;
- }
-
- /**
- * Compile children and throw exception if unexpected at-return
- *
- * @param array[] $stms
- * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $out
- * @param \ScssPhp\ScssPhp\Block $selfParent
- * @param string $traceName
- *
- * @return void
- *
- * @throws \Exception
- */
- protected function compileChildrenNoReturn($stms, OutputBlock $out, $selfParent = null, $traceName = '')
- {
- $this->pushCallStack($traceName);
-
- foreach ($stms as $stm) {
- if ($selfParent && isset($stm[1]) && \is_object($stm[1]) && $stm[1] instanceof Block) {
- $oldSelfParent = $stm[1]->selfParent;
- $stm[1]->selfParent = $selfParent;
- $ret = $this->compileChild($stm, $out);
- $stm[1]->selfParent = $oldSelfParent;
- } elseif ($selfParent && \in_array($stm[0], [Type::T_INCLUDE, Type::T_EXTEND])) {
- $stm['selfParent'] = $selfParent;
- $ret = $this->compileChild($stm, $out);
- } else {
- $ret = $this->compileChild($stm, $out);
- }
-
- if (isset($ret)) {
- throw $this->error('@return may only be used within a function');
- }
- }
-
- $this->popCallStack();
- }
-
-
- /**
- * evaluate media query : compile internal value keeping the structure unchanged
- *
- * @param array $queryList
- *
- * @return array
- */
- protected function evaluateMediaQuery($queryList)
- {
- static $parser = null;
-
- $outQueryList = [];
-
- foreach ($queryList as $kql => $query) {
- $shouldReparse = false;
-
- foreach ($query as $kq => $q) {
- for ($i = 1; $i < \count($q); $i++) {
- $value = $this->compileValue($q[$i]);
-
- // the parser had no mean to know if media type or expression if it was an interpolation
- // so you need to reparse if the T_MEDIA_TYPE looks like anything else a media type
- if (
- $q[0] == Type::T_MEDIA_TYPE &&
- (strpos($value, '(') !== false ||
- strpos($value, ')') !== false ||
- strpos($value, ':') !== false ||
- strpos($value, ',') !== false)
- ) {
- $shouldReparse = true;
- }
-
- $queryList[$kql][$kq][$i] = [Type::T_KEYWORD, $value];
- }
- }
-
- if ($shouldReparse) {
- if (\is_null($parser)) {
- $parser = $this->parserFactory(__METHOD__);
- }
-
- $queryString = $this->compileMediaQuery([$queryList[$kql]]);
- $queryString = reset($queryString);
-
- if ($queryString !== false && strpos($queryString, '@media ') === 0) {
- $queryString = substr($queryString, 7);
- $queries = [];
-
- if ($parser->parseMediaQueryList($queryString, $queries)) {
- $queries = $this->evaluateMediaQuery($queries[2]);
-
- while (\count($queries)) {
- $outQueryList[] = array_shift($queries);
- }
-
- continue;
- }
- }
- }
-
- $outQueryList[] = $queryList[$kql];
- }
-
- return $outQueryList;
- }
-
- /**
- * Compile media query
- *
- * @param array $queryList
- *
- * @return string[]
- */
- protected function compileMediaQuery($queryList)
- {
- $start = '@media ';
- $default = trim($start);
- $out = [];
- $current = '';
-
- foreach ($queryList as $query) {
- $type = null;
- $parts = [];
-
- $mediaTypeOnly = true;
-
- foreach ($query as $q) {
- if ($q[0] !== Type::T_MEDIA_TYPE) {
- $mediaTypeOnly = false;
- break;
- }
- }
-
- foreach ($query as $q) {
- switch ($q[0]) {
- case Type::T_MEDIA_TYPE:
- $newType = array_map([$this, 'compileValue'], \array_slice($q, 1));
-
- // combining not and anything else than media type is too risky and should be avoided
- if (! $mediaTypeOnly) {
- if (\in_array(Type::T_NOT, $newType) || ($type && \in_array(Type::T_NOT, $type) )) {
- if ($type) {
- array_unshift($parts, implode(' ', array_filter($type)));
- }
-
- if (! empty($parts)) {
- if (\strlen($current)) {
- $current .= $this->formatter->tagSeparator;
- }
-
- $current .= implode(' and ', $parts);
- }
-
- if ($current) {
- $out[] = $start . $current;
- }
-
- $current = '';
- $type = null;
- $parts = [];
- }
- }
-
- if ($newType === ['all'] && $default) {
- $default = $start . 'all';
- }
-
- // all can be safely ignored and mixed with whatever else
- if ($newType !== ['all']) {
- if ($type) {
- $type = $this->mergeMediaTypes($type, $newType);
-
- if (empty($type)) {
- // merge failed : ignore this query that is not valid, skip to the next one
- $parts = [];
- $default = ''; // if everything fail, no @media at all
- continue 3;
- }
- } else {
- $type = $newType;
- }
- }
- break;
-
- case Type::T_MEDIA_EXPRESSION:
- if (isset($q[2])) {
- $parts[] = '('
- . $this->compileValue($q[1])
- . $this->formatter->assignSeparator
- . $this->compileValue($q[2])
- . ')';
- } else {
- $parts[] = '('
- . $this->compileValue($q[1])
- . ')';
- }
- break;
-
- case Type::T_MEDIA_VALUE:
- $parts[] = $this->compileValue($q[1]);
- break;
- }
- }
-
- if ($type) {
- array_unshift($parts, implode(' ', array_filter($type)));
- }
-
- if (! empty($parts)) {
- if (\strlen($current)) {
- $current .= $this->formatter->tagSeparator;
- }
-
- $current .= implode(' and ', $parts);
- }
- }
-
- if ($current) {
- $out[] = $start . $current;
- }
-
- // no @media type except all, and no conflict?
- if (! $out && $default) {
- $out[] = $default;
- }
-
- return $out;
- }
-
- /**
- * Merge direct relationships between selectors
- *
- * @param array $selectors1
- * @param array $selectors2
- *
- * @return array
- */
- protected function mergeDirectRelationships($selectors1, $selectors2)
- {
- if (empty($selectors1) || empty($selectors2)) {
- return array_merge($selectors1, $selectors2);
- }
-
- $part1 = end($selectors1);
- $part2 = end($selectors2);
-
- if (! $this->isImmediateRelationshipCombinator($part1[0]) && $part1 !== $part2) {
- return array_merge($selectors1, $selectors2);
- }
-
- $merged = [];
-
- do {
- $part1 = array_pop($selectors1);
- $part2 = array_pop($selectors2);
-
- if (! $this->isImmediateRelationshipCombinator($part1[0]) && $part1 !== $part2) {
- if ($this->isImmediateRelationshipCombinator(reset($merged)[0])) {
- array_unshift($merged, [$part1[0] . $part2[0]]);
- $merged = array_merge($selectors1, $selectors2, $merged);
- } else {
- $merged = array_merge($selectors1, [$part1], $selectors2, [$part2], $merged);
- }
-
- break;
- }
-
- array_unshift($merged, $part1);
- } while (! empty($selectors1) && ! empty($selectors2));
-
- return $merged;
- }
-
- /**
- * Merge media types
- *
- * @param array $type1
- * @param array $type2
- *
- * @return array|null
- */
- protected function mergeMediaTypes($type1, $type2)
- {
- if (empty($type1)) {
- return $type2;
- }
-
- if (empty($type2)) {
- return $type1;
- }
-
- if (\count($type1) > 1) {
- $m1 = strtolower($type1[0]);
- $t1 = strtolower($type1[1]);
- } else {
- $m1 = '';
- $t1 = strtolower($type1[0]);
- }
-
- if (\count($type2) > 1) {
- $m2 = strtolower($type2[0]);
- $t2 = strtolower($type2[1]);
- } else {
- $m2 = '';
- $t2 = strtolower($type2[0]);
- }
-
- if (($m1 === Type::T_NOT) ^ ($m2 === Type::T_NOT)) {
- if ($t1 === $t2) {
- return null;
- }
-
- return [
- $m1 === Type::T_NOT ? $m2 : $m1,
- $m1 === Type::T_NOT ? $t2 : $t1,
- ];
- }
-
- if ($m1 === Type::T_NOT && $m2 === Type::T_NOT) {
- // CSS has no way of representing "neither screen nor print"
- if ($t1 !== $t2) {
- return null;
- }
-
- return [Type::T_NOT, $t1];
- }
-
- if ($t1 !== $t2) {
- return null;
- }
-
- // t1 == t2, neither m1 nor m2 are "not"
- return [empty($m1) ? $m2 : $m1, $t1];
- }
-
- /**
- * Compile import; returns true if the value was something that could be imported
- *
- * @param array $rawPath
- * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $out
- * @param bool $once
- *
- * @return bool
- */
- protected function compileImport($rawPath, OutputBlock $out, $once = false)
- {
- if ($rawPath[0] === Type::T_STRING) {
- $path = $this->compileStringContent($rawPath);
-
- if (strpos($path, 'url(') !== 0 && $filePath = $this->findImport($path, $this->currentDirectory)) {
- $this->registerImport($this->currentDirectory, $path, $filePath);
-
- if (! $once || ! \in_array($filePath, $this->importedFiles)) {
- $this->importFile($filePath, $out);
- $this->importedFiles[] = $filePath;
- }
-
- return true;
- }
-
- $this->appendRootDirective('@import ' . $this->compileImportPath($rawPath) . ';', $out);
-
- return false;
- }
-
- if ($rawPath[0] === Type::T_LIST) {
- // handle a list of strings
- if (\count($rawPath[2]) === 0) {
- return false;
- }
-
- foreach ($rawPath[2] as $path) {
- if ($path[0] !== Type::T_STRING) {
- $this->appendRootDirective('@import ' . $this->compileImportPath($rawPath) . ';', $out);
-
- return false;
- }
- }
-
- foreach ($rawPath[2] as $path) {
- $this->compileImport($path, $out, $once);
- }
-
- return true;
- }
-
- $this->appendRootDirective('@import ' . $this->compileImportPath($rawPath) . ';', $out);
-
- return false;
- }
-
- /**
- * @param array $rawPath
- * @return string
- * @throws CompilerException
- */
- protected function compileImportPath($rawPath)
- {
- $path = $this->compileValue($rawPath);
-
- // case url() without quotes : suppress \r \n remaining in the path
- // if this is a real string there can not be CR or LF char
- if (strpos($path, 'url(') === 0) {
- $path = str_replace(array("\r", "\n"), array('', ' '), $path);
- } else {
- // if this is a file name in a string, spaces should be escaped
- $path = $this->reduce($rawPath);
- $path = $this->escapeImportPathString($path);
- $path = $this->compileValue($path);
- }
-
- return $path;
- }
-
- /**
- * @param array $path
- * @return array
- * @throws CompilerException
- */
- protected function escapeImportPathString($path)
- {
- switch ($path[0]) {
- case Type::T_LIST:
- foreach ($path[2] as $k => $v) {
- $path[2][$k] = $this->escapeImportPathString($v);
- }
- break;
- case Type::T_STRING:
- if ($path[1]) {
- $path = $this->compileValue($path);
- $path = str_replace(' ', '\\ ', $path);
- $path = [Type::T_KEYWORD, $path];
- }
- break;
- }
-
- return $path;
- }
-
- /**
- * Append a root directive like @import or @charset as near as the possible from the source code
- * (keeping before comments, @import and @charset coming before in the source code)
- *
- * @param string $line
- * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $out
- * @param array $allowed
- *
- * @return void
- */
- protected function appendRootDirective($line, $out, $allowed = [Type::T_COMMENT])
- {
- $root = $out;
-
- while ($root->parent) {
- $root = $root->parent;
- }
-
- $i = 0;
-
- while ($i < \count($root->children)) {
- if (! isset($root->children[$i]->type) || ! \in_array($root->children[$i]->type, $allowed)) {
- break;
- }
-
- $i++;
- }
-
- // remove incompatible children from the bottom of the list
- $saveChildren = [];
-
- while ($i < \count($root->children)) {
- $saveChildren[] = array_pop($root->children);
- }
-
- // insert the directive as a comment
- $child = $this->makeOutputBlock(Type::T_COMMENT);
- $child->lines[] = $line;
- $child->sourceName = $this->sourceNames[$this->sourceIndex] ?: '(stdin)';
- $child->sourceLine = $this->sourceLine;
- $child->sourceColumn = $this->sourceColumn;
-
- $root->children[] = $child;
-
- // repush children
- while (\count($saveChildren)) {
- $root->children[] = array_pop($saveChildren);
- }
- }
-
- /**
- * Append lines to the current output block:
- * directly to the block or through a child if necessary
- *
- * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $out
- * @param string $type
- * @param string $line
- *
- * @return void
- */
- protected function appendOutputLine(OutputBlock $out, $type, $line)
- {
- $outWrite = &$out;
-
- // check if it's a flat output or not
- if (\count($out->children)) {
- $lastChild = &$out->children[\count($out->children) - 1];
-
- if (
- $lastChild->depth === $out->depth &&
- \is_null($lastChild->selectors) &&
- ! \count($lastChild->children)
- ) {
- $outWrite = $lastChild;
- } else {
- $nextLines = $this->makeOutputBlock($type);
- $nextLines->parent = $out;
- $nextLines->depth = $out->depth;
-
- $out->children[] = $nextLines;
- $outWrite = &$nextLines;
- }
- }
-
- $outWrite->lines[] = $line;
- }
-
- /**
- * Compile child; returns a value to halt execution
- *
- * @param array $child
- * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $out
- *
- * @return array|Number|null
- */
- protected function compileChild($child, OutputBlock $out)
- {
- if (isset($child[Parser::SOURCE_LINE])) {
- $this->sourceIndex = isset($child[Parser::SOURCE_INDEX]) ? $child[Parser::SOURCE_INDEX] : null;
- $this->sourceLine = $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;
- $this->sourceLine = $child[1]->sourceLine;
- $this->sourceColumn = $child[1]->sourceColumn;
- } elseif (! empty($out->sourceLine) && ! empty($out->sourceName)) {
- $this->sourceLine = $out->sourceLine;
- $sourceIndex = array_search($out->sourceName, $this->sourceNames);
- $this->sourceColumn = $out->sourceColumn;
-
- if ($sourceIndex === false) {
- $sourceIndex = null;
- }
- $this->sourceIndex = $sourceIndex;
- }
-
- 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]);
-
- $this->compileImport($rawPath, $out);
- break;
-
- case Type::T_DIRECTIVE:
- $this->compileDirective($child[1], $out);
- break;
-
- case Type::T_AT_ROOT:
- $this->compileAtRoot($child[1]);
- break;
-
- case Type::T_MEDIA:
- $this->compileMedia($child[1]);
- break;
-
- case Type::T_BLOCK:
- $this->compileBlock($child[1]);
- break;
-
- case Type::T_CHARSET:
- break;
-
- case Type::T_CUSTOM_PROPERTY:
- list(, $name, $value) = $child;
- $compiledName = $this->compileValue($name);
-
- // if the value reduces to null from something else then
- // the property should be discarded
- if ($value[0] !== Type::T_NULL) {
- $value = $this->reduce($value);
-
- if ($value[0] === Type::T_NULL || $value === static::$nullString) {
- break;
- }
- }
-
- $compiledValue = $this->compileValue($value);
-
- $line = $this->formatter->customProperty(
- $compiledName,
- $compiledValue
- );
-
- $this->appendOutputLine($out, Type::T_ASSIGN, $line);
- break;
-
- case Type::T_ASSIGN:
- list(, $name, $value) = $child;
-
- if ($name[0] === Type::T_VARIABLE) {
- $flags = isset($child[3]) ? $child[3] : [];
- $isDefault = \in_array('!default', $flags);
- $isGlobal = \in_array('!global', $flags);
-
- if ($isGlobal) {
- $this->set($name[1], $this->reduce($value), false, $this->rootEnv, $value);
- break;
- }
-
- $shouldSet = $isDefault &&
- (\is_null($result = $this->get($name[1], false)) ||
- $result === static::$null);
-
- if (! $isDefault || $shouldSet) {
- $this->set($name[1], $this->reduce($value), true, null, $value);
- }
- break;
- }
-
- $compiledName = $this->compileValue($name);
-
- // handle shorthand syntaxes : size / line-height...
- if (\in_array($compiledName, ['font', 'grid-row', 'grid-column', 'border-radius'])) {
- if ($value[0] === Type::T_VARIABLE) {
- // if the font value comes from variable, the content is already reduced
- // (i.e., formulas were already calculated), so we need the original unreduced value
- $value = $this->get($value[1], true, null, true);
- }
-
- $shorthandValue=&$value;
-
- $shorthandDividerNeedsUnit = false;
- $maxListElements = null;
- $maxShorthandDividers = 1;
-
- switch ($compiledName) {
- case 'border-radius':
- $maxListElements = 4;
- $shorthandDividerNeedsUnit = true;
- break;
- }
-
- if ($compiledName === 'font' && $value[0] === Type::T_LIST && $value[1] === ',') {
- // this is the case if more than one font is given: example: "font: 400 1em/1.3 arial,helvetica"
- // we need to handle the first list element
- $shorthandValue=&$value[2][0];
- }
-
- if ($shorthandValue[0] === Type::T_EXPRESSION && $shorthandValue[1] === '/') {
- $revert = true;
-
- if ($shorthandDividerNeedsUnit) {
- $divider = $shorthandValue[3];
-
- if (\is_array($divider)) {
- $divider = $this->reduce($divider, true);
- }
-
- if ($divider instanceof Number && \intval($divider->getDimension()) && $divider->unitless()) {
- $revert = false;
- }
- }
-
- if ($revert) {
- $shorthandValue = $this->expToString($shorthandValue);
- }
- } elseif ($shorthandValue[0] === Type::T_LIST) {
- foreach ($shorthandValue[2] as &$item) {
- if ($item[0] === Type::T_EXPRESSION && $item[1] === '/') {
- if ($maxShorthandDividers > 0) {
- $revert = true;
-
- // if the list of values is too long, this has to be a shorthand,
- // otherwise it could be a real division
- if (\is_null($maxListElements) || \count($shorthandValue[2]) <= $maxListElements) {
- if ($shorthandDividerNeedsUnit) {
- $divider = $item[3];
-
- if (\is_array($divider)) {
- $divider = $this->reduce($divider, true);
- }
-
- if ($divider instanceof Number && \intval($divider->getDimension()) && $divider->unitless()) {
- $revert = false;
- }
- }
- }
-
- if ($revert) {
- $item = $this->expToString($item);
- $maxShorthandDividers--;
- }
- }
- }
- }
- }
- }
-
- // if the value reduces to null from something else then
- // the property should be discarded
- if ($value[0] !== Type::T_NULL) {
- $value = $this->reduce($value);
-
- if ($value[0] === Type::T_NULL || $value === static::$nullString) {
- break;
- }
- }
-
- $compiledValue = $this->compileValue($value);
-
- // ignore empty value
- if (\strlen($compiledValue)) {
- $line = $this->formatter->property(
- $compiledName,
- $compiledValue
- );
- $this->appendOutputLine($out, Type::T_ASSIGN, $line);
- }
- break;
-
- case Type::T_COMMENT:
- if ($out->type === Type::T_ROOT) {
- $this->compileComment($child);
- break;
- }
-
- $line = $this->compileCommentValue($child, true);
- $this->appendOutputLine($out, Type::T_COMMENT, $line);
- break;
-
- case Type::T_MIXIN:
- case Type::T_FUNCTION:
- list(, $block) = $child;
- assert($block instanceof CallableBlock);
- // the block need to be able to go up to it's parent env to resolve vars
- $block->parentEnv = $this->getStoreEnv();
- $this->set(static::$namespaces[$block->type] . $block->name, $block, true);
- break;
-
- case Type::T_EXTEND:
- foreach ($child[1] as $sel) {
- $replacedSel = $this->replaceSelfSelector($sel);
-
- if ($replacedSel !== $sel) {
- throw $this->error('Parent selectors aren\'t allowed here.');
- }
-
- $results = $this->evalSelectors([$sel]);
-
- foreach ($results as $result) {
- if (\count($result) !== 1) {
- throw $this->error('complex selectors may not be extended.');
- }
-
- // only use the first one
- $result = $result[0];
- $selectors = $out->selectors;
-
- if (! $selectors && isset($child['selfParent'])) {
- $selectors = $this->multiplySelectors($this->env, $child['selfParent']);
- }
- assert($selectors !== null);
-
- if (\count($result) > 1) {
- $replacement = implode(', ', $result);
- $fname = $this->getPrettyPath($this->sourceNames[$this->sourceIndex]);
- $line = $this->sourceLine;
-
- $message = <<<EOL
-on line $line of $fname:
-Compound selectors may no longer be extended.
-Consider `@extend $replacement` instead.
-See http://bit.ly/ExtendCompound for details.
-EOL;
-
- $this->logger->warn($message);
- }
-
- $this->pushExtends($result, $selectors, $child);
- }
- }
- break;
-
- case Type::T_IF:
- list(, $if) = $child;
- assert($if instanceof IfBlock);
-
- if ($this->isTruthy($this->reduce($if->cond, true))) {
- return $this->compileChildren($if->children, $out);
- }
-
- foreach ($if->cases as $case) {
- if (
- $case instanceof ElseBlock ||
- $case instanceof ElseifBlock && $this->isTruthy($this->reduce($case->cond))
- ) {
- return $this->compileChildren($case->children, $out);
- }
- }
- break;
-
- case Type::T_EACH:
- list(, $each) = $child;
- assert($each instanceof EachBlock);
-
- $list = $this->coerceList($this->reduce($each->list), ',', true);
-
- $this->pushEnv();
-
- foreach ($list[2] as $item) {
- if (\count($each->vars) === 1) {
- $this->set($each->vars[0], $item, true);
- } else {
- list(,, $values) = $this->coerceList($item);
-
- foreach ($each->vars as $i => $var) {
- $this->set($var, isset($values[$i]) ? $values[$i] : static::$null, true);
- }
- }
-
- $ret = $this->compileChildren($each->children, $out);
-
- if ($ret) {
- $store = $this->env->store;
- $this->popEnv();
- $this->backPropagateEnv($store, $each->vars);
-
- return $ret;
- }
- }
- $store = $this->env->store;
- $this->popEnv();
- $this->backPropagateEnv($store, $each->vars);
-
- break;
-
- case Type::T_WHILE:
- list(, $while) = $child;
- assert($while instanceof WhileBlock);
-
- while ($this->isTruthy($this->reduce($while->cond, true))) {
- $ret = $this->compileChildren($while->children, $out);
-
- if ($ret) {
- return $ret;
- }
- }
- break;
-
- case Type::T_FOR:
- list(, $for) = $child;
- assert($for instanceof ForBlock);
-
- $startNumber = $this->assertNumber($this->reduce($for->start, true));
- $endNumber = $this->assertNumber($this->reduce($for->end, true));
-
- $start = $this->assertInteger($startNumber);
-
- $numeratorUnits = $startNumber->getNumeratorUnits();
- $denominatorUnits = $startNumber->getDenominatorUnits();
-
- $end = $this->assertInteger($endNumber->coerce($numeratorUnits, $denominatorUnits));
-
- $d = $start < $end ? 1 : -1;
-
- $this->pushEnv();
-
- for (;;) {
- if (
- (! $for->until && $start - $d == $end) ||
- ($for->until && $start == $end)
- ) {
- break;
- }
-
- $this->set($for->var, new Number($start, $numeratorUnits, $denominatorUnits));
- $start += $d;
-
- $ret = $this->compileChildren($for->children, $out);
-
- if ($ret) {
- $store = $this->env->store;
- $this->popEnv();
- $this->backPropagateEnv($store, [$for->var]);
-
- return $ret;
- }
- }
-
- $store = $this->env->store;
- $this->popEnv();
- $this->backPropagateEnv($store, [$for->var]);
-
- break;
-
- case Type::T_RETURN:
- return $this->reduce($child[1], true);
-
- case Type::T_NESTED_PROPERTY:
- $this->compileNestedPropertiesBlock($child[1], $out);
- break;
-
- case Type::T_INCLUDE:
- // including a mixin
- list(, $name, $argValues, $content, $argUsing) = $child;
-
- $mixin = $this->get(static::$namespaces['mixin'] . $name, false);
-
- if (! $mixin) {
- throw $this->error("Undefined mixin $name");
- }
-
- assert($mixin instanceof CallableBlock);
-
- $callingScope = $this->getStoreEnv();
-
- // push scope, apply args
- $this->pushEnv();
- $this->env->depth--;
-
- // Find the parent selectors in the env to be able to know what '&' refers to in the mixin
- // and assign this fake parent to childs
- $selfParent = null;
-
- if (isset($child['selfParent']) && $child['selfParent'] instanceof Block && isset($child['selfParent']->selectors)) {
- $selfParent = $child['selfParent'];
- } else {
- $parentSelectors = $this->multiplySelectors($this->env);
-
- if ($parentSelectors) {
- $parent = new Block();
- $parent->selectors = $parentSelectors;
-
- foreach ($mixin->children as $k => $child) {
- if (isset($child[1]) && $child[1] instanceof Block) {
- $mixin->children[$k][1]->parent = $parent;
- }
- }
- }
- }
-
- // clone the stored content to not have its scope spoiled by a further call to the same mixin
- // i.e., recursive @include of the same mixin
- if (isset($content)) {
- $copyContent = clone $content;
- $copyContent->scope = clone $callingScope;
-
- $this->setRaw(static::$namespaces['special'] . 'content', $copyContent, $this->env);
- } else {
- $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(static::$namespaces['special'] . 'using', $argUsing, $this->env);
- } else {
- $this->setRaw(static::$namespaces['special'] . 'using', null, $this->env);
- }
-
- if (isset($mixin->args)) {
- $this->applyArguments($mixin->args, $argValues);
- }
-
- $this->env->marker = 'mixin';
-
- if (! empty($mixin->parentEnv)) {
- $this->env->declarationScopeParent = $mixin->parentEnv;
- } else {
- throw $this->error("@mixin $name() without parentEnv");
- }
-
- $this->compileChildrenNoReturn($mixin->children, $out, $selfParent, $this->env->marker . ' ' . $name);
-
- $this->popEnv();
- break;
-
- case Type::T_MIXIN_CONTENT:
- $env = isset($this->storeEnv) ? $this->storeEnv : $this->env;
- $content = $this->get(static::$namespaces['special'] . 'content', false, $env);
- $argUsing = $this->get(static::$namespaces['special'] . 'using', false, $env);
- $argContent = $child[1];
-
- if (! $content) {
- break;
- }
-
- $storeEnv = $this->storeEnv;
- $varsUsing = [];
-
- if (isset($argUsing) && isset($argContent)) {
- // Get the arguments provided for the content with the names provided in the "using" argument list
- $this->storeEnv = null;
- $varsUsing = $this->applyArguments($argUsing, $argContent, false);
- }
-
- // restore the scope from the @content
- $this->storeEnv = $content->scope;
-
- // append the vars from using if any
- foreach ($varsUsing as $name => $val) {
- $this->set($name, $val, true, $this->storeEnv);
- }
-
- $this->compileChildrenNoReturn($content->children, $out);
-
- $this->storeEnv = $storeEnv;
- break;
-
- case Type::T_DEBUG:
- list(, $value) = $child;
-
- $fname = $this->getPrettyPath($this->sourceNames[$this->sourceIndex]);
- $line = $this->sourceLine;
- $value = $this->compileDebugValue($value);
-
- $this->logger->debug("$fname:$line DEBUG: $value");
- break;
-
- case Type::T_WARN:
- list(, $value) = $child;
-
- $fname = $this->getPrettyPath($this->sourceNames[$this->sourceIndex]);
- $line = $this->sourceLine;
- $value = $this->compileDebugValue($value);
-
- $this->logger->warn("$value\n on line $line of $fname");
- break;
-
- case Type::T_ERROR:
- list(, $value) = $child;
-
- $fname = $this->getPrettyPath($this->sourceNames[$this->sourceIndex]);
- $line = $this->sourceLine;
- $value = $this->compileValue($this->reduce($value, true));
-
- throw $this->error("File $fname on line $line ERROR: $value\n");
-
- default:
- throw $this->error("unknown child type: $child[0]");
- }
-
- return null;
- }
-
- /**
- * Reduce expression to string
- *
- * @param array $exp
- * @param bool $keepParens
- *
- * @return array
- */
- protected function expToString($exp, $keepParens = false)
- {
- list(, $op, $left, $right, $inParens, $whiteLeft, $whiteRight) = $exp;
-
- $content = [];
-
- if ($keepParens && $inParens) {
- $content[] = '(';
- }
-
- $content[] = $this->reduce($left);
-
- if ($whiteLeft) {
- $content[] = ' ';
- }
-
- $content[] = $op;
-
- if ($whiteRight) {
- $content[] = ' ';
- }
-
- $content[] = $this->reduce($right);
-
- if ($keepParens && $inParens) {
- $content[] = ')';
- }
-
- return [Type::T_STRING, '', $content];
- }
-
- /**
- * Is truthy?
- *
- * @param array|Number $value
- *
- * @return bool
- */
- public function isTruthy($value)
- {
- return $value !== static::$false && $value !== static::$null;
- }
-
- /**
- * Is the value a direct relationship combinator?
- *
- * @param string $value
- *
- * @return bool
- */
- protected function isImmediateRelationshipCombinator($value)
- {
- return $value === '>' || $value === '+' || $value === '~';
- }
-
- /**
- * Should $value cause its operand to eval
- *
- * @param array $value
- *
- * @return bool
- */
- protected function shouldEval($value)
- {
- switch ($value[0]) {
- case Type::T_EXPRESSION:
- if ($value[1] === '/') {
- return $this->shouldEval($value[2]) || $this->shouldEval($value[3]);
- }
-
- // fall-thru
- case Type::T_VARIABLE:
- case Type::T_FUNCTION_CALL:
- return true;
- }
-
- return false;
- }
-
- /**
- * Reduce value
- *
- * @param array|Number $value
- * @param bool $inExp
- *
- * @return array|Number
- */
- protected function reduce($value, $inExp = false)
- {
- if ($value instanceof Number) {
- return $value;
- }
-
- switch ($value[0]) {
- case Type::T_EXPRESSION:
- list(, $op, $left, $right, $inParens) = $value;
-
- $opName = isset(static::$operatorNames[$op]) ? static::$operatorNames[$op] : $op;
- $inExp = $inExp || $this->shouldEval($left) || $this->shouldEval($right);
-
- $left = $this->reduce($left, true);
-
- if ($op !== 'and' && $op !== 'or') {
- $right = $this->reduce($right, true);
- }
-
- // special case: looks like css shorthand
- if (
- $opName == 'div' && ! $inParens && ! $inExp &&
- (($right[0] !== Type::T_NUMBER && isset($right[2]) && $right[2] != '') ||
- ($right[0] === Type::T_NUMBER && ! $right->unitless()))
- ) {
- return $this->expToString($value);
- }
-
- $left = $this->coerceForExpression($left);
- $right = $this->coerceForExpression($right);
- $ltype = $left[0];
- $rtype = $right[0];
-
- $ucOpName = ucfirst($opName);
- $ucLType = ucfirst($ltype);
- $ucRType = ucfirst($rtype);
-
- $shouldEval = $inParens || $inExp;
-
- // this tries:
- // 1. op[op name][left type][right type]
- // 2. op[left type][right type] (passing the op as first arg)
- // 3. op[op name]
- if (\is_callable([$this, $fn = "op{$ucOpName}{$ucLType}{$ucRType}"])) {
- $out = $this->$fn($left, $right, $shouldEval);
- } elseif (\is_callable([$this, $fn = "op{$ucLType}{$ucRType}"])) {
- $out = $this->$fn($op, $left, $right, $shouldEval);
- } elseif (\is_callable([$this, $fn = "op{$ucOpName}"])) {
- $out = $this->$fn($left, $right, $shouldEval);
- } else {
- $out = null;
- }
-
- if (isset($out)) {
- return $out;
- }
-
- return $this->expToString($value);
-
- case Type::T_UNARY:
- list(, $op, $exp, $inParens) = $value;
-
- $inExp = $inExp || $this->shouldEval($exp);
- $exp = $this->reduce($exp);
-
- if ($exp instanceof Number) {
- switch ($op) {
- case '+':
- return $exp;
-
- case '-':
- return $exp->unaryMinus();
- }
- }
-
- if ($op === 'not') {
- if ($inExp || $inParens) {
- if ($exp === static::$false || $exp === static::$null) {
- return static::$true;
- }
-
- return static::$false;
- }
-
- $op = $op . ' ';
- }
-
- return [Type::T_STRING, '', [$op, $exp]];
-
- case Type::T_VARIABLE:
- return $this->reduce($this->get($value[1]));
-
- case Type::T_LIST:
- foreach ($value[2] as &$item) {
- $item = $this->reduce($item);
- }
- unset($item);
-
- if (isset($value[3]) && \is_array($value[3])) {
- foreach ($value[3] as &$item) {
- $item = $this->reduce($item);
- }
- unset($item);
- }
-
- return $value;
-
- case Type::T_MAP:
- foreach ($value[1] as &$item) {
- $item = $this->reduce($item);
- }
-
- foreach ($value[2] as &$item) {
- $item = $this->reduce($item);
- }
-
- return $value;
-
- case Type::T_STRING:
- foreach ($value[2] as &$item) {
- if (\is_array($item) || $item instanceof Number) {
- $item = $this->reduce($item);
- }
- }
-
- return $value;
-
- case Type::T_INTERPOLATE:
- $value[1] = $this->reduce($value[1]);
-
- if ($inExp) {
- return [Type::T_KEYWORD, $this->compileValue($value, false)];
- }
-
- return $value;
-
- case Type::T_FUNCTION_CALL:
- return $this->fncall($value[1], $value[2]);
-
- case Type::T_SELF:
- $selfParent = ! empty($this->env->block->selfParent) ? $this->env->block->selfParent : null;
- $selfSelector = $this->multiplySelectors($this->env, $selfParent);
- $selfSelector = $this->collapseSelectorsAsList($selfSelector);
-
- return $selfSelector;
-
- default:
- return $value;
- }
- }
-
- /**
- * Function caller
- *
- * @param string|array $functionReference
- * @param array $argValues
- *
- * @return array|Number
- */
- protected function fncall($functionReference, $argValues)
- {
- // a string means this is a static hard reference coming from the parsing
- if (is_string($functionReference)) {
- $name = $functionReference;
-
- $functionReference = $this->getFunctionReference($name);
- if ($functionReference === static::$null || $functionReference[0] !== Type::T_FUNCTION_REFERENCE) {
- $functionReference = [Type::T_FUNCTION, $name, [Type::T_LIST, ',', []]];
- }
- }
-
- // a function type means we just want a plain css function call
- if ($functionReference[0] === Type::T_FUNCTION) {
- // for CSS functions, simply flatten the arguments into a list
- $listArgs = [];
-
- foreach ((array) $argValues as $arg) {
- if (empty($arg[0]) || count($argValues) === 1) {
- $listArgs[] = $this->reduce($this->stringifyFncallArgs($arg[1]));
- }
- }
-
- return [Type::T_FUNCTION, $functionReference[1], [Type::T_LIST, ',', $listArgs]];
- }
-
- if ($functionReference === static::$null || $functionReference[0] !== Type::T_FUNCTION_REFERENCE) {
- return static::$defaultValue;
- }
-
-
- switch ($functionReference[1]) {
- // SCSS @function
- case 'scss':
- return $this->callScssFunction($functionReference[3], $argValues);
-
- // native PHP functions
- case 'user':
- case 'native':
- list(,,$name, $fn, $prototype) = $functionReference;
-
- // special cases of css valid functions min/max
- $name = strtolower($name);
- if (\in_array($name, ['min', 'max']) && count($argValues) >= 1) {
- $cssFunction = $this->cssValidArg(
- [Type::T_FUNCTION_CALL, $name, $argValues],
- ['min', 'max', 'calc', 'env', 'var']
- );
- if ($cssFunction !== false) {
- return $cssFunction;
- }
- }
- $returnValue = $this->callNativeFunction($name, $fn, $prototype, $argValues);
-
- if (! isset($returnValue)) {
- return $this->fncall([Type::T_FUNCTION, $name, [Type::T_LIST, ',', []]], $argValues);
- }
-
- return $returnValue;
-
- default:
- return static::$defaultValue;
- }
- }
-
- /**
- * @param array|Number $arg
- * @param string[] $allowed_function
- * @param bool $inFunction
- *
- * @return array|Number|false
- */
- protected function cssValidArg($arg, $allowed_function = [], $inFunction = false)
- {
- if ($arg instanceof Number) {
- return $this->stringifyFncallArgs($arg);
- }
-
- switch ($arg[0]) {
- case Type::T_INTERPOLATE:
- return [Type::T_KEYWORD, $this->CompileValue($arg)];
-
- case Type::T_FUNCTION:
- if (! \in_array($arg[1], $allowed_function)) {
- return false;
- }
- if ($arg[2][0] === Type::T_LIST) {
- foreach ($arg[2][2] as $k => $subarg) {
- $arg[2][2][$k] = $this->cssValidArg($subarg, $allowed_function, $arg[1]);
- if ($arg[2][2][$k] === false) {
- return false;
- }
- }
- }
- return $arg;
-
- case Type::T_FUNCTION_CALL:
- if (! \in_array($arg[1], $allowed_function)) {
- return false;
- }
- $cssArgs = [];
- foreach ($arg[2] as $argValue) {
- if ($argValue === static::$null) {
- return false;
- }
- $cssArg = $this->cssValidArg($argValue[1], $allowed_function, $arg[1]);
- if (empty($argValue[0]) && $cssArg !== false) {
- $cssArgs[] = [$argValue[0], $cssArg];
- } else {
- return false;
- }
- }
-
- return $this->fncall([Type::T_FUNCTION, $arg[1], [Type::T_LIST, ',', []]], $cssArgs);
-
- case Type::T_STRING:
- case Type::T_KEYWORD:
- if (!$inFunction or !\in_array($inFunction, ['calc', 'env', 'var'])) {
- return false;
- }
- return $this->stringifyFncallArgs($arg);
-
- case Type::T_LIST:
- if (!$inFunction) {
- return false;
- }
- if (empty($arg['enclosing']) and $arg[1] === '') {
- foreach ($arg[2] as $k => $subarg) {
- $arg[2][$k] = $this->cssValidArg($subarg, $allowed_function, $inFunction);
- if ($arg[2][$k] === false) {
- return false;
- }
- }
- $arg[0] = Type::T_STRING;
- return $arg;
- }
- return false;
-
- case Type::T_EXPRESSION:
- if (! \in_array($arg[1], ['+', '-', '/', '*'])) {
- return false;
- }
- $arg[2] = $this->cssValidArg($arg[2], $allowed_function, $inFunction);
- $arg[3] = $this->cssValidArg($arg[3], $allowed_function, $inFunction);
- if ($arg[2] === false || $arg[3] === false) {
- return false;
- }
- return $this->expToString($arg, true);
-
- case Type::T_VARIABLE:
- case Type::T_SELF:
- default:
- return false;
- }
- }
-
-
- /**
- * Reformat fncall arguments to proper css function output
- *
- * @param array|Number $arg
- *
- * @return array|Number
- */
- protected function stringifyFncallArgs($arg)
- {
- if ($arg instanceof Number) {
- return $arg;
- }
-
- switch ($arg[0]) {
- case Type::T_LIST:
- foreach ($arg[2] as $k => $v) {
- $arg[2][$k] = $this->stringifyFncallArgs($v);
- }
- break;
-
- case Type::T_EXPRESSION:
- if ($arg[1] === '/') {
- $arg[2] = $this->stringifyFncallArgs($arg[2]);
- $arg[3] = $this->stringifyFncallArgs($arg[3]);
- $arg[5] = $arg[6] = false; // no space around /
- $arg = $this->expToString($arg);
- }
- break;
-
- case Type::T_FUNCTION_CALL:
- $name = strtolower($arg[1]);
-
- if (in_array($name, ['max', 'min', 'calc'])) {
- $args = $arg[2];
- $arg = $this->fncall([Type::T_FUNCTION, $name, [Type::T_LIST, ',', []]], $args);
- }
- break;
- }
-
- return $arg;
- }
-
- /**
- * Find a function reference
- * @param string $name
- * @param bool $safeCopy
- * @return array
- */
- protected function getFunctionReference($name, $safeCopy = false)
- {
- // SCSS @function
- if ($func = $this->get(static::$namespaces['function'] . $name, false)) {
- if ($safeCopy) {
- $func = clone $func;
- }
-
- return [Type::T_FUNCTION_REFERENCE, 'scss', $name, $func];
- }
-
- // native PHP functions
-
- // try to find a native lib function
- $normalizedName = $this->normalizeName($name);
-
- if (isset($this->userFunctions[$normalizedName])) {
- // see if we can find a user function
- list($f, $prototype) = $this->userFunctions[$normalizedName];
-
- return [Type::T_FUNCTION_REFERENCE, 'user', $name, $f, $prototype];
- }
-
- $lowercasedName = strtolower($normalizedName);
-
- // Special functions overriding a CSS function are case-insensitive. We normalize them as lowercase
- // to avoid the deprecation warning about the wrong case being used.
- if ($lowercasedName === 'min' || $lowercasedName === 'max' || $lowercasedName === 'rgb' || $lowercasedName === 'rgba' || $lowercasedName === 'hsl' || $lowercasedName === 'hsla') {
- $normalizedName = $lowercasedName;
- }
-
- if (($f = $this->getBuiltinFunction($normalizedName)) && \is_callable($f)) {
- /** @var string $libName */
- $libName = $f[1];
- $prototype = isset(static::$$libName) ? static::$$libName : null;
-
- // All core functions have a prototype defined. Not finding the
- // prototype can mean 2 things:
- // - the function comes from a child class (deprecated just after)
- // - the function was found with a different case, which relates to calling the
- // wrong Sass function due to our camelCase usage (`fade-in()` vs `fadein()`),
- // because PHP method names are case-insensitive while property names are
- // case-sensitive.
- if ($prototype === null || strtolower($normalizedName) !== $normalizedName) {
- $r = new \ReflectionMethod($this, $libName);
- $actualLibName = $r->name;
-
- if ($actualLibName !== $libName || strtolower($normalizedName) !== $normalizedName) {
- $kebabCaseName = preg_replace('~(?<=\\w)([A-Z])~', '-$1', substr($actualLibName, 3));
- assert($kebabCaseName !== null);
- $originalName = strtolower($kebabCaseName);
- $warning = "Calling built-in functions with a non-standard name is deprecated since Scssphp 1.8.0 and will not work anymore in 2.0 (they will be treated as CSS function calls instead).\nUse \"$originalName\" instead of \"$name\".";
- @trigger_error($warning, E_USER_DEPRECATED);
- $fname = $this->getPrettyPath($this->sourceNames[$this->sourceIndex]);
- $line = $this->sourceLine;
- Warn::deprecation("$warning\n on line $line of $fname");
-
- // Use the actual function definition
- $prototype = isset(static::$$actualLibName) ? static::$$actualLibName : null;
- $f[1] = $libName = $actualLibName;
- }
- }
-
- if (\get_class($this) !== __CLASS__ && !isset($this->warnedChildFunctions[$libName])) {
- $r = new \ReflectionMethod($this, $libName);
- $declaringClass = $r->getDeclaringClass()->name;
-
- $needsWarning = $this->warnedChildFunctions[$libName] = $declaringClass !== __CLASS__;
-
- if ($needsWarning) {
- if (method_exists(__CLASS__, $libName)) {
- @trigger_error(sprintf('Overriding the "%s" core function by extending the Compiler is deprecated and will be unsupported in 2.0. Remove the "%s::%s" method.', $normalizedName, $declaringClass, $libName), E_USER_DEPRECATED);
- } else {
- @trigger_error(sprintf('Registering custom functions by extending the Compiler and using the lib* discovery mechanism is deprecated and will be removed in 2.0. Replace the "%s::%s" method with registering the "%s" function through "Compiler::registerFunction".', $declaringClass, $libName, $normalizedName), E_USER_DEPRECATED);
- }
- }
- }
-
- return [Type::T_FUNCTION_REFERENCE, 'native', $name, $f, $prototype];
- }
-
- return static::$null;
- }
-
-
- /**
- * Normalize name
- *
- * @param string $name
- *
- * @return string
- */
- protected function normalizeName($name)
- {
- return str_replace('-', '_', $name);
- }
-
- /**
- * Normalize value
- *
- * @internal
- *
- * @param array|Number $value
- *
- * @return array|Number
- */
- public function normalizeValue($value)
- {
- $value = $this->coerceForExpression($this->reduce($value));
-
- if ($value instanceof Number) {
- return $value;
- }
-
- switch ($value[0]) {
- case Type::T_LIST:
- $value = $this->extractInterpolation($value);
-
- if ($value[0] !== Type::T_LIST) {
- return [Type::T_KEYWORD, $this->compileValue($value)];
- }
-
- foreach ($value[2] as $key => $item) {
- $value[2][$key] = $this->normalizeValue($item);
- }
-
- if (! empty($value['enclosing'])) {
- unset($value['enclosing']);
- }
-
- if ($value[1] === '' && count($value[2]) > 1) {
- $value[1] = ' ';
- }
-
- return $value;
-
- case Type::T_STRING:
- return [$value[0], '"', [$this->compileStringContent($value)]];
-
- case Type::T_INTERPOLATE:
- return [Type::T_KEYWORD, $this->compileValue($value)];
-
- default:
- return $value;
- }
- }
-
- /**
- * Add numbers
- *
- * @param Number $left
- * @param Number $right
- *
- * @return Number
- */
- protected function opAddNumberNumber(Number $left, Number $right)
- {
- return $left->plus($right);
- }
-
- /**
- * Multiply numbers
- *
- * @param Number $left
- * @param Number $right
- *
- * @return Number
- */
- protected function opMulNumberNumber(Number $left, Number $right)
- {
- return $left->times($right);
- }
-
- /**
- * Subtract numbers
- *
- * @param Number $left
- * @param Number $right
- *
- * @return Number
- */
- protected function opSubNumberNumber(Number $left, Number $right)
- {
- return $left->minus($right);
- }
-
- /**
- * Divide numbers
- *
- * @param Number $left
- * @param Number $right
- *
- * @return Number
- */
- protected function opDivNumberNumber(Number $left, Number $right)
- {
- return $left->dividedBy($right);
- }
-
- /**
- * Mod numbers
- *
- * @param Number $left
- * @param Number $right
- *
- * @return Number
- */
- protected function opModNumberNumber(Number $left, Number $right)
- {
- return $left->modulo($right);
- }
-
- /**
- * Add strings
- *
- * @param array $left
- * @param array $right
- *
- * @return array|null
- */
- protected function opAdd($left, $right)
- {
- if ($strLeft = $this->coerceString($left)) {
- if ($right[0] === Type::T_STRING) {
- $right[1] = '';
- }
-
- $strLeft[2][] = $right;
-
- return $strLeft;
- }
-
- if ($strRight = $this->coerceString($right)) {
- if ($left[0] === Type::T_STRING) {
- $left[1] = '';
- }
-
- array_unshift($strRight[2], $left);
-
- return $strRight;
- }
-
- return null;
- }
-
- /**
- * Boolean and
- *
- * @param array|Number $left
- * @param array|Number $right
- * @param bool $shouldEval
- *
- * @return array|Number|null
- */
- protected function opAnd($left, $right, $shouldEval)
- {
- $truthy = ($left === static::$null || $right === static::$null) ||
- ($left === static::$false || $left === static::$true) &&
- ($right === static::$false || $right === static::$true);
-
- if (! $shouldEval) {
- if (! $truthy) {
- return null;
- }
- }
-
- if ($left !== static::$false && $left !== static::$null) {
- return $this->reduce($right, true);
- }
-
- return $left;
- }
-
- /**
- * Boolean or
- *
- * @param array|Number $left
- * @param array|Number $right
- * @param bool $shouldEval
- *
- * @return array|Number|null
- */
- protected function opOr($left, $right, $shouldEval)
- {
- $truthy = ($left === static::$null || $right === static::$null) ||
- ($left === static::$false || $left === static::$true) &&
- ($right === static::$false || $right === static::$true);
-
- if (! $shouldEval) {
- if (! $truthy) {
- return null;
- }
- }
-
- if ($left !== static::$false && $left !== static::$null) {
- return $left;
- }
-
- return $this->reduce($right, true);
- }
-
- /**
- * Compare colors
- *
- * @param string $op
- * @param array $left
- * @param array $right
- *
- * @return array
- */
- protected function opColorColor($op, $left, $right)
- {
- if ($op !== '==' && $op !== '!=') {
- $warning = "Color arithmetic is deprecated and will be an error in future versions.\n"
- . "Consider using Sass's color functions instead.";
- $fname = $this->getPrettyPath($this->sourceNames[$this->sourceIndex]);
- $line = $this->sourceLine;
-
- Warn::deprecation("$warning\n on line $line of $fname");
- }
-
- $out = [Type::T_COLOR];
-
- foreach ([1, 2, 3] as $i) {
- $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");
- }
-
- $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);
- }
-
- /**
- * Compare color and number
- *
- * @param string $op
- * @param array $left
- * @param Number $right
- *
- * @return array
- */
- protected function opColorNumber($op, $left, Number $right)
- {
- if ($op === '==') {
- return static::$false;
- }
-
- if ($op === '!=') {
- return static::$true;
- }
-
- $value = $right->getDimension();
-
- return $this->opColorColor(
- $op,
- $left,
- [Type::T_COLOR, $value, $value, $value]
- );
- }
-
- /**
- * Compare number and color
- *
- * @param string $op
- * @param Number $left
- * @param array $right
- *
- * @return array
- */
- protected function opNumberColor($op, Number $left, $right)
- {
- if ($op === '==') {
- return static::$false;
- }
-
- if ($op === '!=') {
- return static::$true;
- }
-
- $value = $left->getDimension();
-
- return $this->opColorColor(
- $op,
- [Type::T_COLOR, $value, $value, $value],
- $right
- );
- }
-
- /**
- * Compare number1 == number2
- *
- * @param array|Number $left
- * @param array|Number $right
- *
- * @return array
- */
- protected function opEq($left, $right)
- {
- if (($lStr = $this->coerceString($left)) && ($rStr = $this->coerceString($right))) {
- $lStr[1] = '';
- $rStr[1] = '';
-
- $left = $this->compileValue($lStr);
- $right = $this->compileValue($rStr);
- }
-
- return $this->toBool($left === $right);
- }
-
- /**
- * Compare number1 != number2
- *
- * @param array|Number $left
- * @param array|Number $right
- *
- * @return array
- */
- protected function opNeq($left, $right)
- {
- if (($lStr = $this->coerceString($left)) && ($rStr = $this->coerceString($right))) {
- $lStr[1] = '';
- $rStr[1] = '';
-
- $left = $this->compileValue($lStr);
- $right = $this->compileValue($rStr);
- }
-
- return $this->toBool($left !== $right);
- }
-
- /**
- * Compare number1 == number2
- *
- * @param Number $left
- * @param Number $right
- *
- * @return array
- */
- protected function opEqNumberNumber(Number $left, Number $right)
- {
- return $this->toBool($left->equals($right));
- }
-
- /**
- * Compare number1 != number2
- *
- * @param Number $left
- * @param Number $right
- *
- * @return array
- */
- protected function opNeqNumberNumber(Number $left, Number $right)
- {
- return $this->toBool(!$left->equals($right));
- }
-
- /**
- * Compare number1 >= number2
- *
- * @param Number $left
- * @param Number $right
- *
- * @return array
- */
- protected function opGteNumberNumber(Number $left, Number $right)
- {
- return $this->toBool($left->greaterThanOrEqual($right));
- }
-
- /**
- * Compare number1 > number2
- *
- * @param Number $left
- * @param Number $right
- *
- * @return array
- */
- protected function opGtNumberNumber(Number $left, Number $right)
- {
- return $this->toBool($left->greaterThan($right));
- }
-
- /**
- * Compare number1 <= number2
- *
- * @param Number $left
- * @param Number $right
- *
- * @return array
- */
- protected function opLteNumberNumber(Number $left, Number $right)
- {
- return $this->toBool($left->lessThanOrEqual($right));
- }
-
- /**
- * Compare number1 < number2
- *
- * @param Number $left
- * @param Number $right
- *
- * @return array
- */
- protected function opLtNumberNumber(Number $left, Number $right)
- {
- return $this->toBool($left->lessThan($right));
- }
-
- /**
- * Cast to boolean
- *
- * @api
- *
- * @param bool $thing
- *
- * @return array
- */
- public function toBool($thing)
- {
- return $thing ? static::$true : static::$false;
- }
-
- /**
- * Escape non printable chars in strings output as in dart-sass
- *
- * @internal
- *
- * @param string $string
- * @param bool $inKeyword
- *
- * @return string
- */
- public function escapeNonPrintableChars($string, $inKeyword = false)
- {
- static $replacement = [];
- if (empty($replacement[$inKeyword])) {
- for ($i = 0; $i < 32; $i++) {
- if ($i !== 9 || $inKeyword) {
- $replacement[$inKeyword][chr($i)] = '\\' . dechex($i) . ($inKeyword ? ' ' : chr(0));
- }
- }
- }
- $string = str_replace(array_keys($replacement[$inKeyword]), array_values($replacement[$inKeyword]), $string);
- // chr(0) is not a possible char from the input, so any chr(0) comes from our escaping replacement
- if (strpos($string, chr(0)) !== false) {
- if (substr($string, -1) === chr(0)) {
- $string = substr($string, 0, -1);
- }
- $string = str_replace(
- [chr(0) . '\\',chr(0) . ' '],
- [ '\\', ' '],
- $string
- );
- if (strpos($string, chr(0)) !== false) {
- $parts = explode(chr(0), $string);
- $string = array_shift($parts);
- while (count($parts)) {
- $next = array_shift($parts);
- if (strpos("0123456789abcdefABCDEF" . chr(9), $next[0]) !== false) {
- $string .= " ";
- }
- $string .= $next;
- }
- }
- }
-
- return $string;
- }
-
- /**
- * Compiles a primitive value into a CSS property value.
- *
- * Values in scssphp are typed by being wrapped in arrays, their format is
- * typically:
- *
- * array(type, contents [, additional_contents]*)
- *
- * 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, $quote = true)
- {
- $value = $this->reduce($value);
-
- if ($value instanceof Number) {
- return $value->output($this);
- }
-
- switch ($value[0]) {
- case Type::T_KEYWORD:
- return $this->escapeNonPrintableChars($value[1], true);
-
- case Type::T_COLOR:
- // [1] - red component (either number for a %)
- // [2] - green component
- // [3] - blue component
- // [4] - optional alpha component
- list(, $r, $g, $b) = $value;
-
- $r = $this->compileRGBAValue($r);
- $g = $this->compileRGBAValue($g);
- $b = $this->compileRGBAValue($b);
-
- if (\count($value) === 5) {
- $alpha = $this->compileRGBAValue($value[4], true);
-
- if (! is_numeric($alpha) || $alpha < 1) {
- $colorName = Colors::RGBaToColorName($r, $g, $b, $alpha);
-
- if (! \is_null($colorName)) {
- return $colorName;
- }
-
- if (\is_int($alpha) || \is_float($alpha)) {
- $a = new Number($alpha, '');
- } elseif (is_numeric($alpha)) {
- $a = new Number((float) $alpha, '');
- } else {
- $a = $alpha;
- }
-
- return 'rgba(' . $r . ', ' . $g . ', ' . $b . ', ' . $a . ')';
- }
- }
-
- if (! is_numeric($r) || ! is_numeric($g) || ! is_numeric($b)) {
- return 'rgb(' . $r . ', ' . $g . ', ' . $b . ')';
- }
-
- $colorName = Colors::RGBaToColorName($r, $g, $b);
-
- if (! \is_null($colorName)) {
- return $colorName;
- }
-
- $h = sprintf('#%02x%02x%02x', $r, $g, $b);
-
- // Converting hex color to short notation (e.g. #003399 to #039)
- if ($h[1] === $h[2] && $h[3] === $h[4] && $h[5] === $h[6]) {
- $h = '#' . $h[1] . $h[3] . $h[5];
- }
-
- return $h;
-
- case Type::T_STRING:
- $content = $this->compileStringContent($value, $quote);
-
- if ($value[1] && $quote) {
- $content = str_replace('\\', '\\\\', $content);
-
- $content = $this->escapeNonPrintableChars($content);
-
- // force double quote as string quote for the output in certain cases
- if (
- $value[1] === "'" &&
- (strpos($content, '"') === false or strpos($content, "'") !== false)
- ) {
- $value[1] = '"';
- } elseif (
- $value[1] === '"' &&
- (strpos($content, '"') !== false and strpos($content, "'") === false)
- ) {
- $value[1] = "'";
- }
-
- $content = str_replace($value[1], '\\' . $value[1], $content);
- }
-
- return $value[1] . $content . $value[1];
-
- case Type::T_FUNCTION:
- $args = ! empty($value[2]) ? $this->compileValue($value[2], $quote) : '';
-
- return "$value[1]($args)";
-
- case Type::T_FUNCTION_REFERENCE:
- $name = ! empty($value[2]) ? $value[2] : '';
-
- return "get-function(\"$name\")";
-
- case Type::T_LIST:
- $value = $this->extractInterpolation($value);
-
- if ($value[0] !== Type::T_LIST) {
- return $this->compileValue($value, $quote);
- }
-
- list(, $delim, $items) = $value;
- $pre = $post = '';
-
- if (! empty($value['enclosing'])) {
- switch ($value['enclosing']) {
- case 'parent':
- //$pre = '(';
- //$post = ')';
- break;
- case 'forced_parent':
- $pre = '(';
- $post = ')';
- break;
- case 'bracket':
- case 'forced_bracket':
- $pre = '[';
- $post = ']';
- break;
- }
- }
-
- $separator = $delim === '/' ? ' /' : $delim;
-
- $prefix_value = '';
-
- if ($delim !== ' ') {
- $prefix_value = ' ';
- }
-
- $filtered = [];
-
- $same_string_quote = null;
- foreach ($items as $item) {
- if (\is_null($same_string_quote)) {
- $same_string_quote = false;
- if ($item[0] === Type::T_STRING) {
- $same_string_quote = $item[1];
- foreach ($items as $ii) {
- if ($ii[0] !== Type::T_STRING) {
- $same_string_quote = false;
- break;
- }
- }
- }
- }
- if ($item[0] === Type::T_NULL) {
- continue;
- }
- if ($same_string_quote === '"' && $item[0] === Type::T_STRING && $item[1]) {
- $item[1] = $same_string_quote;
- }
-
- $compiled = $this->compileValue($item, $quote);
-
- if ($prefix_value && \strlen($compiled)) {
- $compiled = $prefix_value . $compiled;
- }
-
- $filtered[] = $compiled;
- }
-
- return $pre . substr(implode($separator, $filtered), \strlen($prefix_value)) . $post;
-
- case Type::T_MAP:
- $keys = $value[1];
- $values = $value[2];
- $filtered = [];
-
- for ($i = 0, $s = \count($keys); $i < $s; $i++) {
- $filtered[$this->compileValue($keys[$i], $quote)] = $this->compileValue($values[$i], $quote);
- }
-
- array_walk($filtered, function (&$value, $key) {
- $value = $key . ': ' . $value;
- });
-
- return '(' . implode(', ', $filtered) . ')';
-
- case Type::T_INTERPOLATED:
- // node created by extractInterpolation
- list(, $interpolate, $left, $right) = $value;
- list(,, $whiteLeft, $whiteRight) = $interpolate;
-
- $delim = $left[1];
-
- if ($delim && $delim !== ' ' && ! $whiteLeft) {
- $delim .= ' ';
- }
-
- $left = \count($left[2]) > 0
- ? $this->compileValue($left, $quote) . $delim . $whiteLeft
- : '';
-
- $delim = $right[1];
-
- if ($delim && $delim !== ' ') {
- $delim .= ' ';
- }
-
- $right = \count($right[2]) > 0 ?
- $whiteRight . $delim . $this->compileValue($right, $quote) : '';
-
- return $left . $this->compileValue($interpolate, $quote) . $right;
-
- case Type::T_INTERPOLATE:
- // strip quotes if it's a string
- $reduced = $this->reduce($value[1]);
-
- if ($reduced instanceof Number) {
- return $this->compileValue($reduced, $quote);
- }
-
- switch ($reduced[0]) {
- case Type::T_LIST:
- $reduced = $this->extractInterpolation($reduced);
-
- if ($reduced[0] !== Type::T_LIST) {
- break;
- }
-
- list(, $delim, $items) = $reduced;
-
- if ($delim !== ' ') {
- $delim .= ' ';
- }
-
- $filtered = [];
-
- foreach ($items as $item) {
- if ($item[0] === Type::T_NULL) {
- continue;
- }
-
- if ($item[0] === Type::T_STRING) {
- $filtered[] = $this->compileStringContent($item, $quote);
- } elseif ($item[0] === Type::T_KEYWORD) {
- $filtered[] = $item[1];
- } else {
- $filtered[] = $this->compileValue($item, $quote);
- }
- }
-
- $reduced = [Type::T_KEYWORD, implode("$delim", $filtered)];
- break;
-
- case Type::T_STRING:
- $reduced = [Type::T_STRING, '', [$this->compileStringContent($reduced)]];
- break;
-
- case Type::T_NULL:
- $reduced = [Type::T_KEYWORD, ''];
- }
-
- return $this->compileValue($reduced, $quote);
-
- case Type::T_NULL:
- return 'null';
-
- case Type::T_COMMENT:
- return $this->compileCommentValue($value);
-
- default:
- throw $this->error('unknown value type: ' . json_encode($value));
- }
- }
-
- /**
- * @param array|Number $value
- *
- * @return string
- */
- protected function compileDebugValue($value)
- {
- $value = $this->reduce($value, true);
-
- if ($value instanceof Number) {
- return $this->compileValue($value);
- }
-
- switch ($value[0]) {
- case Type::T_STRING:
- return $this->compileStringContent($value);
-
- default:
- return $this->compileValue($value);
- }
- }
-
- /**
- * 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
- *
- * Calling this method on anything else than a SassString is unsupported. Use {@see assertString} first
- * to ensure that the value is indeed a string.
- *
- * @param array $value
- *
- * @return string
- */
- public function getStringText(array $value)
- {
- if ($value[0] !== Type::T_STRING) {
- throw new \InvalidArgumentException('The argument is not a sass string. Did you forgot to use "assertString"?');
- }
-
- return $this->compileStringContent($value);
- }
-
- /**
- * Compile string content
- *
- * @param array $string
- * @param bool $quote
- *
- * @return string
- */
- protected function compileStringContent($string, $quote = true)
- {
- $parts = [];
-
- foreach ($string[2] as $part) {
- if (\is_array($part) || $part instanceof Number) {
- $parts[] = $this->compileValue($part, $quote);
- } else {
- $parts[] = $part;
- }
- }
-
- return implode($parts);
- }
-
- /**
- * Extract interpolation; it doesn't need to be recursive, compileValue will handle that
- *
- * @param array $list
- *
- * @return array
- */
- protected function extractInterpolation($list)
- {
- $items = $list[2];
-
- foreach ($items as $i => $item) {
- if ($item[0] === Type::T_INTERPOLATE) {
- $before = [Type::T_LIST, $list[1], \array_slice($items, 0, $i)];
- $after = [Type::T_LIST, $list[1], \array_slice($items, $i + 1)];
-
- return [Type::T_INTERPOLATED, $item, $before, $after];
- }
- }
-
- return $list;
- }
-
- /**
- * Find the final set of selectors
- *
- * @param \ScssPhp\ScssPhp\Compiler\Environment $env
- * @param \ScssPhp\ScssPhp\Block $selfParent
- *
- * @return array
- */
- protected function multiplySelectors(Environment $env, $selfParent = null)
- {
- $envs = $this->compactEnv($env);
- $selectors = [];
- $parentSelectors = [[]];
-
- $selfParentSelectors = null;
-
- if (! \is_null($selfParent) && $selfParent->selectors) {
- $selfParentSelectors = $this->evalSelectors($selfParent->selectors);
- }
-
- while ($env = array_pop($envs)) {
- if (empty($env->selectors)) {
- continue;
- }
-
- $selectors = $env->selectors;
-
- do {
- $stillHasSelf = false;
- $prevSelectors = $selectors;
- $selectors = [];
-
- foreach ($parentSelectors as $parent) {
- foreach ($prevSelectors as $selector) {
- if ($selfParentSelectors) {
- foreach ($selfParentSelectors as $selfParent) {
- // if no '&' in the selector, each call will give same result, only add once
- $s = $this->joinSelectors($parent, $selector, $stillHasSelf, $selfParent);
- $selectors[serialize($s)] = $s;
- }
- } else {
- $s = $this->joinSelectors($parent, $selector, $stillHasSelf);
- $selectors[serialize($s)] = $s;
- }
- }
- }
- } while ($stillHasSelf);
-
- $parentSelectors = $selectors;
- }
-
- $selectors = array_values($selectors);
-
- // case we are just starting a at-root : nothing to multiply but parentSelectors
- if (! $selectors && $selfParentSelectors) {
- $selectors = $selfParentSelectors;
- }
-
- return $selectors;
- }
-
- /**
- * Join selectors; looks for & to replace, or append parent before child
- *
- * @param array $parent
- * @param array $child
- * @param bool $stillHasSelf
- * @param array $selfParentSelectors
-
- * @return array
- */
- protected function joinSelectors($parent, $child, &$stillHasSelf, $selfParentSelectors = null)
- {
- $setSelf = false;
- $out = [];
-
- foreach ($child as $part) {
- $newPart = [];
-
- foreach ($part as $p) {
- // only replace & once and should be recalled to be able to make combinations
- if ($p === static::$selfSelector && $setSelf) {
- $stillHasSelf = true;
- }
-
- if ($p === static::$selfSelector && ! $setSelf) {
- $setSelf = true;
-
- if (\is_null($selfParentSelectors)) {
- $selfParentSelectors = $parent;
- }
-
- foreach ($selfParentSelectors as $i => $parentPart) {
- if ($i > 0) {
- $out[] = $newPart;
- $newPart = [];
- }
-
- foreach ($parentPart as $pp) {
- if (\is_array($pp)) {
- $flatten = [];
-
- array_walk_recursive($pp, function ($a) use (&$flatten) {
- $flatten[] = $a;
- });
-
- $pp = implode($flatten);
- }
-
- $newPart[] = $pp;
- }
- }
- } else {
- $newPart[] = $p;
- }
- }
-
- $out[] = $newPart;
- }
-
- return $setSelf ? $out : array_merge($parent, $child);
- }
-
- /**
- * Multiply media
- *
- * @param \ScssPhp\ScssPhp\Compiler\Environment $env
- * @param array $childQueries
- *
- * @return array
- */
- protected function multiplyMedia(Environment $env = null, $childQueries = null)
- {
- if (
- ! isset($env) ||
- ! empty($env->block->type) && $env->block->type !== Type::T_MEDIA
- ) {
- return $childQueries;
- }
-
- // plain old block, skip
- if (empty($env->block->type)) {
- return $this->multiplyMedia($env->parent, $childQueries);
- }
-
- assert($env->block instanceof MediaBlock);
-
- $parentQueries = isset($env->block->queryList)
- ? $env->block->queryList
- : [[[Type::T_MEDIA_VALUE, $env->block->value]]];
-
- $store = [$this->env, $this->storeEnv];
-
- $this->env = $env;
- $this->storeEnv = null;
- $parentQueries = $this->evaluateMediaQuery($parentQueries);
-
- list($this->env, $this->storeEnv) = $store;
-
- if (\is_null($childQueries)) {
- $childQueries = $parentQueries;
- } else {
- $originalQueries = $childQueries;
- $childQueries = [];
-
- foreach ($parentQueries as $parentQuery) {
- foreach ($originalQueries as $childQuery) {
- $childQueries[] = array_merge(
- $parentQuery,
- [[Type::T_MEDIA_TYPE, [Type::T_KEYWORD, 'all']]],
- $childQuery
- );
- }
- }
- }
-
- return $this->multiplyMedia($env->parent, $childQueries);
- }
-
- /**
- * Convert env linked list to stack
- *
- * @param Environment $env
- *
- * @return Environment[]
- *
- * @phpstan-return non-empty-array<Environment>
- */
- protected function compactEnv(Environment $env)
- {
- for ($envs = []; $env; $env = $env->parent) {
- $envs[] = $env;
- }
-
- return $envs;
- }
-
- /**
- * Convert env stack to singly linked list
- *
- * @param Environment[] $envs
- *
- * @return Environment
- *
- * @phpstan-param non-empty-array<Environment> $envs
- */
- protected function extractEnv($envs)
- {
- for ($env = null; $e = array_pop($envs);) {
- $e->parent = $env;
- $env = $e;
- }
-
- return $env;
- }
-
- /**
- * Push environment
- *
- * @param \ScssPhp\ScssPhp\Block $block
- *
- * @return \ScssPhp\ScssPhp\Compiler\Environment
- */
- protected function pushEnv(Block $block = null)
- {
- $env = new Environment();
- $env->parent = $this->env;
- $env->parentStore = $this->storeEnv;
- $env->store = [];
- $env->block = $block;
- $env->depth = isset($this->env->depth) ? $this->env->depth + 1 : 0;
-
- $this->env = $env;
- $this->storeEnv = null;
-
- return $env;
- }
-
- /**
- * Pop environment
- *
- * @return void
- */
- protected function popEnv()
- {
- $this->storeEnv = $this->env->parentStore;
- $this->env = $this->env->parent;
- }
-
- /**
- * Propagate vars from a just poped Env (used in @each and @for)
- *
- * @param array $store
- * @param null|string[] $excludedVars
- *
- * @return void
- */
- protected function backPropagateEnv($store, $excludedVars = null)
- {
- foreach ($store as $key => $value) {
- if (empty($excludedVars) || ! \in_array($key, $excludedVars)) {
- $this->set($key, $value, true);
- }
- }
- }
-
- /**
- * Get store environment
- *
- * @return \ScssPhp\ScssPhp\Compiler\Environment
- */
- protected function getStoreEnv()
- {
- return isset($this->storeEnv) ? $this->storeEnv : $this->env;
- }
-
- /**
- * Set variable
- *
- * @param string $name
- * @param mixed $value
- * @param bool $shadow
- * @param \ScssPhp\ScssPhp\Compiler\Environment $env
- * @param mixed $valueUnreduced
- *
- * @return void
- */
- protected function set($name, $value, $shadow = false, Environment $env = null, $valueUnreduced = null)
- {
- $name = $this->normalizeName($name);
-
- if (! isset($env)) {
- $env = $this->getStoreEnv();
- }
-
- if ($shadow) {
- $this->setRaw($name, $value, $env, $valueUnreduced);
- } else {
- $this->setExisting($name, $value, $env, $valueUnreduced);
- }
- }
-
- /**
- * Set existing variable
- *
- * @param string $name
- * @param mixed $value
- * @param \ScssPhp\ScssPhp\Compiler\Environment $env
- * @param mixed $valueUnreduced
- *
- * @return void
- */
- protected function setExisting($name, $value, Environment $env, $valueUnreduced = null)
- {
- $storeEnv = $env;
- $specialContentKey = static::$namespaces['special'] . 'content';
-
- $hasNamespace = $name[0] === '^' || $name[0] === '@' || $name[0] === '%';
-
- $maxDepth = 10000;
-
- for (;;) {
- if ($maxDepth-- <= 0) {
- break;
- }
-
- if (\array_key_exists($name, $env->store)) {
- break;
- }
-
- if (! $hasNamespace && isset($env->marker)) {
- if (! empty($env->store[$specialContentKey])) {
- $env = $env->store[$specialContentKey]->scope;
- continue;
- }
-
- if (! empty($env->declarationScopeParent)) {
- $env = $env->declarationScopeParent;
- continue;
- } else {
- $env = $storeEnv;
- break;
- }
- }
-
- if (isset($env->parentStore)) {
- $env = $env->parentStore;
- } elseif (isset($env->parent)) {
- $env = $env->parent;
- } else {
- $env = $storeEnv;
- break;
- }
- }
-
- $env->store[$name] = $value;
-
- if ($valueUnreduced) {
- $env->storeUnreduced[$name] = $valueUnreduced;
- }
- }
-
- /**
- * Set raw variable
- *
- * @param string $name
- * @param mixed $value
- * @param \ScssPhp\ScssPhp\Compiler\Environment $env
- * @param mixed $valueUnreduced
- *
- * @return void
- */
- protected function setRaw($name, $value, Environment $env, $valueUnreduced = null)
- {
- $env->store[$name] = $value;
-
- if ($valueUnreduced) {
- $env->storeUnreduced[$name] = $valueUnreduced;
- }
- }
-
- /**
- * Get variable
- *
- * @internal
- *
- * @param string $name
- * @param bool $shouldThrow
- * @param \ScssPhp\ScssPhp\Compiler\Environment $env
- * @param bool $unreduced
- *
- * @return mixed|null
- */
- public function get($name, $shouldThrow = true, Environment $env = null, $unreduced = false)
- {
- $normalizedName = $this->normalizeName($name);
- $specialContentKey = static::$namespaces['special'] . 'content';
-
- if (! isset($env)) {
- $env = $this->getStoreEnv();
- }
-
- $hasNamespace = $normalizedName[0] === '^' || $normalizedName[0] === '@' || $normalizedName[0] === '%';
-
- $maxDepth = 10000;
-
- for (;;) {
- if ($maxDepth-- <= 0) {
- break;
- }
-
- if (\array_key_exists($normalizedName, $env->store)) {
- if ($unreduced && isset($env->storeUnreduced[$normalizedName])) {
- return $env->storeUnreduced[$normalizedName];
- }
-
- return $env->store[$normalizedName];
- }
-
- if (! $hasNamespace && isset($env->marker)) {
- if (! empty($env->store[$specialContentKey])) {
- $env = $env->store[$specialContentKey]->scope;
- continue;
- }
-
- if (! empty($env->declarationScopeParent)) {
- $env = $env->declarationScopeParent;
- } else {
- $env = $this->rootEnv;
- }
- continue;
- }
-
- if (isset($env->parentStore)) {
- $env = $env->parentStore;
- } elseif (isset($env->parent)) {
- $env = $env->parent;
- } else {
- break;
- }
- }
-
- if ($shouldThrow) {
- throw $this->error("Undefined variable \$$name" . ($maxDepth <= 0 ? ' (infinite recursion)' : ''));
- }
-
- // found nothing
- return null;
- }
-
- /**
- * Has variable?
- *
- * @param string $name
- * @param \ScssPhp\ScssPhp\Compiler\Environment $env
- *
- * @return bool
- */
- protected function has($name, Environment $env = null)
- {
- return ! \is_null($this->get($name, false, $env));
- }
-
- /**
- * Inject variables
- *
- * @param array $args
- *
- * @return void
- */
- protected function injectVariables(array $args)
- {
- if (empty($args)) {
- return;
- }
-
- $parser = $this->parserFactory(__METHOD__);
-
- foreach ($args as $name => $strValue) {
- if ($name[0] === '$') {
- $name = substr($name, 1);
- }
-
- if (!\is_string($strValue) || ! $parser->parseValue($strValue, $value)) {
- $value = $this->coerceValue($strValue);
- }
-
- $this->set($name, $value);
- }
- }
-
- /**
* Replaces variables.
*
- * @param array<string, mixed> $variables
- *
- * @return void
+ * @param array<string, Value> $variables
*/
- public function replaceVariables(array $variables)
+ public function replaceVariables(array $variables): void
{
$this->registeredVars = [];
$this->addVariables($variables);
@@ -5412,55 +154,23 @@ EOL;
/**
* Replaces variables.
*
- * @param array<string, mixed> $variables
- *
- * @return void
+ * @param array<string, Value> $variables
*/
- public function addVariables(array $variables)
+ public function addVariables(array $variables): void
{
- $triggerWarning = false;
-
foreach ($variables as $name => $value) {
- if (!$value instanceof Number && !\is_array($value)) {
- $triggerWarning = true;
+ if (!$value instanceof 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.');
}
$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($name)
+ public function unsetVariable(string $name): void
{
unset($this->registeredVars[$name]);
}
@@ -5468,53 +178,24 @@ EOL;
/**
* Returns list of variables
*
- * @api
- *
- * @return array
+ * @return array<string, Value>
*/
- public function getVariables()
+ public function getVariables(): array
{
return $this->registeredVars;
}
- /**
- * Adds to list of parsed files
- *
- * @internal
- *
- * @param string|null $path
- *
- * @return 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<string, int>
- */
- public function getParsedFiles()
+ public function addImporter(Importer $importer): void
{
- @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;
+ $this->importers[] = $importer;
}
/**
* Add import path
*
- * @api
- *
- * @param string|callable $path
- *
- * @return void
+ * @param string|callable(string): (string|null) $path
*/
- public function addImportPath($path)
+ public function addImportPath(string|callable $path): void
{
if (! \in_array($path, $this->importPaths)) {
$this->importPaths[] = $path;
@@ -5524,110 +205,28 @@ EOL;
/**
* Set import paths
*
- * @api
- *
- * @param string|array<string|callable> $path
- *
- * @return void
+ * @param string|array<string|callable(string): (string|null)> $path
*/
- public function setImportPaths($path)
+ public function setImportPaths($path): void
{
$paths = (array) $path;
$actualImportPaths = array_filter($paths, function ($path) {
return $path !== '';
});
- $this->legacyCwdImportPath = \count($actualImportPaths) !== \count($paths);
-
- if ($this->legacyCwdImportPath) {
- @trigger_error('Passing an empty string in the import paths to refer to the current working directory is deprecated. If that\'s the intended behavior, the value of "getcwd()" should be used directly instead. If this was used for resolving relative imports of the input alongside "chdir" with the source directory, the path of the input file should be passed to "compileString()" instead.', E_USER_DEPRECATED);
+ 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->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($style)
+ public function setOutputStyle(OutputStyle $style): void
{
- switch ($style) {
- case OutputStyle::EXPANDED:
- $this->configuredFormatter = Expanded::class;
- break;
-
- case OutputStyle::COMPRESSED:
- $this->configuredFormatter = Compressed::class;
- break;
-
- default:
- throw new \InvalidArgumentException(sprintf('Invalid output style "%s".', $style));
- }
- }
-
- /**
- * Set formatter
- *
- * @api
- *
- * @param string $formatterName
- *
- * @return void
- *
- * @deprecated Use {@see setOutputStyle} instead.
- *
- * @phpstan-param class-string<Formatter> $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);
+ $this->outputStyle = $style;
}
/**
@@ -5639,1712 +238,454 @@ EOL;
* byte-order mark.
*
* [byte-order mark]: https://en.wikipedia.org/wiki/Byte_order_mark#UTF-8
- *
- * @param bool $charset
- *
- * @return void
*/
- public function setCharset($charset)
+ public function setCharset(bool $charset): void
{
$this->charset = $charset;
}
/**
- * Enable/disable source maps
- *
- * @api
- *
- * @param int $sourceMap
- *
- * @return void
- *
- * @phpstan-param self::SOURCE_MAP_* $sourceMap
- */
- public function setSourceMap($sourceMap)
- {
- $this->sourceMap = $sourceMap;
- }
-
- /**
- * 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($sourceMapOptions)
- {
- $this->sourceMapOptions = $sourceMapOptions;
- }
-
- /**
- * Register function
- *
- * @api
- *
- * @param string $name
- * @param callable $callback
- * @param string[]|null $argumentDeclaration
- *
- * @return void
- */
- public function registerFunction($name, $callback, $argumentDeclaration = null)
- {
- if (self::isNativeFunction($name)) {
- @trigger_error(sprintf('The "%s" function is a core sass function. Overriding it with a custom implementation through "%s" is deprecated and won\'t be supported in ScssPhp 2.0 anymore.', $name, __METHOD__), E_USER_DEPRECATED);
- }
-
- if ($argumentDeclaration === null) {
- @trigger_error('Omitting the argument declaration when registering custom function is deprecated and won\'t be supported in ScssPhp 2.0 anymore.', E_USER_DEPRECATED);
- }
-
- $this->userFunctions[$this->normalizeName($name)] = [$callback, $argumentDeclaration];
- }
-
- /**
- * Unregister function
- *
- * @api
- *
- * @param string $name
- *
- * @return 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 \ScssPhp\ScssPhp\Formatter\OutputBlock $out
- *
- * @return void
+ * If set to `true`, this will silence compiler warnings emitted for stylesheets loaded through {@see $importers} or {@see $importPaths}
*/
- protected function importFile($path, OutputBlock $out)
+ public function setQuietDeps(bool $quietDeps): void
{
- $this->pushCallStack('import ' . $this->getPrettyPath($path));
- // see if tree is cached
- $realPath = realpath($path);
-
- if ($realPath === false) {
- $realPath = $path;
- }
-
- if (substr($path, -5) === '.sass') {
- $this->sourceIndex = \count($this->sourceNames);
- $this->sourceNames[] = $path;
- $this->sourceLine = 1;
- $this->sourceColumn = 1;
-
- throw $this->error('The Sass indented syntax is not implemented.');
- }
-
- if (isset($this->importCache[$realPath])) {
- $this->handleImportLoop($realPath);
-
- $tree = $this->importCache[$realPath];
- } else {
- $code = file_get_contents($path);
- $parser = $this->parserFactory($path);
- $tree = $parser->parse($code);
-
- $this->importCache[$realPath] = $tree;
- }
-
- $currentDirectory = $this->currentDirectory;
- $this->currentDirectory = dirname($path);
-
- $this->compileChildrenNoReturn($tree->children, $out);
- $this->currentDirectory = $currentDirectory;
- $this->popCallStack();
- }
-
- /**
- * Save the imported files with their resolving path context
- *
- * @param string|null $currentDirectory
- * @param string $path
- * @param string $filePath
- *
- * @return void
- */
- private function registerImport($currentDirectory, $path, $filePath)
- {
- $this->resolvedImports[] = ['currentDir' => $currentDirectory, 'path' => $path, 'filePath' => $filePath];
- }
-
- /**
- * Detects whether the import is a CSS import.
- *
- * For legacy reasons, custom importers are called for those, allowing them
- * to replace them with an actual Sass import. However this behavior is
- * deprecated. Custom importers are expected to return null when they receive
- * a CSS import.
- *
- * @param string $url
- *
- * @return bool
- */
- public static function isCssImport($url)
- {
- return 1 === preg_match('~\.css$|^https?://|^//~', $url);
- }
-
- /**
- * Return the file path for an import url if it exists
- *
- * @internal
- *
- * @param string $url
- * @param string|null $currentDir
- *
- * @return string|null
- */
- public function findImport($url, $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;
- }
-
- if (!\is_null($currentDir)) {
- $relativePath = $this->resolveImportPath($url, $currentDir);
-
- if (!\is_null($relativePath)) {
- return $relativePath;
- }
- }
-
- foreach ($this->importPaths as $dir) {
- if (\is_string($dir)) {
- $path = $this->resolveImportPath($url, $dir);
-
- if (!\is_null($path)) {
- return $path;
- }
- } elseif (\is_callable($dir)) {
- // check custom callback for import path
- $file = \call_user_func($dir, $url);
-
- if (! \is_null($file)) {
- return $file;
- }
- }
- }
-
- if ($this->legacyCwdImportPath) {
- $path = $this->resolveImportPath($url, getcwd());
-
- if (!\is_null($path)) {
- @trigger_error('Resolving imports relatively to the current working directory is deprecated. If that\'s the intended behavior, the value of "getcwd()" should be added as an import path explicitly instead. If this was used for resolving relative imports of the input alongside "chdir" with the source directory, the path of the input file should be passed to "compileString()" instead.', E_USER_DEPRECATED);
-
- return $path;
- }
- }
-
- throw $this->error("`$url` file not found for @import");
+ $this->quietDeps = $quietDeps;
}
/**
- * @param string $url
- * @param string $baseDir
+ * Configures the deprecation warning types that will be ignored.
*
- * @return string|null
+ * @param Deprecation[] $silenceDeprecations
*/
- private function resolveImportPath($url, $baseDir)
+ public function setSilenceDeprecations(array $silenceDeprecations): void
{
- $path = Path::join($baseDir, $url);
-
- $hasExtension = preg_match('/.s[ac]ss$/', $url);
-
- if ($hasExtension) {
- return $this->checkImportPathConflicts($this->tryImportPath($path));
- }
-
- $result = $this->checkImportPathConflicts($this->tryImportPathWithExtensions($path));
-
- if (!\is_null($result)) {
- return $result;
- }
-
- return $this->tryImportPathAsDirectory($path);
+ $this->silenceDeprecations = $silenceDeprecations;
}
/**
- * @param string[] $paths
+ * Configures the deprecation warning types that will cause an error to be thrown.
*
- * @return string|null
+ * @param Deprecation[] $fatalDeprecations
*/
- private function checkImportPathConflicts(array $paths)
+ public function setFatalDeprecations(array $fatalDeprecations): void
{
- if (\count($paths) === 0) {
- return null;
- }
-
- if (\count($paths) === 1) {
- return $paths[0];
- }
-
- $formattedPrettyPaths = [];
-
- foreach ($paths as $path) {
- $formattedPrettyPaths[] = ' ' . $this->getPrettyPath($path);
- }
-
- throw $this->error("It's not clear which file to import. Found:\n" . implode("\n", $formattedPrettyPaths));
+ $this->fatalDeprecations = $fatalDeprecations;
}
/**
- * @param string $path
+ * Configures the opt-in for future deprecation warning types.
*
- * @return string[]
+ * @param Deprecation[] $futureDeprecations
*/
- private function tryImportPathWithExtensions($path)
+ public function setFutureDeprecations(array $futureDeprecations): void
{
- $result = array_merge(
- $this->tryImportPath($path . '.sass'),
- $this->tryImportPath($path . '.scss')
- );
-
- if ($result) {
- return $result;
- }
-
- return $this->tryImportPath($path . '.css');
+ $this->futureDeprecations = $futureDeprecations;
}
/**
- * @param string $path
+ * Configures the verbosity of deprecation warnings.
*
- * @return string[]
+ * In non-verbose mode, repeated deprecations are hidden once reaching the
+ * threshold, with a summary at the end. In verbose mode, all deprecation
+ * warnings are emitted to the logger.
*/
- private function tryImportPath($path)
+ public function setVerbose(bool $verbose): void
{
- $partial = dirname($path) . '/_' . basename($path);
-
- $candidates = [];
-
- if (is_file($partial)) {
- $candidates[] = $partial;
- }
-
- if (is_file($path)) {
- $candidates[] = $path;
- }
-
- return $candidates;
+ $this->verbose = $verbose;
}
/**
- * @param string $path
+ * Enable/disable source maps
*
- * @return string|null
+ * @param self::SOURCE_MAP_* $sourceMap
*/
- private function tryImportPathAsDirectory($path)
+ public function setSourceMap(int $sourceMap): void
{
- if (!is_dir($path)) {
- return null;
- }
-
- return $this->checkImportPathConflicts($this->tryImportPathWithExtensions($path . '/index'));
+ $this->sourceMap = $sourceMap;
}
/**
- * @param string|null $path
+ * Set source map options
*
- * @return string
+ * @param array{sourceRoot?: string, sourceMapFilename?: string|null, sourceMapURL?: string|null, outputSourceFiles?: bool, sourceMapRootpath?: string, sourceMapBasepath?: string} $sourceMapOptions
*/
- private function getPrettyPath($path)
+ public function setSourceMapOptions(array $sourceMapOptions): void
{
- if ($path === null) {
- return '(unknown file)';
- }
-
- $normalizedPath = $path;
- $normalizedRootDirectory = $this->rootDirectory . '/';
-
- if (\DIRECTORY_SEPARATOR === '\\') {
- $normalizedRootDirectory = str_replace('\\', '/', $normalizedRootDirectory);
- $normalizedPath = str_replace('\\', '/', $path);
- }
-
- if (0 === strpos($normalizedPath, $normalizedRootDirectory)) {
- return substr($path, \strlen($normalizedRootDirectory));
- }
-
- return $path;
+ $this->sourceMapOptions = $sourceMapOptions;
}
/**
- * Set encoding
- *
- * @api
- *
- * @param string|null $encoding
- *
- * @return void
+ * Registers a custom function
*
- * @deprecated Non-compliant support for other encodings than UTF-8 is deprecated.
+ * @param (callable(list<Value>): Value)|(callable(list<array|Number>): (array|Number)) $callback
+ * @param string[] $argumentDeclaration
*/
- public function setEncoding($encoding)
+ public function registerFunction(string $name, callable $callback, array $argumentDeclaration): void
{
- 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);
+ $normalizedName = $this->normalizeName($name);
+ if (FunctionRegistry::isBuiltinFunction($normalizedName)) {
+ 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__));
}
- $this->encoding = $encoding;
+ $this->userFunctions[$normalizedName] = [$callback, $argumentDeclaration];
}
/**
- * Ignore errors?
- *
- * @api
- *
- * @param bool $ignoreErrors
- *
- * @return \ScssPhp\ScssPhp\Compiler
- *
- * @deprecated Ignoring Sass errors is not longer supported.
+ * Unregisters a custom function
*/
- public function setIgnoreErrors($ignoreErrors)
+ public function unregisterFunction(string $name): void
{
- @trigger_error('Ignoring Sass errors is not longer supported.', E_USER_DEPRECATED);
-
- return $this;
+ unset($this->userFunctions[$this->normalizeName($name)]);
}
- /**
- * Get source position
- *
- * @api
- *
- * @return array
- *
- * @deprecated
- */
- public function getSourcePosition()
+ private function normalizeName(string $name): string
{
- @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];
+ return str_replace('-', '_', $name);
}
/**
- * Throw error (exception)
+ * Compiles the provided scss file into CSS.
*
- * @api
+ * Imports are resolved by trying, in order:
*
- * @param string $msg Message with optional sprintf()-style vararg parameters
+ * * Loading a file relative to $path.
*
- * @return never
+ * * Each importer in {@see $importers}.
*
- * @throws \ScssPhp\ScssPhp\Exception\CompilerException
+ * * Each load path in {@see $importPaths}. Note that this is a shorthand for adding
+ * {@see FilesystemImporter}s to {@see $importers}.
*
- * @deprecated use "error" and throw the exception in the caller instead.
- */
- public function throwError($msg)
- {
- @trigger_error(
- 'The method "throwError" is deprecated. Use "error" and throw the exception in the caller instead',
- E_USER_DEPRECATED
- );
-
- throw $this->error(...func_get_args());
- }
-
- /**
- * Build an error (exception)
- *
- * @internal
- *
- * @param string $msg Message with optional sprintf()-style vararg parameters
- * @param bool|float|int|string|null ...$args
- *
- * @return CompilerException
- */
- public function error($msg, ...$args)
- {
- if ($args) {
- $msg = sprintf($msg, ...$args);
- }
-
- if (! $this->ignoreCallStackMessage) {
- $msg = $this->addLocationToMessage($msg);
- }
-
- return new CompilerException($msg);
- }
-
- /**
- * @param string $msg
- *
- * @return string
+ * @throws SassException when the source fails to compile
*/
- private function addLocationToMessage($msg)
+ public function compileFile(string $path): CompilationResult
{
- $line = $this->sourceLine;
- $column = $this->sourceColumn;
+ // Force loading the CssParentNode and CssVisitor before using the AST classes because of a weird PHP behavior.
+ class_exists(CssParentNode::class);
+ class_exists(CssVisitor::class);
- $loc = isset($this->sourceNames[$this->sourceIndex])
- ? $this->getPrettyPath($this->sourceNames[$this->sourceIndex]) . " on line $line, at column $column"
- : "line: $line, column: $column";
+ $logger = new DeprecationProcessingLogger($this->logger, $this->silenceDeprecations, $this->fatalDeprecations, $this->futureDeprecations, !$this->verbose);
+ $logger->validate();
+ $importCache = $this->createImportCache($logger);
- $msg = "$msg: $loc";
+ $importer = new FilesystemImporter(null);
- $callStackMsg = $this->callStackMessage();
+ $stylesheet = $importCache->importCanonical($importer, Path::toUri(Path::canonicalize($path)), Path::toUri($path));
- if ($callStackMsg) {
- $msg .= "\nCall Stack:\n" . $callStackMsg;
- }
+ \assert($stylesheet !== null, 'The filesystem importer never returns null when loading a canonical URL. It either succeeds or throws an error.');
- return $msg;
- }
+ $result = $this->compileStylesheet($stylesheet, $importCache, $logger, $importer);
- /**
- * @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));
- }
+ $logger->summarize();
- return $this->error(
- 'Error: %s() argument%s %s missing.',
- $functionName,
- count($missing) > 1 ? 's' : '',
- implode(', ', $missing)
- );
- }
+ return $result;
}
/**
- * Beautify call stack for output
- *
- * @param bool $all
- * @param int|null $limit
+ * Compiles the provided scss source code into CSS.
*
- * @return string
- */
- protected function callStackMessage($all = false, $limit = null)
- {
- $callStackMsg = [];
- $ncall = 0;
-
- if ($this->callStack) {
- foreach (array_reverse($this->callStack) as $call) {
- if ($all || (isset($call['n']) && $call['n'])) {
- $msg = '#' . $ncall++ . ' ' . $call['n'] . ' ';
- $msg .= (isset($this->sourceNames[$call[Parser::SOURCE_INDEX]])
- ? $this->getPrettyPath($this->sourceNames[$call[Parser::SOURCE_INDEX]])
- : '(unknown file)');
- $msg .= ' on line ' . $call[Parser::SOURCE_LINE];
-
- $callStackMsg[] = $msg;
-
- if (! \is_null($limit) && $ncall > $limit) {
- break;
- }
- }
- }
- }
-
- return implode("\n", $callStackMsg);
- }
-
- /**
- * Handle import loop
+ * Imports are resolved by trying, in order:
*
- * @param string $name
+ * * The given $importer, with the imported URL resolved relative to $url.
*
- * @return void
+ * * Each importer in {@see $importers}.
*
- * @throws \Exception
- */
- protected function handleImportLoop($name)
- {
- for ($env = $this->env; $env; $env = $env->parent) {
- if (! $env->block) {
- continue;
- }
-
- $file = $this->sourceNames[$env->block->sourceIndex];
-
- if ($file === null) {
- continue;
- }
-
- if (realpath($file) === $name) {
- throw $this->error('An @import loop has been found: %s imports %s', $file, basename($file));
- }
- }
- }
-
- /**
- * Call SCSS @function
+ * * Each load path in {@see $importPaths}. Note that this is a shorthand for adding
+ * {@see FilesystemImporter}s to {@see $importers}.
*
- * @param CallableBlock|null $func
- * @param array $argValues
+ * The $url indicates the location from which $source was loaded. If $importer is
+ * passed, $url must be passed as well and `$importer->load($url)` should
+ * return `$source`.
*
- * @return array|Number
+ * @throws SassException when the source fails to compile
*/
- protected function callScssFunction($func, $argValues)
+ public function compileString(string $source, UriInterface|string|null $url = null, ?Importer $importer = null, Syntax $syntax = Syntax::SCSS): CompilationResult
{
- if (! $func) {
- return static::$defaultValue;
- }
- $name = $func->name;
+ // Force loading the CssParentNode and CssVisitor before using the AST classes because of a weird PHP behavior.
+ class_exists(CssParentNode::class);
+ class_exists(CssVisitor::class);
- $this->pushEnv();
+ $logger = new DeprecationProcessingLogger($this->logger, $this->silenceDeprecations, $this->fatalDeprecations, $this->futureDeprecations, !$this->verbose);
+ $logger->validate();
- // set the args
- if (isset($func->args)) {
- $this->applyArguments($func->args, $argValues);
+ if (\is_string($url)) {
+ @trigger_error('Passing a path to "Compiler::compileString" is deprecated. Use `Compiler::compileFile" or pass a "UriInterface" instead.', E_USER_DEPRECATED);
+ $url = Path::toUri($url);
+ $importer ??= new FilesystemImporter(null);
}
- // throw away lines and children
- $tmp = new OutputBlock();
- $tmp->lines = [];
- $tmp->children = [];
-
- $this->env->marker = 'function';
+ $importCache = $this->createImportCache($logger);
+ $stylesheet = Stylesheet::parse($source, $syntax, $logger, $url);
- if (! empty($func->parentEnv)) {
- $this->env->declarationScopeParent = $func->parentEnv;
- } else {
- throw $this->error("@function $name() without parentEnv");
- }
+ $importer ??= $url === null ? new NoOpImporter() : new FilesystemImporter(null);
- $ret = $this->compileChildren($func->children, $tmp, $this->env->marker . ' ' . $name);
+ $result = $this->compileStylesheet($stylesheet, $importCache, $logger, $importer);
- $this->popEnv();
+ $logger->summarize();
- return ! isset($ret) ? static::$defaultValue : $ret;
+ return $result;
}
- /**
- * Call built-in and registered (PHP) functions
- *
- * @param string $name
- * @param callable $function
- * @param array $prototype
- * @param array $args
- *
- * @return array|Number|null
- */
- protected function callNativeFunction($name, $function, $prototype, $args)
+ private function createImportCache(LoggerInterface $logger): ImportCache
{
- $libName = (is_array($function) ? end($function) : null);
- $sorted_kwargs = $this->sortNativeFunctionArgs($libName, $prototype, $args);
-
- if (\is_null($sorted_kwargs)) {
- return null;
- }
- @list($sorted, $kwargs) = $sorted_kwargs;
+ $importers = $this->importers;
- if ($name !== 'if') {
- foreach ($sorted as &$val) {
- if ($val !== null) {
- $val = $this->reduce($val, true);
- }
+ foreach ($this->importPaths as $importPath) {
+ if (\is_string($importPath)) {
+ $importers[] = new FilesystemImporter($importPath);
+ } elseif (is_callable($importPath)) {
+ $importers[] = new LegacyCallbackImporter($importPath(...));
+ // TODO report deprecation
}
}
- $returnValue = \call_user_func($function, $sorted, $kwargs);
-
- if (! isset($returnValue)) {
- return null;
- }
-
- if (\is_array($returnValue) || $returnValue instanceof Number) {
- return $returnValue;
- }
-
- @trigger_error(sprintf('Returning a PHP value from the "%s" custom function is deprecated. A sass value must be returned instead.', $name), E_USER_DEPRECATED);
-
- return $this->coerceValue($returnValue);
+ return new ImportCache($importers, $logger);
}
/**
- * Get built-in function
- *
- * @param string $name Normalized name
- *
- * @return array
+ * @throws SassException
*/
- protected function getBuiltinFunction($name)
+ private function compileStylesheet(Stylesheet $stylesheet, ImportCache $importCache, LoggerInterface $logger, Importer $importer): CompilationResult
{
- $libName = self::normalizeNativeFunctionName($name);
- return [$this, $libName];
- }
+ $wantsSourceMap = $this->sourceMap !== self::SOURCE_MAP_NONE;
- /**
- * Normalize native function name
- *
- * @internal
- *
- * @param string $name
- *
- * @return string
- */
- public static function normalizeNativeFunctionName($name)
- {
- $name = str_replace("-", "_", $name);
- $libName = 'lib' . preg_replace_callback(
- '/_(.)/',
- function ($m) {
- return ucfirst($m[1]);
- },
- ucfirst($name)
- );
- return $libName;
- }
-
- /**
- * Check if a function is a native built-in scss function, for css parsing
- *
- * @internal
- *
- * @param string $name
- *
- * @return bool
- */
- public static function isNativeFunction($name)
- {
- return method_exists(Compiler::class, self::normalizeNativeFunctionName($name));
- }
+ $functions = [];
+ foreach ($this->userFunctions as $name => $userFunction) {
+ $ref = new \ReflectionFunction($userFunction[0](...));
+ $signature = implode(', ', array_map(fn (string $arg) => '$' . $arg, $userFunction[1]));
- /**
- * Sorts keyword arguments
- *
- * @param string $functionName
- * @param array|null $prototypes
- * @param array $args
- *
- * @return array|null
- */
- 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
- foreach ($args as $k => $arg) {
- if (!isset($arg[1])) {
- continue; // This happens when using a trailing comma
- }
- if ($arg[1][0] === Type::T_LIST && \count($arg[1][2]) === 3) {
- $args[$k][1][2] = $this->extractSlashAlphaInColorFunction($arg[1][2]);
- }
- }
- }
-
- list($positionalArgs, $namedArgs, $names, $separator, $hasSplat) = $this->evaluateArguments($args, false);
-
- if (! \is_array(reset($prototypes))) {
- $prototypes = [$prototypes];
- }
-
- $parsedPrototypes = array_map([$this, 'parseFunctionPrototype'], $prototypes);
- assert(!empty($parsedPrototypes));
- $matchedPrototype = $this->selectFunctionPrototype($parsedPrototypes, \count($positionalArgs), $names);
-
- $this->verifyPrototype($matchedPrototype, \count($positionalArgs), $names, $hasSplat);
-
- $vars = $this->applyArgumentsToDeclaration($matchedPrototype, $positionalArgs, $namedArgs, $separator);
-
- $finalArgs = [];
- $keyArgs = [];
-
- foreach ($matchedPrototype['arguments'] as $argument) {
- list($normalizedName, $originalName, $default) = $argument;
-
- if (isset($vars[$normalizedName])) {
- $value = $vars[$normalizedName];
+ if ($ref->hasReturnType() && $ref->getReturnType() instanceof \ReflectionNamedType && $ref->getReturnType()->getName() === Value::class) {
+ $callback = $userFunction[0];
} else {
- $value = $default;
- }
-
- // special null value as default: translate to real null here
- if ($value === [Type::T_KEYWORD, 'null']) {
- $value = null;
- }
-
- $finalArgs[] = $value;
- $keyArgs[$originalName] = $value;
- }
-
- if ($matchedPrototype['rest_argument'] !== null) {
- $value = $vars[$matchedPrototype['rest_argument']];
-
- $finalArgs[] = $value;
- $keyArgs[$matchedPrototype['rest_argument']] = $value;
- }
-
- return [$finalArgs, $keyArgs];
- }
+ $legacyCallback = $userFunction[0];
+ $callback = function (array $arguments) use ($legacyCallback): Value {
+ $args = [];
- /**
- * Parses a function prototype to the internal representation of arguments.
- *
- * The input is an array of strings describing each argument, as supported
- * in {@see registerFunction}. Argument names don't include the `$`.
- * The output contains the list of positional argument, with their normalized
- * name (underscores are replaced by dashes), their original name (to be used
- * in case of error reporting) and their default value. The output also contains
- * the normalized name of the rest argument, or null if the function prototype
- * is not variadic.
- *
- * @param string[] $prototype
- *
- * @return array
- * @phpstan-return array{arguments: list<array{0: string, 1: string, 2: array|Number|null}>, rest_argument: string|null}
- */
- private function parseFunctionPrototype(array $prototype)
- {
- static $parser = null;
-
- $arguments = [];
- $restArgument = null;
-
- foreach ($prototype as $p) {
- if (null !== $restArgument) {
- throw new \InvalidArgumentException('The argument declaration is invalid. The rest argument must be the last one.');
- }
-
- $default = null;
- $p = explode(':', $p, 2);
- $name = str_replace('_', '-', $p[0]);
-
- if (isset($p[1])) {
- $defaultSource = trim($p[1]);
-
- if ($defaultSource === 'null') {
- // differentiate this null from the static::$null
- $default = [Type::T_KEYWORD, 'null'];
- } else {
- if (\is_null($parser)) {
- $parser = $this->parserFactory(__METHOD__);
+ foreach ($arguments as $argument) {
+ $args[] = $this->valueToLegacyValue($argument);
}
- $parser->parseValue($defaultSource, $default);
- }
- }
-
- if (substr($name, -3) === '...') {
- $restArgument = substr($name, 0, -3);
- } else {
- $arguments[] = [$name, $p[0], $default];
- }
- }
-
- return [
- 'arguments' => $arguments,
- 'rest_argument' => $restArgument,
- ];
- }
-
- /**
- * Returns the function prototype for the given positional and named arguments.
- *
- * If no exact match is found, finds the closest approximation. Note that this
- * doesn't guarantee that $positional and $names are valid for the returned
- * prototype.
- *
- * @param array[] $prototypes
- * @param int $positional
- * @param array<string, string> $names A set of names, as both keys and values
- *
- * @return array
- *
- * @phpstan-param non-empty-array<array{arguments: list<array{0: string, 1: string, 2: array|Number|null}>, rest_argument: string|null}> $prototypes
- * @phpstan-return array{arguments: list<array{0: string, 1: string, 2: array|Number|null}>, rest_argument: string|null}
- */
- private function selectFunctionPrototype(array $prototypes, $positional, array $names)
- {
- $fuzzyMatch = null;
- $minMismatchDistance = null;
-
- foreach ($prototypes as $prototype) {
- // Ideally, find an exact match.
- if ($this->checkPrototypeMatches($prototype, $positional, $names)) {
- return $prototype;
- }
-
- $mismatchDistance = \count($prototype['arguments']) - $positional;
-
- if ($minMismatchDistance !== null) {
- if (abs($mismatchDistance) > abs($minMismatchDistance)) {
- continue;
- }
-
- // If two overloads have the same mismatch distance, favor the overload
- // that has more arguments.
- if (abs($mismatchDistance) === abs($minMismatchDistance) && $mismatchDistance < 0) {
- continue;
- }
- }
-
- $minMismatchDistance = $mismatchDistance;
- $fuzzyMatch = $prototype;
- }
-
- return $fuzzyMatch;
- }
-
- /**
- * Checks whether the argument invocation matches the callable prototype.
- *
- * The rules are similar to {@see verifyPrototype}. The boolean return value
- * avoids the overhead of building and catching exceptions when the reason of
- * not matching the prototype does not need to be known.
- *
- * @param array $prototype
- * @param int $positional
- * @param array<string, string> $names
- *
- * @return bool
- *
- * @phpstan-param array{arguments: list<array{0: string, 1: string, 2: array|Number|null}>, rest_argument: string|null} $prototype
- */
- private function checkPrototypeMatches(array $prototype, $positional, array $names)
- {
- $nameUsed = 0;
-
- foreach ($prototype['arguments'] as $i => $argument) {
- list ($name, $originalName, $default) = $argument;
-
- if ($i < $positional) {
- if (isset($names[$name])) {
- return false;
- }
- } elseif (isset($names[$name])) {
- $nameUsed++;
- } elseif ($default === null) {
- return false;
- }
- }
-
- if ($prototype['rest_argument'] !== null) {
- return true;
- }
-
- if ($positional > \count($prototype['arguments'])) {
- return false;
- }
-
- if ($nameUsed < \count($names)) {
- return false;
- }
-
- return true;
- }
-
- /**
- * Verifies that the argument invocation is valid for the callable prototype.
- *
- * @param array $prototype
- * @param int $positional
- * @param array<string, string> $names
- * @param bool $hasSplat
- *
- * @return void
- *
- * @throws SassScriptException
- *
- * @phpstan-param array{arguments: list<array{0: string, 1: string, 2: array|Number|null}>, rest_argument: string|null} $prototype
- */
- private function verifyPrototype(array $prototype, $positional, array $names, $hasSplat)
- {
- $nameUsed = 0;
+ $result = $legacyCallback($args);
- foreach ($prototype['arguments'] as $i => $argument) {
- list ($name, $originalName, $default) = $argument;
+ if ($result instanceof Value) {
+ return $result;
+ }
- if ($i < $positional) {
- if (isset($names[$name])) {
- throw new SassScriptException(sprintf('Argument $%s was passed both by position and by name.', $originalName));
- }
- } elseif (isset($names[$name])) {
- $nameUsed++;
- } elseif ($default === null) {
- throw new SassScriptException(sprintf('Missing argument $%s', $originalName));
+ return $this->legacyValueToValue($result);
+ };
}
+ $functions[] = BuiltInCallable::function($name, $signature, $callback);
}
- if ($prototype['rest_argument'] !== null) {
- return;
- }
-
- if ($positional > \count($prototype['arguments'])) {
- $message = sprintf(
- 'Only %d %sargument%s allowed, but %d %s passed.',
- \count($prototype['arguments']),
- empty($names) ? '' : 'positional ',
- \count($prototype['arguments']) === 1 ? '' : 's',
- $positional,
- $positional === 1 ? 'was' : 'were'
- );
- if (!$hasSplat) {
- throw new SassScriptException($message);
+ $initialVariables = [];
+ foreach ($this->registeredVars as $variableName => $variable) {
+ if ($variableName[0] === '$') {
+ $variableName = substr($variableName, 1);
}
-
- $message = $this->addLocationToMessage($message);
- $message .= "\nThis will be an error in future versions of Sass.";
- $this->logger->warn($message, true);
+ $variableName = str_replace('_', '-', $variableName);
+ $initialVariables[$variableName] = $variable;
}
- if ($nameUsed < \count($names)) {
- $unknownNames = array_values(array_diff($names, array_column($prototype['arguments'], 0)));
- $lastName = array_pop($unknownNames);
- $message = sprintf(
- 'No argument%s named $%s%s.',
- $unknownNames ? 's' : '',
- $unknownNames ? implode(', $', $unknownNames) . ' or $' : '',
- $lastName
- );
- throw new SassScriptException($message);
- }
- }
+ $evaluateResult = (new EvaluateVisitor($importCache, $functions, $logger, $this->quietDeps, sourceMap: $wantsSourceMap))->run($importer, $stylesheet, $initialVariables);
- /**
- * Evaluates the argument from the invocation.
- *
- * This returns several things about this invocation:
- * - the list of positional arguments
- * - the map of named arguments, indexed by normalized names
- * - the set of names used in the arguments (that's an array using the normalized names as keys for O(1) access)
- * - the separator used by the list using the splat operator, if any
- * - a boolean indicator whether any splat argument (list or map) was used, to support the incomplete error reporting.
- *
- * @param array[] $args
- * @param bool $reduce Whether arguments should be reduced to their value
- *
- * @return array
- *
- * @throws SassScriptException
- *
- * @phpstan-return array{0: list<array|Number>, 1: array<string, array|Number>, 2: array<string, string>, 3: string|null, 4: bool}
- */
- private function evaluateArguments(array $args, $reduce = true)
- {
- // this represents trailing commas
- if (\count($args) && end($args) === static::$null) {
- array_pop($args);
- }
+ $serializeResult = Serializer::serialize($evaluateResult->getStylesheet(), style: $this->outputStyle, sourceMap: $wantsSourceMap, charset: $this->charset, logger: $logger);
- $splatSeparator = null;
- $keywordArgs = [];
- $names = [];
- $positionalArgs = [];
- $hasKeywordArgument = false;
- $hasSplat = false;
+ $css = $serializeResult->css;
+ $sourceMap = null;
- foreach ($args as $arg) {
- if (!empty($arg[0])) {
- $hasKeywordArgument = true;
+ if ($serializeResult->mapping !== null) {
+ $mapping = $serializeResult->mapping;
- assert(\is_string($arg[0][1]));
- $name = str_replace('_', '-', $arg[0][1]);
+ if (isset($this->sourceMapOptions['sourceMapBasepath']) || isset($this->sourceMapOptions['sourceMapRootpath'])) {
+ $mapping = $mapping->mapUrls(function (string $url) {
+ $uri = Uri::new($url);
- if (isset($keywordArgs[$name])) {
- throw new SassScriptException(sprintf('Duplicate named argument $%s.', $arg[0][1]));
- }
-
- $keywordArgs[$name] = $this->maybeReduce($reduce, $arg[1]);
- $names[$name] = $name;
- } elseif (! empty($arg[2])) {
- // $arg[2] means a var followed by ... in the arg ($list... )
- $val = $this->reduce($arg[1], true);
- $hasSplat = true;
-
- if ($val[0] === Type::T_LIST) {
- foreach ($val[2] as $item) {
- if (\is_null($splatSeparator)) {
- $splatSeparator = $val[1];
- }
-
- $positionalArgs[] = $this->maybeReduce($reduce, $item);
+ if ($uri->getScheme() !== null && $uri->getScheme() !== 'file') {
+ return $uri->toString();
}
- if (isset($val[3]) && \is_array($val[3])) {
- foreach ($val[3] as $name => $item) {
- assert(\is_string($name));
+ $path = Path::fromUri($uri);
- $normalizedName = str_replace('_', '-', $name);
-
- if (isset($keywordArgs[$normalizedName])) {
- throw new SassScriptException(sprintf('Duplicate named argument $%s.', $name));
- }
-
- $keywordArgs[$normalizedName] = $this->maybeReduce($reduce, $item);
- $names[$normalizedName] = $normalizedName;
- $hasKeywordArgument = true;
- }
+ if (isset($this->sourceMapOptions['sourceMapBasepath']) && $this->sourceMapOptions['sourceMapBasepath'] !== '') {
+ $path = Path::relative($path, $this->sourceMapOptions['sourceMapBasepath']);
}
- } elseif ($val[0] === Type::T_MAP) {
- foreach ($val[1] as $i => $name) {
- $name = $this->compileStringContent($this->coerceString($name));
- $item = $val[2][$i];
-
- if (! is_numeric($name)) {
- $normalizedName = str_replace('_', '-', $name);
-
- if (isset($keywordArgs[$normalizedName])) {
- throw new SassScriptException(sprintf('Duplicate named argument $%s.', $name));
- }
-
- $keywordArgs[$normalizedName] = $this->maybeReduce($reduce, $item);
- $names[$normalizedName] = $normalizedName;
- $hasKeywordArgument = true;
- } else {
- if (\is_null($splatSeparator)) {
- $splatSeparator = $val[1];
- }
-
- $positionalArgs[] = $this->maybeReduce($reduce, $item);
- }
- }
- } elseif ($val[0] !== Type::T_NULL) { // values other than null are treated a single-element lists, while null is the empty list
- $positionalArgs[] = $this->maybeReduce($reduce, $val);
- }
- } elseif ($hasKeywordArgument) {
- throw new SassScriptException('Positional arguments must come before keyword arguments.');
- } else {
- $positionalArgs[] = $this->maybeReduce($reduce, $arg[1]);
- }
- }
-
- return [$positionalArgs, $keywordArgs, $names, $splatSeparator, $hasSplat];
- }
-
- /**
- * @param bool $reduce
- * @param array|Number $value
- *
- * @return array|Number
- */
- private function maybeReduce($reduce, $value)
- {
- if ($reduce) {
- return $this->reduce($value, true);
- }
-
- return $value;
- }
-
- /**
- * Apply argument values per definition
- *
- * @param array[] $argDef
- * @param array|null $argValues
- * @param bool $storeInEnv
- * @param bool $reduce only used if $storeInEnv = false
- *
- * @return array<string, array|Number>
- *
- * @phpstan-param list<array{0: string, 1: array|Number|null, 2: bool}> $argDef
- *
- * @throws \Exception
- */
- protected function applyArguments($argDef, $argValues, $storeInEnv = true, $reduce = true)
- {
- $output = [];
-
- if (\is_null($argValues)) {
- $argValues = [];
- }
-
- if ($storeInEnv) {
- $storeEnv = $this->getStoreEnv();
-
- $env = new Environment();
- $env->store = $storeEnv->store;
- }
-
- $prototype = ['arguments' => [], 'rest_argument' => null];
- $originalRestArgumentName = null;
-
- foreach ($argDef as $arg) {
- list($name, $default, $isVariable) = $arg;
- $normalizedName = str_replace('_', '-', $name);
- if ($isVariable) {
- $originalRestArgumentName = $name;
- $prototype['rest_argument'] = $normalizedName;
- } else {
- $prototype['arguments'][] = [$normalizedName, $name, !empty($default) ? $default : null];
+ return Path::normalize(Path::join($this->sourceMapOptions['sourceMapRootpath'] ?? '', $path));
+ });
}
- }
-
- list($positionalArgs, $namedArgs, $names, $splatSeparator, $hasSplat) = $this->evaluateArguments($argValues, $reduce);
-
- $this->verifyPrototype($prototype, \count($positionalArgs), $names, $hasSplat);
- $vars = $this->applyArgumentsToDeclaration($prototype, $positionalArgs, $namedArgs, $splatSeparator);
-
- foreach ($prototype['arguments'] as $argument) {
- list($normalizedName, $name) = $argument;
-
- if (!isset($vars[$normalizedName])) {
- continue;
+ if (isset($this->sourceMapOptions['sourceMapFilename'])) {
+ $mapping->targetUrl = $this->sourceMapOptions['sourceMapFilename'];
}
- $val = $vars[$normalizedName];
-
- if ($storeInEnv) {
- $this->set($name, $this->reduce($val, true), true, $env);
- } else {
- $output[$name] = ($reduce ? $this->reduce($val, true) : $val);
+ if (isset($this->sourceMapOptions['sourceRoot'])) {
+ $mapping->sourceRoot = $this->sourceMapOptions['sourceRoot'];
}
- }
-
- if ($prototype['rest_argument'] !== null) {
- assert($originalRestArgumentName !== null);
- $name = $originalRestArgumentName;
- $val = $vars[$prototype['rest_argument']];
- if ($storeInEnv) {
- $this->set($name, $this->reduce($val, true), true, $env);
- } else {
- $output[$name] = ($reduce ? $this->reduce($val, true) : $val);
- }
- }
+ $sourceMap = json_encode($mapping->toJson($this->sourceMapOptions['outputSourceFiles'] ?? false), \JSON_THROW_ON_ERROR);
- if ($storeInEnv) {
- $storeEnv->store = $env->store;
- }
+ $sourceMapUrl = null;
- foreach ($prototype['arguments'] as $argument) {
- list($normalizedName, $name, $default) = $argument;
+ switch ($this->sourceMap) {
+ case self::SOURCE_MAP_INLINE:
+ $sourceMapUrl = 'data:application/json;charset=utf-8,' . Util::encodeURIComponent($sourceMap);
+ break;
- if (isset($vars[$normalizedName])) {
- continue;
+ case self::SOURCE_MAP_FILE:
+ if (isset($this->sourceMapOptions['sourceMapURL'])) {
+ $sourceMapUrl = $this->sourceMapOptions['sourceMapURL'];
+ }
+ break;
}
- assert($default !== null);
- if ($storeInEnv) {
- $this->set($name, $this->reduce($default, true), true);
- } else {
- $output[$name] = ($reduce ? $this->reduce($default, true) : $default);
+ if ($sourceMapUrl !== null) {
+ $escapedUrl = str_replace('*/', '%2A/', $sourceMapUrl);
+
+ $css .= ($this->outputStyle === OutputStyle::COMPRESSED ? '' : "\n\n") . "/*# sourceMappingURL=$escapedUrl */";
}
}
- return $output;
+ return new CompilationResult($css, $sourceMap, $evaluateResult->getLoadedUrls());
}
/**
- * Apply argument values per definition.
- *
- * This method assumes that the arguments are valid for the provided prototype.
- * The validation with {@see verifyPrototype} must have been run before calling
- * it.
- * Arguments are returned as a map from the normalized argument names to the
- * value. Additional arguments are collected in a sass argument list available
- * under the name of the rest argument in the result.
- *
- * Defaults are not applied as they are resolved in a different environment.
- *
- * @param array $prototype
- * @param array<array|Number> $positionalArgs
- * @param array<string, array|Number> $namedArgs
- * @param string|null $splatSeparator
- *
- * @return array<string, array|Number>
+ * Converts a Sass value to its legacy representation.
*
- * @phpstan-param array{arguments: list<array{0: string, 1: string, 2: array|Number|null}>, rest_argument: string|null} $prototype
+ * @return array|Number
*/
- private function applyArgumentsToDeclaration(array $prototype, array $positionalArgs, array $namedArgs, $splatSeparator)
+ private function valueToLegacyValue(Value $value)
{
- $output = [];
- $minLength = min(\count($positionalArgs), \count($prototype['arguments']));
-
- for ($i = 0; $i < $minLength; $i++) {
- list($name) = $prototype['arguments'][$i];
- $val = $positionalArgs[$i];
-
- $output[$name] = $val;
- }
+ $visitor = new LegacyValueVisitor();
- $restNamed = $namedArgs;
-
- for ($i = \count($positionalArgs); $i < \count($prototype['arguments']); $i++) {
- $argument = $prototype['arguments'][$i];
- list($name) = $argument;
-
- if (isset($namedArgs[$name])) {
- $val = $namedArgs[$name];
- unset($restNamed[$name]);
- } else {
- continue;
- }
-
- $output[$name] = $val;
- }
-
- if ($prototype['rest_argument'] !== null) {
- $name = $prototype['rest_argument'];
- $rest = array_values(array_slice($positionalArgs, \count($prototype['arguments'])));
-
- $val = [Type::T_LIST, \is_null($splatSeparator) ? ',' : $splatSeparator , $rest, $restNamed];
-
- $output[$name] = $val;
- }
-
- return $output;
+ return $value->accept($visitor);
}
/**
- * Coerce a php value into a scss one
- *
- * @param mixed $value
+ * Converts a legacy Sass value to its modern representation.
*
- * @return array|Number
+ * @param array|Number $legacyValue
*/
- protected function coerceValue($value)
+ private function legacyValueToValue($legacyValue): Value
{
- if (\is_array($value) || $value instanceof Number) {
- return $value;
- }
-
- if (\is_bool($value)) {
- return $this->toBool($value);
+ if ($legacyValue instanceof Number) {
+ return SassNumber::withUnits($legacyValue->getDimension(), $legacyValue->getNumeratorUnits(), $legacyValue->getDenominatorUnits());
}
- if (\is_null($value)) {
- return static::$null;
- }
+ switch ($legacyValue[0]) {
+ case Type::T_KEYWORD:
+ if ($legacyValue === self::$true || $legacyValue === self::$false) {
+ return SassBoolean::create($legacyValue === self::$true);
+ }
- if (\is_int($value) || \is_float($value)) {
- return new Number($value, '');
- }
+ throw new \UnexpectedValueException('Unsupported value using the "keyword" type. Only boolean values should use it as their representation.');
- if (is_numeric($value)) {
- return new Number((float) $value, '');
- }
+ case Type::T_COLOR:
+ return SassColor::rgb($legacyValue[1], $legacyValue[2], $legacyValue[3], $legacyValue[4] ?? 1.0);
- if ($value === '') {
- return static::$emptyString;
- }
+ case Type::T_STRING:
+ return new SassString($this->getStringText($legacyValue), $legacyValue[1] !== '');
- $value = [Type::T_KEYWORD, $value];
- $color = $this->coerceColor($value);
+ case Type::T_LIST:
+ $items = [];
+ foreach ($legacyValue[2] as $item) {
+ $items[] = $this->legacyValueToValue($item);
+ }
+ $separator = match ($legacyValue[1]) {
+ ',' => ListSeparator::COMMA,
+ ' ' => ListSeparator::SPACE,
+ '/' => ListSeparator::SLASH,
+ '' => ListSeparator::UNDECIDED,
+ default => throw new \LogicException(\sprintf('Unsupported list separator "%s".', $legacyValue[1]))
+ };
- if ($color) {
- return $color;
- }
+ if (isset($legacyValue[3]) && \is_array($legacyValue[3])) {
+ $keywords = [];
+ foreach ($legacyValue[3] as $name => $item) {
+ assert(\is_string($name));
+ $keywords[$name] = $this->legacyValueToValue($item);
+ }
+ return new SassArgumentList($items, $keywords, $separator);
+ }
- return $value;
- }
+ $hasBrackets = ($legacyValue['enclosing'] ?? null) === 'bracket';
- /**
- * Tries to convert an item to a Sass map
- *
- * @param Number|array $item
- *
- * @return array|null
- */
- private function tryMap($item)
- {
- if ($item instanceof Number) {
- return null;
- }
+ return new SassList($items, $separator, $hasBrackets);
- if ($item[0] === Type::T_MAP) {
- return $item;
- }
+ case Type::T_MAP:
+ $map = new Map();
+ $keys = $legacyValue[1];
+ $values = $legacyValue[2];
- if (
- $item[0] === Type::T_LIST &&
- $item[2] === []
- ) {
- return static::$emptyMap;
- }
+ for ($i = 0, $s = \count($keys); $i < $s; $i++) {
+ $map->put($this->legacyValueToValue($keys[$i]), $this->legacyValueToValue($values[$i]));
+ }
- return null;
- }
+ return SassMap::create($map);
- /**
- * Coerce something to map
- *
- * @param array|Number $item
- *
- * @return array|Number
- */
- protected function coerceMap($item)
- {
- $map = $this->tryMap($item);
+ case Type::T_NULL:
+ return SassNull::create();
- if ($map !== null) {
- return $map;
+ default:
+ throw new \UnexpectedValueException(sprintf('"Unsupported type "%s" for the value conversion.', $legacyValue[0]));
}
-
- return $item;
}
/**
- * Coerce something to list
- *
- * @param array|Number $item
- * @param string $delim
- * @param bool $removeTrailingNull
- *
- * @return array
+ * Detects whether the import is a CSS import.
*/
- protected function coerceList($item, $delim = ',', $removeTrailingNull = false)
+ public static function isCssImport(string $url): bool
{
- if ($item instanceof Number) {
- return [Type::T_LIST, '', [$item]];
- }
-
- if ($item[0] === Type::T_LIST) {
- // remove trailing null from the list
- if ($removeTrailingNull && end($item[2]) === static::$null) {
- array_pop($item[2]);
- }
-
- return $item;
- }
-
- if ($item[0] === Type::T_MAP) {
- $keys = $item[1];
- $values = $item[2];
- $list = [];
-
- for ($i = 0, $s = \count($keys); $i < $s; $i++) {
- $key = $keys[$i];
- $value = $values[$i];
-
- $list[] = [
- Type::T_LIST,
- ' ',
- [$key, $value]
- ];
- }
-
- return [Type::T_LIST, $list ? ',' : '', $list];
- }
-
- return [Type::T_LIST, '', [$item]];
+ return 1 === preg_match('~\.css$|^https?://|^//~', $url);
}
/**
- * Coerce color for expression
+ * Is truthy?
*
* @param array|Number $value
- *
- * @return array|Number
*/
- protected function coerceForExpression($value)
+ public function isTruthy($value): bool
{
- if ($color = $this->coerceColor($value)) {
- return $color;
- }
-
- return $value;
+ return $value !== self::$false && $value !== self::$null;
}
/**
- * Coerce value to color
- *
- * @param array|Number $value
- * @param bool $inRGBFunction
- *
- * @return array|null
+ * Cast to Sass boolean
*/
- protected function coerceColor($value, $inRGBFunction = false)
+ public function toBool(bool $thing): array
{
- if ($value instanceof Number) {
- return null;
- }
-
- switch ($value[0]) {
- case Type::T_COLOR:
- for ($i = 1; $i <= 3; $i++) {
- if (! is_numeric($value[$i])) {
- $cv = $this->compileRGBAValue($value[$i]);
-
- if (! is_numeric($cv)) {
- return null;
- }
-
- $value[$i] = $cv;
- }
-
- if (isset($value[4])) {
- if (! is_numeric($value[4])) {
- $cv = $this->compileRGBAValue($value[4], true);
-
- if (! is_numeric($cv)) {
- return null;
- }
-
- $value[4] = $cv;
- }
- }
- }
-
- return $value;
-
- case Type::T_LIST:
- if ($inRGBFunction) {
- if (\count($value[2]) == 3 || \count($value[2]) == 4) {
- $color = $value[2];
- array_unshift($color, Type::T_COLOR);
-
- return $this->coerceColor($color);
- }
- }
-
- return null;
-
- case Type::T_KEYWORD:
- if (! \is_string($value[1])) {
- return null;
- }
-
- $name = strtolower($value[1]);
-
- // hexa color?
- if (preg_match('/^#([0-9a-f]+)$/i', $name, $m)) {
- $nofValues = \strlen($m[1]);
-
- if (\in_array($nofValues, [3, 4, 6, 8])) {
- $nbChannels = 3;
- $color = [];
- $num = hexdec($m[1]);
-
- switch ($nofValues) {
- case 4:
- $nbChannels = 4;
- // then continuing with the case 3:
- case 3:
- for ($i = 0; $i < $nbChannels; $i++) {
- $t = $num & 0xf;
- array_unshift($color, $t << 4 | $t);
- $num >>= 4;
- }
-
- break;
-
- case 8:
- $nbChannels = 4;
- // then continuing with the case 6:
- case 6:
- for ($i = 0; $i < $nbChannels; $i++) {
- array_unshift($color, $num & 0xff);
- $num >>= 8;
- }
-
- break;
- }
-
- if ($nbChannels === 4) {
- if ($color[3] === 255) {
- $color[3] = 1; // fully opaque
- } else {
- $color[3] = round($color[3] / 255, Number::PRECISION);
- }
- }
-
- array_unshift($color, Type::T_COLOR);
-
- return $color;
- }
- }
-
- if ($rgba = Colors::colorNameToRGBa($name)) {
- return isset($rgba[3])
- ? [Type::T_COLOR, $rgba[0], $rgba[1], $rgba[2], $rgba[3]]
- : [Type::T_COLOR, $rgba[0], $rgba[1], $rgba[2]];
- }
-
- return null;
- }
-
- return null;
+ return $thing ? self::$true : self::$false;
}
/**
- * @param int|Number $value
- * @param bool $isAlpha
+ * Gets the text of a Sass string
*
- * @return int|mixed
+ * Calling this method on anything else than a SassString is unsupported. Use {@see assertString} first
+ * to ensure that the value is indeed a string.
*/
- protected function compileRGBAValue($value, $isAlpha = false)
+ public function getStringText(array $value): string
{
- if ($isAlpha) {
- return $this->compileColorPartValue($value, 0, 1, false);
+ if ($value[0] !== Type::T_STRING) {
+ throw new \InvalidArgumentException('The argument is not a sass string. Did you forgot to use "assertString"?');
}
- return $this->compileColorPartValue($value, 0, 255, true);
+ return $this->compileStringContent($value);
}
/**
- * @param mixed $value
- * @param int|float $min
- * @param int|float $max
- * @param bool $isInt
- *
- * @return int|mixed
+ * Compile string content
*/
- protected function compileColorPartValue($value, $min, $max, $isInt = true)
+ private function compileStringContent(array $string): string
{
- if (! is_numeric($value)) {
- if (\is_array($value)) {
- $reduced = $this->reduce($value);
-
- if ($reduced instanceof Number) {
- $value = $reduced;
- }
- }
-
- if ($value instanceof Number) {
- if ($value->unitless()) {
- $num = $value->getDimension();
- } elseif ($value->hasUnit('%')) {
- $num = $max * $value->getDimension() / 100;
- } else {
- throw $this->error('Expected %s to have no units or "%%".', $value);
- }
-
- $value = $num;
- } elseif (\is_array($value)) {
- $value = $this->compileValue($value);
- }
- }
+ $parts = [];
- if (is_numeric($value)) {
- if ($isInt) {
- $value = round($value);
+ foreach ($string[2] as $part) {
+ if (\is_array($part) || $part instanceof Number) {
+ $parts[] = $this->compileValue($part);
+ } else {
+ $parts[] = $part;
}
-
- $value = min($max, max($min, $value));
-
- return $value;
- }
-
- return $value;
- }
-
- /**
- * Coerce value to string
- *
- * @param array|Number $value
- *
- * @return array
- */
- protected function coerceString($value)
- {
- if ($value[0] === Type::T_STRING) {
- assert(\is_array($value));
-
- return $value;
}
- return [Type::T_STRING, '', [$this->compileValue($value)]];
+ return implode($parts);
}
/**
@@ -7355,67 +696,30 @@ EOL;
* other types.
* The returned value is always using the T_STRING type.
*
- * @api
- *
* @param array|Number $value
- * @param string|null $varName
- *
- * @return array
*
* @throws SassScriptException
*/
- public function assertString($value, $varName = null)
- {
- // case of url(...) parsed a a function
- if ($value[0] === Type::T_FUNCTION) {
- $value = $this->coerceString($value);
- }
-
- if (! \in_array($value[0], [Type::T_STRING, Type::T_KEYWORD])) {
- $value = $this->compileValue($value);
- throw SassScriptException::forArgument("$value is not a string.", $varName);
- }
-
- return $this->coerceString($value);
- }
-
- /**
- * Coerce value to a percentage
- *
- * @param array|Number $value
- *
- * @return int|float
- *
- * @deprecated
- */
- protected function coercePercent($value)
+ public function assertString($value, ?string $varName = null): array
{
- @trigger_error(sprintf('"%s" is deprecated since 1.7.0.', __METHOD__), E_USER_DEPRECATED);
-
- if ($value instanceof Number) {
- if ($value->hasUnit('%')) {
- return $value->getDimension() / 100;
- }
+ if ($value[0] === Type::T_STRING) {
+ assert(\is_array($value));
- return $value->getDimension();
+ return $value;
}
- return 0;
+ $value = $this->compileValue($value);
+ throw SassScriptException::forArgument("$value is not a string.", $varName);
}
/**
* Assert value is a map
*
- * @api
- *
* @param array|Number $value
- * @param string|null $varName
- *
- * @return array
*
* @throws SassScriptException
*/
- public function assertMap($value, $varName = null)
+ public function assertMap($value, ?string $varName = null): array
{
$map = $this->tryMap($value);
@@ -7429,24 +733,28 @@ EOL;
}
/**
- * Assert value is a list
- *
- * @api
- *
- * @param array|Number $value
- *
- * @return array
+ * Tries to convert an item to a Sass map
*
- * @throws \Exception
+ * @param Number|array $item
*/
- public function assertList($value)
+ private function tryMap($item): ?array
{
- if ($value[0] !== Type::T_LIST) {
- throw $this->error('expecting list, %s received', $value[0]);
+ if ($item instanceof Number) {
+ return null;
}
- assert(\is_array($value));
- return $value;
+ if ($item[0] === Type::T_MAP) {
+ return $item;
+ }
+
+ if (
+ $item[0] === Type::T_LIST &&
+ $item[2] === []
+ ) {
+ return self::$emptyMap;
+ }
+
+ return null;
}
/**
@@ -7461,7 +769,7 @@ EOL;
*
* @return array<string, array|Number>
*/
- public function getArgumentListKeywords($value)
+ public function getArgumentListKeywords($value): array
{
if ($value[0] !== Type::T_LIST || !isset($value[3]) || !\is_array($value[3])) {
throw new \InvalidArgumentException('The argument is not a sass argument list.');
@@ -7473,19 +781,16 @@ EOL;
/**
* Assert value is a color
*
- * @api
- *
* @param array|Number $value
- * @param string|null $varName
- *
- * @return array
*
* @throws SassScriptException
*/
- public function assertColor($value, $varName = null)
+ public function assertColor($value, ?string $varName = null): array
{
- if ($color = $this->coerceColor($value)) {
- return $color;
+ if ($value[0] === Type::T_COLOR) {
+ assert(\is_array($value));
+
+ return $value;
}
$value = $this->compileValue($value);
@@ -7496,16 +801,11 @@ EOL;
/**
* Assert value is a number
*
- * @api
- *
* @param array|Number $value
- * @param string|null $varName
- *
- * @return Number
*
* @throws SassScriptException
*/
- public function assertNumber($value, $varName = null)
+ public function assertNumber($value, ?string $varName = null): Number
{
if (!$value instanceof Number) {
$value = $this->compileValue($value);
@@ -7518,16 +818,11 @@ EOL;
/**
* Assert value is a integer
*
- * @api
- *
* @param array|Number $value
- * @param string|null $varName
- *
- * @return int
*
* @throws SassScriptException
*/
- public function assertInteger($value, $varName = null)
+ public function assertInteger($value, ?string $varName = null): int
{
$value = $this->assertNumber($value, $varName)->getDimension();
if (round($value - \intval($value), Number::PRECISION) > 0) {
@@ -7538,2935 +833,17 @@ EOL;
}
/**
- * Extract the ... / alpha on the last argument of channel arg
- * in color functions
- *
- * @param array $args
- * @return array
- */
- private function extractSlashAlphaInColorFunction($args)
- {
- $last = end($args);
- if (\count($args) === 3 && $last[0] === Type::T_EXPRESSION && $last[1] === '/') {
- array_pop($args);
- $args[] = $last[2];
- $args[] = $last[3];
- }
- return $args;
- }
-
-
- /**
- * Make sure a color's components don't go out of bounds
- *
- * @param array $c
- *
- * @return array
- */
- protected function fixColor($c)
- {
- foreach ([1, 2, 3] as $i) {
- if ($c[$i] < 0) {
- $c[$i] = 0;
- }
-
- if ($c[$i] > 255) {
- $c[$i] = 255;
- }
-
- if (!\is_int($c[$i])) {
- $c[$i] = round($c[$i]);
- }
- }
-
- return $c;
- }
-
- /**
- * Convert RGB to HSL
- *
- * @internal
- *
- * @param int $red
- * @param int $green
- * @param int $blue
- *
- * @return array
- */
- public function toHSL($red, $green, $blue)
- {
- $min = min($red, $green, $blue);
- $max = max($red, $green, $blue);
-
- $l = $min + $max;
- $d = $max - $min;
-
- if ((int) $d === 0) {
- $h = $s = 0;
- } else {
- if ($l < 255) {
- $s = $d / $l;
- } else {
- $s = $d / (510 - $l);
- }
-
- if ($red == $max) {
- $h = 60 * ($green - $blue) / $d;
- } elseif ($green == $max) {
- $h = 60 * ($blue - $red) / $d + 120;
- } else {
- $h = 60 * ($red - $green) / $d + 240;
- }
- }
-
- return [Type::T_HSL, fmod($h + 360, 360), $s * 100, $l / 5.1];
- }
-
- /**
- * Hue to RGB helper
- *
- * @param float $m1
- * @param float $m2
- * @param float $h
- *
- * @return float
- */
- protected function hueToRGB($m1, $m2, $h)
- {
- if ($h < 0) {
- $h += 1;
- } elseif ($h > 1) {
- $h -= 1;
- }
-
- if ($h * 6 < 1) {
- return $m1 + ($m2 - $m1) * $h * 6;
- }
-
- if ($h * 2 < 1) {
- return $m2;
- }
-
- if ($h * 3 < 2) {
- return $m1 + ($m2 - $m1) * (2 / 3 - $h) * 6;
- }
-
- return $m1;
- }
-
- /**
- * 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
- */
- public function toRGB($hue, $saturation, $lightness)
- {
- if ($hue < 0) {
- $hue += 360;
- }
-
- $h = $hue / 360;
- $s = min(100, max(0, $saturation)) / 100;
- $l = min(100, max(0, $lightness)) / 100;
-
- $m2 = $l <= 0.5 ? $l * ($s + 1) : $l + $s - $l * $s;
- $m1 = $l * 2 - $m2;
-
- $r = $this->hueToRGB($m1, $m2, $h + 1 / 3) * 255;
- $g = $this->hueToRGB($m1, $m2, $h) * 255;
- $b = $this->hueToRGB($m1, $m2, $h - 1 / 3) * 255;
-
- $out = [Type::T_COLOR, $r, $g, $b];
-
- return $out;
- }
-
- /**
- * Convert HWB to RGB
- * https://www.w3.org/TR/css-color-4/#hwb-to-rgb
- *
- * @api
- *
- * @param int|float $hue H from 0 to 360
- * @param int|float $whiteness W from 0 to 100
- * @param int|float $blackness B from 0 to 100
- *
- * @return array
- */
- private function HWBtoRGB($hue, $whiteness, $blackness)
- {
- $w = min(100, max(0, $whiteness)) / 100;
- $b = min(100, max(0, $blackness)) / 100;
-
- $sum = $w + $b;
- if ($sum > 1.0) {
- $w = $w / $sum;
- $b = $b / $sum;
- }
- $b = min(1.0 - $w, $b);
-
- $rgb = $this->toRGB($hue, 100, 50);
- for ($i = 1; $i < 4; $i++) {
- $rgb[$i] *= (1.0 - $w - $b);
- $rgb[$i] = round($rgb[$i] + 255 * $w + 0.0001);
- }
-
- return $rgb;
- }
-
- /**
- * Convert RGB to HWB
- *
- * @api
- *
- * @param int $red
- * @param int $green
- * @param int $blue
- *
- * @return array
- */
- private function RGBtoHWB($red, $green, $blue)
- {
- $min = min($red, $green, $blue);
- $max = max($red, $green, $blue);
-
- $d = $max - $min;
-
- if ((int) $d === 0) {
- $h = 0;
- } else {
- if ($red == $max) {
- $h = 60 * ($green - $blue) / $d;
- } elseif ($green == $max) {
- $h = 60 * ($blue - $red) / $d + 120;
- } else {
- $h = 60 * ($red - $green) / $d + 240;
- }
- }
-
- return [Type::T_HWB, fmod($h, 360), $min / 255 * 100, 100 - $max / 255 * 100];
- }
-
-
- // Built in functions
-
- protected static $libCall = ['function', 'args...'];
- protected function libCall($args)
- {
- $functionReference = $args[0];
-
- if (in_array($functionReference[0], [Type::T_STRING, Type::T_KEYWORD])) {
- $name = $this->compileStringContent($this->coerceString($functionReference));
- $warning = "Passing a string to call() is deprecated and will be illegal\n"
- . "in Sass 4.0. Use call(function-reference($name)) instead.";
- Warn::deprecation($warning);
- $functionReference = $this->libGetFunction([$this->assertString($functionReference, 'function')]);
- }
-
- if ($functionReference === static::$null) {
- return static::$null;
- }
-
- if (! in_array($functionReference[0], [Type::T_FUNCTION_REFERENCE, Type::T_FUNCTION])) {
- throw $this->error('Function reference expected, got ' . $functionReference[0]);
- }
-
- $callArgs = [
- [null, $args[1], true]
- ];
-
- return $this->reduce([Type::T_FUNCTION_CALL, $functionReference, $callArgs]);
- }
-
-
- protected static $libGetFunction = [
- ['name'],
- ['name', 'css']
- ];
- protected function libGetFunction($args)
- {
- $name = $this->compileStringContent($this->assertString(array_shift($args), 'name'));
- $isCss = false;
-
- if (count($args)) {
- $isCss = array_shift($args);
- $isCss = (($isCss === static::$true) ? true : false);
- }
-
- if ($isCss) {
- return [Type::T_FUNCTION, $name, [Type::T_LIST, ',', []]];
- }
-
- return $this->getFunctionReference($name, true);
- }
-
- protected static $libIf = ['condition', 'if-true', 'if-false:'];
- protected function libIf($args)
- {
- list($cond, $t, $f) = $args;
-
- if (! $this->isTruthy($this->reduce($cond, true))) {
- return $this->reduce($f, true);
- }
-
- return $this->reduce($t, true);
- }
-
- protected static $libIndex = ['list', 'value'];
- protected function libIndex($args)
- {
- list($list, $value) = $args;
-
- if (
- $list[0] === Type::T_MAP ||
- $list[0] === Type::T_STRING ||
- $list[0] === Type::T_KEYWORD ||
- $list[0] === Type::T_INTERPOLATE
- ) {
- $list = $this->coerceList($list, ' ');
- }
-
- if ($list[0] !== Type::T_LIST) {
- return static::$null;
- }
-
- // Numbers are represented with value objects, for which the PHP equality operator does not
- // match the Sass rules (and we cannot overload it). As they are the only type of values
- // represented with a value object for now, they require a special case.
- if ($value instanceof Number) {
- $key = 0;
- foreach ($list[2] as $item) {
- $key++;
- $itemValue = $this->normalizeValue($item);
-
- if ($itemValue instanceof Number && $value->equals($itemValue)) {
- return new Number($key, '');
- }
- }
- return static::$null;
- }
-
- $values = [];
-
- foreach ($list[2] as $item) {
- $values[] = $this->normalizeValue($item);
- }
-
- $key = array_search($this->normalizeValue($value), $values);
-
- return false === $key ? static::$null : new Number($key + 1, '');
- }
-
- protected static $libRgb = [
- ['color'],
- ['color', 'alpha'],
- ['channels'],
- ['red', 'green', 'blue'],
- ['red', 'green', 'blue', 'alpha'] ];
-
- /**
- * @param array $args
- * @param array $kwargs
- * @param string $funcName
- *
- * @return array
- */
- protected function libRgb($args, $kwargs, $funcName = 'rgb')
- {
- switch (\count($args)) {
- case 1:
- if (! $color = $this->coerceColor($args[0], true)) {
- $color = [Type::T_STRING, '', [$funcName . '(', $args[0], ')']];
- }
- break;
-
- case 3:
- $color = [Type::T_COLOR, $args[0], $args[1], $args[2]];
-
- if (! $color = $this->coerceColor($color)) {
- $color = [Type::T_STRING, '', [$funcName . '(', $args[0], ', ', $args[1], ', ', $args[2], ')']];
- }
-
- return $color;
-
- case 2:
- if ($color = $this->coerceColor($args[0], true)) {
- $alpha = $this->compileRGBAValue($args[1], true);
-
- if (is_numeric($alpha)) {
- $color[4] = $alpha;
- } else {
- $color = [Type::T_STRING, '',
- [$funcName . '(', $color[1], ', ', $color[2], ', ', $color[3], ', ', $alpha, ')']];
- }
- } else {
- $color = [Type::T_STRING, '', [$funcName . '(', $args[0], ', ', $args[1], ')']];
- }
- break;
-
- case 4:
- default:
- $color = [Type::T_COLOR, $args[0], $args[1], $args[2], $args[3]];
-
- if (! $color = $this->coerceColor($color)) {
- $color = [Type::T_STRING, '',
- [$funcName . '(', $args[0], ', ', $args[1], ', ', $args[2], ', ', $args[3], ')']];
- }
- break;
- }
-
- return $color;
- }
-
- protected static $libRgba = [
- ['color'],
- ['color', 'alpha'],
- ['channels'],
- ['red', 'green', 'blue'],
- ['red', 'green', 'blue', 'alpha'] ];
- protected function libRgba($args, $kwargs)
- {
- return $this->libRgb($args, $kwargs, 'rgba');
- }
-
- /**
- * Helper function for adjust_color, change_color, and scale_color
- *
- * @param array<array|Number> $args
- * @param string $operation
- * @param callable $fn
- *
- * @return array
+ * Compiles a primitive value into a string for debugging purposes.
*
- * @phpstan-param callable(float|int, float|int|null, float|int): (float|int) $fn
- */
- protected function alterColor(array $args, $operation, $fn)
- {
- $color = $this->assertColor($args[0], 'color');
-
- if ($args[1][2]) {
- throw new SassScriptException('Only one positional argument is allowed. All other arguments must be passed by name.');
- }
-
- $kwargs = $this->getArgumentListKeywords($args[1]);
-
- $scale = $operation === 'scale';
- $change = $operation === 'change';
-
- /**
- * @param string $name
- * @param float|int $max
- * @param bool $checkPercent
- * @param bool $assertPercent
- * @return float|int|null
- */
- $getParam = function ($name, $max, $checkPercent = false, $assertPercent = false) use (&$kwargs, $scale, $change) {
- if (!isset($kwargs[$name])) {
- return null;
- }
-
- $number = $this->assertNumber($kwargs[$name], $name);
- unset($kwargs[$name]);
-
- if (!$scale && $checkPercent) {
- if (!$number->hasUnit('%')) {
- $warning = $this->error("{$name} Passing a number `$number` without unit % is deprecated.");
- $this->logger->warn($warning->getMessage(), true);
- }
- }
-
- if ($scale || $assertPercent) {
- $number->assertUnit('%', $name);
- }
-
- if ($scale) {
- $max = 100;
- }
-
- if ($scale || $assertPercent) {
- return $number->valueInRange($change ? 0 : -$max, $max, $name);
- }
-
- return $number->valueInRangeWithUnit($change ? 0 : -$max, $max, $name, $checkPercent ? '%' : '');
- };
-
- $alpha = $getParam('alpha', 1);
- $red = $getParam('red', 255);
- $green = $getParam('green', 255);
- $blue = $getParam('blue', 255);
-
- if ($scale || !isset($kwargs['hue'])) {
- $hue = null;
- } else {
- $hueNumber = $this->assertNumber($kwargs['hue'], 'hue');
- unset($kwargs['hue']);
- $hue = $hueNumber->getDimension();
- }
- $saturation = $getParam('saturation', 100, true);
- $lightness = $getParam('lightness', 100, true);
- $whiteness = $getParam('whiteness', 100, false, true);
- $blackness = $getParam('blackness', 100, false, true);
-
- if (!empty($kwargs)) {
- $unknownNames = array_keys($kwargs);
- $lastName = array_pop($unknownNames);
- $message = sprintf(
- 'No argument%s named $%s%s.',
- $unknownNames ? 's' : '',
- $unknownNames ? implode(', $', $unknownNames) . ' or $' : '',
- $lastName
- );
- throw new SassScriptException($message);
- }
-
- $hasRgb = $red !== null || $green !== null || $blue !== null;
- $hasSL = $saturation !== null || $lightness !== null;
- $hasWB = $whiteness !== null || $blackness !== null;
-
- if ($hasRgb && ($hasSL || $hasWB || $hue !== null)) {
- throw new SassScriptException(sprintf('RGB parameters may not be passed along with %s parameters.', $hasWB ? 'HWB' : 'HSL'));
- }
-
- if ($hasWB && $hasSL) {
- throw new SassScriptException('HSL parameters may not be passed along with HWB parameters.');
- }
-
- if ($hasRgb) {
- $color[1] = round($fn($color[1], $red, 255));
- $color[2] = round($fn($color[2], $green, 255));
- $color[3] = round($fn($color[3], $blue, 255));
- } elseif ($hasWB) {
- $hwb = $this->RGBtoHWB($color[1], $color[2], $color[3]);
- if ($hue !== null) {
- $hwb[1] = $change ? $hue : $hwb[1] + $hue;
- }
- $hwb[2] = $fn($hwb[2], $whiteness, 100);
- $hwb[3] = $fn($hwb[3], $blackness, 100);
-
- $rgb = $this->HWBtoRGB($hwb[1], $hwb[2], $hwb[3]);
-
- if (isset($color[4])) {
- $rgb[4] = $color[4];
- }
-
- $color = $rgb;
- } elseif ($hue !== null || $hasSL) {
- $hsl = $this->toHSL($color[1], $color[2], $color[3]);
-
- if ($hue !== null) {
- $hsl[1] = $change ? $hue : $hsl[1] + $hue;
- }
- $hsl[2] = $fn($hsl[2], $saturation, 100);
- $hsl[3] = $fn($hsl[3], $lightness, 100);
-
- $rgb = $this->toRGB($hsl[1], $hsl[2], $hsl[3]);
-
- if (isset($color[4])) {
- $rgb[4] = $color[4];
- }
-
- $color = $rgb;
- }
-
- if ($alpha !== null) {
- $existingAlpha = isset($color[4]) ? $color[4] : 1;
- $color[4] = $fn($existingAlpha, $alpha, 1);
- }
-
- return $color;
- }
-
- protected static $libAdjustColor = ['color', 'kwargs...'];
- protected function libAdjustColor($args)
- {
- return $this->alterColor($args, 'adjust', function ($base, $alter, $max) {
- if ($alter === null) {
- return $base;
- }
-
- $new = $base + $alter;
-
- if ($new < 0) {
- return 0;
- }
-
- if ($new > $max) {
- return $max;
- }
-
- return $new;
- });
- }
-
- protected static $libChangeColor = ['color', 'kwargs...'];
- protected function libChangeColor($args)
- {
- return $this->alterColor($args, 'change', function ($base, $alter, $max) {
- if ($alter === null) {
- return $base;
- }
-
- return $alter;
- });
- }
-
- protected static $libScaleColor = ['color', 'kwargs...'];
- protected function libScaleColor($args)
- {
- return $this->alterColor($args, 'scale', function ($base, $scale, $max) {
- if ($scale === null) {
- return $base;
- }
-
- $scale = $scale / 100;
-
- if ($scale < 0) {
- return $base * $scale + $base;
- }
-
- return ($max - $base) * $scale + $base;
- });
- }
-
- protected static $libIeHexStr = ['color'];
- protected function libIeHexStr($args)
- {
- $color = $this->coerceColor($args[0]);
-
- if (\is_null($color)) {
- throw $this->error('Error: argument `$color` of `ie-hex-str($color)` must be a color');
- }
-
- $color[4] = isset($color[4]) ? round(255 * $color[4]) : 255;
-
- return [Type::T_STRING, '', [sprintf('#%02X%02X%02X%02X', $color[4], $color[1], $color[2], $color[3])]];
- }
-
- protected static $libRed = ['color'];
- protected function libRed($args)
- {
- $color = $this->coerceColor($args[0]);
-
- if (\is_null($color)) {
- throw $this->error('Error: argument `$color` of `red($color)` must be a color');
- }
-
- return new Number((int) $color[1], '');
- }
-
- protected static $libGreen = ['color'];
- protected function libGreen($args)
- {
- $color = $this->coerceColor($args[0]);
-
- if (\is_null($color)) {
- throw $this->error('Error: argument `$color` of `green($color)` must be a color');
- }
-
- return new Number((int) $color[2], '');
- }
-
- protected static $libBlue = ['color'];
- protected function libBlue($args)
- {
- $color = $this->coerceColor($args[0]);
-
- if (\is_null($color)) {
- throw $this->error('Error: argument `$color` of `blue($color)` must be a color');
- }
-
- return new Number((int) $color[3], '');
- }
-
- protected static $libAlpha = ['color'];
- protected function libAlpha($args)
- {
- if ($color = $this->coerceColor($args[0])) {
- return new Number(isset($color[4]) ? $color[4] : 1, '');
- }
-
- // this might be the IE function, so return value unchanged
- return null;
- }
-
- protected static $libOpacity = ['color'];
- protected function libOpacity($args)
- {
- $value = $args[0];
-
- if ($value instanceof Number) {
- return null;
- }
-
- return $this->libAlpha($args);
- }
-
- // mix two colors
- protected static $libMix = [
- ['color1', 'color2', 'weight:50%'],
- ['color-1', 'color-2', 'weight:50%']
- ];
- protected function libMix($args)
- {
- list($first, $second, $weight) = $args;
-
- $first = $this->assertColor($first, 'color1');
- $second = $this->assertColor($second, 'color2');
- $weightScale = $this->assertNumber($weight, 'weight')->valueInRange(0, 100, 'weight') / 100;
-
- $firstAlpha = isset($first[4]) ? $first[4] : 1;
- $secondAlpha = isset($second[4]) ? $second[4] : 1;
-
- $normalizedWeight = $weightScale * 2 - 1;
- $alphaDistance = $firstAlpha - $secondAlpha;
-
- $combinedWeight = $normalizedWeight * $alphaDistance == -1 ? $normalizedWeight : ($normalizedWeight + $alphaDistance) / (1 + $normalizedWeight * $alphaDistance);
- $weight1 = ($combinedWeight + 1) / 2.0;
- $weight2 = 1.0 - $weight1;
-
- $new = [Type::T_COLOR,
- $weight1 * $first[1] + $weight2 * $second[1],
- $weight1 * $first[2] + $weight2 * $second[2],
- $weight1 * $first[3] + $weight2 * $second[3],
- ];
-
- if ($firstAlpha != 1.0 || $secondAlpha != 1.0) {
- $new[] = $firstAlpha * $weightScale + $secondAlpha * (1 - $weightScale);
- }
-
- return $this->fixColor($new);
- }
-
- protected static $libHsl = [
- ['channels'],
- ['hue', 'saturation'],
- ['hue', 'saturation', 'lightness'],
- ['hue', 'saturation', 'lightness', 'alpha'] ];
-
- /**
- * @param array $args
- * @param array $kwargs
- * @param string $funcName
- *
- * @return array|null
- */
- protected function libHsl($args, $kwargs, $funcName = 'hsl')
- {
- $args_to_check = $args;
-
- if (\count($args) == 1) {
- if ($args[0][0] !== Type::T_LIST || \count($args[0][2]) < 3 || \count($args[0][2]) > 4) {
- return [Type::T_STRING, '', [$funcName . '(', $args[0], ')']];
- }
-
- $args = $args[0][2];
- $args_to_check = $kwargs['channels'][2];
- }
-
- if (\count($args) === 2) {
- // if var() is used as an argument, return as a css function
- foreach ($args as $arg) {
- if ($arg[0] === Type::T_FUNCTION && in_array($arg[1], ['var'])) {
- return null;
- }
- }
-
- throw new SassScriptException('Missing argument $lightness.');
- }
-
- foreach ($kwargs as $arg) {
- if (in_array($arg[0], [Type::T_FUNCTION_CALL, Type::T_FUNCTION]) && in_array($arg[1], ['min', 'max'])) {
- return null;
- }
- }
-
- foreach ($args_to_check as $k => $arg) {
- if (in_array($arg[0], [Type::T_FUNCTION_CALL, Type::T_FUNCTION]) && in_array($arg[1], ['min', 'max'])) {
- if (count($kwargs) > 1 || ($k >= 2 && count($args) === 4)) {
- return null;
- }
-
- $args[$k] = $this->stringifyFncallArgs($arg);
- }
-
- if (
- $k >= 2 && count($args) === 4 &&
- in_array($arg[0], [Type::T_FUNCTION_CALL, Type::T_FUNCTION]) &&
- in_array($arg[1], ['calc','env'])
- ) {
- return null;
- }
- }
-
- $hue = $this->reduce($args[0]);
- $saturation = $this->reduce($args[1]);
- $lightness = $this->reduce($args[2]);
- $alpha = null;
-
- if (\count($args) === 4) {
- $alpha = $this->compileColorPartValue($args[3], 0, 100, false);
-
- if (!$hue instanceof Number || !$saturation instanceof Number || ! $lightness instanceof Number || ! is_numeric($alpha)) {
- return [Type::T_STRING, '',
- [$funcName . '(', $args[0], ', ', $args[1], ', ', $args[2], ', ', $args[3], ')']];
- }
- } else {
- if (!$hue instanceof Number || !$saturation instanceof Number || ! $lightness instanceof Number) {
- return [Type::T_STRING, '', [$funcName . '(', $args[0], ', ', $args[1], ', ', $args[2], ')']];
- }
- }
-
- $hueValue = fmod($hue->getDimension(), 360);
-
- while ($hueValue < 0) {
- $hueValue += 360;
- }
-
- $color = $this->toRGB($hueValue, max(0, min($saturation->getDimension(), 100)), max(0, min($lightness->getDimension(), 100)));
-
- if (! \is_null($alpha)) {
- $color[4] = $alpha;
- }
-
- return $color;
- }
-
- protected static $libHsla = [
- ['channels'],
- ['hue', 'saturation'],
- ['hue', 'saturation', 'lightness'],
- ['hue', 'saturation', 'lightness', 'alpha']];
- protected function libHsla($args, $kwargs)
- {
- return $this->libHsl($args, $kwargs, 'hsla');
- }
-
- protected static $libHue = ['color'];
- protected function libHue($args)
- {
- $color = $this->assertColor($args[0], 'color');
- $hsl = $this->toHSL($color[1], $color[2], $color[3]);
-
- return new Number($hsl[1], 'deg');
- }
-
- protected static $libSaturation = ['color'];
- protected function libSaturation($args)
- {
- $color = $this->assertColor($args[0], 'color');
- $hsl = $this->toHSL($color[1], $color[2], $color[3]);
-
- return new Number($hsl[2], '%');
- }
-
- protected static $libLightness = ['color'];
- protected function libLightness($args)
- {
- $color = $this->assertColor($args[0], 'color');
- $hsl = $this->toHSL($color[1], $color[2], $color[3]);
-
- return new Number($hsl[3], '%');
- }
-
- /*
- * Todo : a integrer dans le futur module color
- protected static $libHwb = [
- ['channels'],
- ['hue', 'whiteness', 'blackness'],
- ['hue', 'whiteness', 'blackness', 'alpha'] ];
- protected function libHwb($args, $kwargs, $funcName = 'hwb')
- {
- $args_to_check = $args;
-
- if (\count($args) == 1) {
- if ($args[0][0] !== Type::T_LIST) {
- throw $this->error("Missing elements \$whiteness and \$blackness");
- }
-
- if (\trim($args[0][1])) {
- throw $this->error("\$channels must be a space-separated list.");
- }
-
- if (! empty($args[0]['enclosing'])) {
- throw $this->error("\$channels must be an unbracketed list.");
- }
-
- $args = $args[0][2];
- if (\count($args) > 3) {
- throw $this->error("hwb() : Only 3 elements are allowed but ". \count($args) . "were passed");
- }
-
- $args_to_check = $this->extractSlashAlphaInColorFunction($kwargs['channels'][2]);
- if (\count($args_to_check) !== \count($kwargs['channels'][2])) {
- $args = $args_to_check;
- }
- }
-
- if (\count($args_to_check) < 2) {
- throw $this->error("Missing elements \$whiteness and \$blackness");
- }
- if (\count($args_to_check) < 3) {
- throw $this->error("Missing element \$blackness");
- }
- if (\count($args_to_check) > 4) {
- throw $this->error("hwb() : Only 4 elements are allowed but ". \count($args) . "were passed");
- }
-
- foreach ($kwargs as $k => $arg) {
- if (in_array($arg[0], [Type::T_FUNCTION_CALL]) && in_array($arg[1], ['min', 'max'])) {
- return null;
- }
- }
-
- foreach ($args_to_check as $k => $arg) {
- if (in_array($arg[0], [Type::T_FUNCTION_CALL]) && in_array($arg[1], ['min', 'max'])) {
- if (count($kwargs) > 1 || ($k >= 2 && count($args) === 4)) {
- return null;
- }
-
- $args[$k] = $this->stringifyFncallArgs($arg);
- }
-
- if (
- $k >= 2 && count($args) === 4 &&
- in_array($arg[0], [Type::T_FUNCTION_CALL, Type::T_FUNCTION]) &&
- in_array($arg[1], ['calc','env'])
- ) {
- return null;
- }
- }
-
- $hue = $this->reduce($args[0]);
- $whiteness = $this->reduce($args[1]);
- $blackness = $this->reduce($args[2]);
- $alpha = null;
-
- if (\count($args) === 4) {
- $alpha = $this->compileColorPartValue($args[3], 0, 1, false);
-
- if (! \is_numeric($alpha)) {
- $val = $this->compileValue($args[3]);
- throw $this->error("\$alpha: $val is not a number");
- }
- }
-
- $this->assertNumber($hue, 'hue');
- $this->assertUnit($whiteness, ['%'], 'whiteness');
- $this->assertUnit($blackness, ['%'], 'blackness');
-
- $this->assertRange($whiteness, 0, 100, "0% and 100%", "whiteness");
- $this->assertRange($blackness, 0, 100, "0% and 100%", "blackness");
-
- $w = $whiteness->getDimension();
- $b = $blackness->getDimension();
-
- $hueValue = $hue->getDimension() % 360;
-
- while ($hueValue < 0) {
- $hueValue += 360;
- }
-
- $color = $this->HWBtoRGB($hueValue, $w, $b);
-
- if (! \is_null($alpha)) {
- $color[4] = $alpha;
- }
-
- return $color;
- }
-
- protected static $libWhiteness = ['color'];
- protected function libWhiteness($args, $kwargs, $funcName = 'whiteness') {
-
- $color = $this->assertColor($args[0]);
- $hwb = $this->RGBtoHWB($color[1], $color[2], $color[3]);
-
- return new Number($hwb[2], '%');
- }
-
- protected static $libBlackness = ['color'];
- protected function libBlackness($args, $kwargs, $funcName = 'blackness') {
-
- $color = $this->assertColor($args[0]);
- $hwb = $this->RGBtoHWB($color[1], $color[2], $color[3]);
-
- return new Number($hwb[3], '%');
- }
- */
-
- /**
- * @param array $color
- * @param int $idx
- * @param int|float $amount
- *
- * @return array
- */
- protected function adjustHsl($color, $idx, $amount)
- {
- $hsl = $this->toHSL($color[1], $color[2], $color[3]);
- $hsl[$idx] += $amount;
-
- if ($idx !== 1) {
- // Clamp the saturation and lightness
- $hsl[$idx] = min(max(0, $hsl[$idx]), 100);
- }
-
- $out = $this->toRGB($hsl[1], $hsl[2], $hsl[3]);
-
- if (isset($color[4])) {
- $out[4] = $color[4];
- }
-
- return $out;
- }
-
- protected static $libAdjustHue = ['color', 'degrees'];
- protected function libAdjustHue($args)
- {
- $color = $this->assertColor($args[0], 'color');
- $degrees = $this->assertNumber($args[1], 'degrees')->getDimension();
-
- return $this->adjustHsl($color, 1, $degrees);
- }
-
- 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], '%');
-
- return $this->adjustHsl($color, 3, $amount);
- }
-
- 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], '%');
-
- return $this->adjustHsl($color, 3, -$amount);
- }
-
- protected static $libSaturate = [['color', 'amount'], ['amount']];
- protected function libSaturate($args)
- {
- $value = $args[0];
-
- if (count($args) === 1) {
- $this->assertNumber($args[0], 'amount');
-
- return null;
- }
-
- $color = $this->assertColor($args[0], 'color');
- $amount = $this->assertNumber($args[1], 'amount');
-
- return $this->adjustHsl($color, 2, $amount->valueInRange(0, 100, 'amount'));
- }
-
- protected static $libDesaturate = ['color', 'amount'];
- protected function libDesaturate($args)
- {
- $color = $this->assertColor($args[0], 'color');
- $amount = $this->assertNumber($args[1], 'amount');
-
- return $this->adjustHsl($color, 2, -$amount->valueInRange(0, 100, 'amount'));
- }
-
- protected static $libGrayscale = ['color'];
- protected function libGrayscale($args)
- {
- $value = $args[0];
-
- if ($value instanceof Number) {
- return null;
- }
-
- return $this->adjustHsl($this->assertColor($value, 'color'), 2, -100);
- }
-
- protected static $libComplement = ['color'];
- protected function libComplement($args)
- {
- return $this->adjustHsl($this->assertColor($args[0], 'color'), 1, 180);
- }
-
- protected static $libInvert = ['color', 'weight:100%'];
- protected function libInvert($args)
- {
- $value = $args[0];
-
- $weight = $this->assertNumber($args[1], 'weight');
-
- if ($value instanceof Number) {
- if ($weight->getDimension() != 100 || !$weight->hasUnit('%')) {
- throw new SassScriptException('Only one argument may be passed to the plain-CSS invert() function.');
- }
-
- return null;
- }
-
- $color = $this->assertColor($value, 'color');
- $inverted = $color;
- $inverted[1] = 255 - $inverted[1];
- $inverted[2] = 255 - $inverted[2];
- $inverted[3] = 255 - $inverted[3];
-
- return $this->libMix([$inverted, $color, $weight]);
- }
-
- // increases opacity by amount
- protected static $libOpacify = ['color', 'amount'];
- protected function libOpacify($args)
- {
- $color = $this->assertColor($args[0], 'color');
- $amount = $this->assertNumber($args[1], 'amount');
-
- $color[4] = (isset($color[4]) ? $color[4] : 1) + $amount->valueInRangeWithUnit(0, 1, 'amount', '');
- $color[4] = min(1, max(0, $color[4]));
-
- return $color;
- }
-
- protected static $libFadeIn = ['color', 'amount'];
- protected function libFadeIn($args)
- {
- return $this->libOpacify($args);
- }
-
- // decreases opacity by amount
- protected static $libTransparentize = ['color', 'amount'];
- protected function libTransparentize($args)
- {
- $color = $this->assertColor($args[0], 'color');
- $amount = $this->assertNumber($args[1], 'amount');
-
- $color[4] = (isset($color[4]) ? $color[4] : 1) - $amount->valueInRangeWithUnit(0, 1, 'amount', '');
- $color[4] = min(1, max(0, $color[4]));
-
- return $color;
- }
-
- protected static $libFadeOut = ['color', 'amount'];
- protected function libFadeOut($args)
- {
- return $this->libTransparentize($args);
- }
-
- protected static $libUnquote = ['string'];
- protected function libUnquote($args)
- {
- try {
- $str = $this->assertString($args[0], 'string');
- } catch (SassScriptException $e) {
- $value = $this->compileValue($args[0]);
- $fname = $this->getPrettyPath($this->sourceNames[$this->sourceIndex]);
- $line = $this->sourceLine;
-
- $message = "Passing $value, a non-string value, to unquote()
-will be an error in future versions of Sass.\n on line $line of $fname";
-
- $this->logger->warn($message, true);
-
- return $args[0];
- }
-
- $str[1] = '';
-
- return $str;
- }
-
- protected static $libQuote = ['string'];
- protected function libQuote($args)
- {
- $value = $this->assertString($args[0], 'string');
-
- $value[1] = '"';
-
- return $value;
- }
-
- protected static $libPercentage = ['number'];
- protected function libPercentage($args)
- {
- $num = $this->assertNumber($args[0], 'number');
- $num->assertNoUnits('number');
-
- return new Number($num->getDimension() * 100, '%');
- }
-
- protected static $libRound = ['number'];
- protected function libRound($args)
- {
- $num = $this->assertNumber($args[0], 'number');
-
- return new Number(round($num->getDimension()), $num->getNumeratorUnits(), $num->getDenominatorUnits());
- }
-
- protected static $libFloor = ['number'];
- protected function libFloor($args)
- {
- $num = $this->assertNumber($args[0], 'number');
-
- return new Number(floor($num->getDimension()), $num->getNumeratorUnits(), $num->getDenominatorUnits());
- }
-
- protected static $libCeil = ['number'];
- protected function libCeil($args)
- {
- $num = $this->assertNumber($args[0], 'number');
-
- return new Number(ceil($num->getDimension()), $num->getNumeratorUnits(), $num->getDenominatorUnits());
- }
-
- protected static $libAbs = ['number'];
- protected function libAbs($args)
- {
- $num = $this->assertNumber($args[0], 'number');
-
- return new Number(abs($num->getDimension()), $num->getNumeratorUnits(), $num->getDenominatorUnits());
- }
-
- protected static $libMin = ['numbers...'];
- protected function libMin($args)
- {
- /**
- * @var Number|null
- */
- $min = null;
-
- foreach ($args[0][2] as $arg) {
- $number = $this->assertNumber($arg);
-
- if (\is_null($min) || $min->greaterThan($number)) {
- $min = $number;
- }
- }
-
- if (!\is_null($min)) {
- return $min;
- }
-
- throw $this->error('At least one argument must be passed.');
- }
-
- protected static $libMax = ['numbers...'];
- protected function libMax($args)
- {
- /**
- * @var Number|null
- */
- $max = null;
-
- foreach ($args[0][2] as $arg) {
- $number = $this->assertNumber($arg);
-
- if (\is_null($max) || $max->lessThan($number)) {
- $max = $number;
- }
- }
-
- if (!\is_null($max)) {
- return $max;
- }
-
- throw $this->error('At least one argument must be passed.');
- }
-
- protected static $libLength = ['list'];
- protected function libLength($args)
- {
- $list = $this->coerceList($args[0], ',', true);
-
- return new Number(\count($list[2]), '');
- }
-
- 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'];
- }
-
- $list = $this->coerceList($args[0]);
-
- if ($list[1] === '' && \count($list[2]) <= 1 && empty($list['enclosing'])) {
- return [Type::T_KEYWORD, 'space'];
- }
-
- if ($list[1] === ',') {
- return [Type::T_KEYWORD, 'comma'];
- }
-
- if ($list[1] === '/') {
- return [Type::T_KEYWORD, 'slash'];
- }
-
- return [Type::T_KEYWORD, 'space'];
- }
-
- protected static $libNth = ['list', 'n'];
- protected function libNth($args)
- {
- $list = $this->coerceList($args[0], ',', false);
- $n = $this->assertInteger($args[1]);
-
- if ($n > 0) {
- $n--;
- } elseif ($n < 0) {
- $n += \count($list[2]);
- }
-
- return isset($list[2][$n]) ? $list[2][$n] : static::$defaultValue;
- }
-
- protected static $libSetNth = ['list', 'n', 'value'];
- protected function libSetNth($args)
- {
- $list = $this->coerceList($args[0]);
- $n = $this->assertInteger($args[1]);
-
- if ($n > 0) {
- $n--;
- } elseif ($n < 0) {
- $n += \count($list[2]);
- }
-
- if (! isset($list[2][$n])) {
- throw $this->error('Invalid argument for "n"');
- }
-
- $list[2][$n] = $args[2];
-
- return $list;
- }
-
- protected static $libMapGet = ['map', 'key', 'keys...'];
- protected function libMapGet($args)
- {
- $map = $this->assertMap($args[0], 'map');
- if (!isset($args[2])) {
- // BC layer for usages of the function from PHP code rather than from the Sass function
- $args[2] = self::$emptyArgumentList;
- }
- $keys = array_merge([$args[1]], $args[2][2]);
- $value = static::$null;
-
- foreach ($keys as $key) {
- if (!\is_array($map) || $map[0] !== Type::T_MAP) {
- return static::$null;
- }
-
- $map = $this->mapGet($map, $key);
-
- if ($map === null) {
- return static::$null;
- }
-
- $value = $map;
- }
-
- return $value;
- }
-
- /**
- * Gets the value corresponding to that key in the map
- *
- * @param array $map
- * @param Number|array $key
- *
- * @return Number|array|null
- */
- private function mapGet(array $map, $key)
- {
- $index = $this->mapGetEntryIndex($map, $key);
-
- if ($index !== null) {
- return $map[2][$index];
- }
-
- return null;
- }
-
- /**
- * Gets the index corresponding to that key in the map entries
- *
- * @param array $map
- * @param Number|array $key
- *
- * @return int|null
- */
- private function mapGetEntryIndex(array $map, $key)
- {
- $key = $this->compileStringContent($this->coerceString($key));
-
- for ($i = \count($map[1]) - 1; $i >= 0; $i--) {
- if ($key === $this->compileStringContent($this->coerceString($map[1][$i]))) {
- return $i;
- }
- }
-
- return null;
- }
-
- protected static $libMapKeys = ['map'];
- protected function libMapKeys($args)
- {
- $map = $this->assertMap($args[0], 'map');
- $keys = $map[1];
-
- return [Type::T_LIST, ',', $keys];
- }
-
- protected static $libMapValues = ['map'];
- protected function libMapValues($args)
- {
- $map = $this->assertMap($args[0], 'map');
- $values = $map[2];
-
- return [Type::T_LIST, ',', $values];
- }
-
- protected static $libMapRemove = [
- ['map'],
- ['map', 'key', 'keys...'],
- ];
- protected function libMapRemove($args)
- {
- $map = $this->assertMap($args[0], 'map');
-
- if (\count($args) === 1) {
- return $map;
- }
-
- $keys = [];
- $keys[] = $this->compileStringContent($this->coerceString($args[1]));
-
- foreach ($args[2][2] as $key) {
- $keys[] = $this->compileStringContent($this->coerceString($key));
- }
-
- for ($i = \count($map[1]) - 1; $i >= 0; $i--) {
- if (in_array($this->compileStringContent($this->coerceString($map[1][$i])), $keys)) {
- array_splice($map[1], $i, 1);
- array_splice($map[2], $i, 1);
- }
- }
-
- return $map;
- }
-
- protected static $libMapHasKey = ['map', 'key', 'keys...'];
- protected function libMapHasKey($args)
- {
- $map = $this->assertMap($args[0], 'map');
- if (!isset($args[2])) {
- // BC layer for usages of the function from PHP code rather than from the Sass function
- $args[2] = self::$emptyArgumentList;
- }
- $keys = array_merge([$args[1]], $args[2][2]);
- $lastKey = array_pop($keys);
-
- foreach ($keys as $key) {
- $value = $this->mapGet($map, $key);
-
- if ($value === null || $value instanceof Number || $value[0] !== Type::T_MAP) {
- return self::$false;
- }
-
- $map = $value;
- }
-
- return $this->toBool($this->mapHasKey($map, $lastKey));
- }
-
- /**
- * @param array|Number $keyValue
- *
- * @return bool
- */
- private function mapHasKey(array $map, $keyValue)
- {
- $key = $this->compileStringContent($this->coerceString($keyValue));
-
- for ($i = \count($map[1]) - 1; $i >= 0; $i--) {
- if ($key === $this->compileStringContent($this->coerceString($map[1][$i]))) {
- return true;
- }
- }
-
- return false;
- }
-
- protected static $libMapMerge = [
- ['map1', 'map2'],
- ['map-1', 'map-2'],
- ['map1', 'args...']
- ];
- protected function libMapMerge($args)
- {
- $map1 = $this->assertMap($args[0], 'map1');
- $map2 = $args[1];
- $keys = [];
- if ($map2[0] === Type::T_LIST && isset($map2[3]) && \is_array($map2[3])) {
- // This is an argument list for the variadic signature
- if (\count($map2[2]) === 0) {
- throw new SassScriptException('Expected $args to contain a key.');
- }
- if (\count($map2[2]) === 1) {
- throw new SassScriptException('Expected $args to contain a value.');
- }
- $keys = $map2[2];
- $map2 = array_pop($keys);
- }
- $map2 = $this->assertMap($map2, 'map2');
-
- return $this->modifyMap($map1, $keys, function ($oldValue) use ($map2) {
- $nestedMap = $this->tryMap($oldValue);
-
- if ($nestedMap === null) {
- return $map2;
- }
-
- return $this->mergeMaps($nestedMap, $map2);
- });
- }
-
- /**
- * @param array $map
- * @param array $keys
- * @param callable $modify
- * @param bool $addNesting
- *
- * @return Number|array
- *
- * @phpstan-param array<Number|array> $keys
- * @phpstan-param callable(Number|array): (Number|array) $modify
- */
- private function modifyMap(array $map, array $keys, callable $modify, $addNesting = true)
- {
- if ($keys === []) {
- return $modify($map);
- }
-
- return $this->modifyNestedMap($map, $keys, $modify, $addNesting);
- }
-
- /**
- * @param array $map
- * @param array $keys
- * @param callable $modify
- * @param bool $addNesting
- *
- * @return array
- *
- * @phpstan-param non-empty-array<Number|array> $keys
- * @phpstan-param callable(Number|array): (Number|array) $modify
- */
- private function modifyNestedMap(array $map, array $keys, callable $modify, $addNesting)
- {
- $key = array_shift($keys);
-
- $nestedValueIndex = $this->mapGetEntryIndex($map, $key);
-
- if ($keys === []) {
- if ($nestedValueIndex !== null) {
- $map[2][$nestedValueIndex] = $modify($map[2][$nestedValueIndex]);
- } else {
- $map[1][] = $key;
- $map[2][] = $modify(self::$null);
- }
-
- return $map;
- }
-
- $nestedMap = $nestedValueIndex !== null ? $this->tryMap($map[2][$nestedValueIndex]) : null;
-
- if ($nestedMap === null && !$addNesting) {
- return $map;
- }
-
- if ($nestedMap === null) {
- $nestedMap = self::$emptyMap;
- }
-
- $newNestedMap = $this->modifyNestedMap($nestedMap, $keys, $modify, $addNesting);
-
- if ($nestedValueIndex !== null) {
- $map[2][$nestedValueIndex] = $newNestedMap;
- } else {
- $map[1][] = $key;
- $map[2][] = $newNestedMap;
- }
-
- return $map;
- }
-
- /**
- * Merges 2 Sass maps together
- *
- * @param array $map1
- * @param array $map2
- *
- * @return array
- */
- private function mergeMaps(array $map1, array $map2)
- {
- foreach ($map2[1] as $i2 => $key2) {
- $map1EntryIndex = $this->mapGetEntryIndex($map1, $key2);
-
- if ($map1EntryIndex !== null) {
- $map1[2][$map1EntryIndex] = $map2[2][$i2];
- continue;
- }
-
- $map1[1][] = $key2;
- $map1[2][] = $map2[2][$i2];
- }
-
- return $map1;
- }
-
- protected static $libKeywords = ['args'];
- protected function libKeywords($args)
- {
- $value = $args[0];
-
- if ($value[0] !== Type::T_LIST || !isset($value[3]) || !\is_array($value[3])) {
- $compiledValue = $this->compileValue($value);
-
- throw SassScriptException::forArgument($compiledValue . ' is not an argument list.', 'args');
- }
-
- $keys = [];
- $values = [];
-
- foreach ($this->getArgumentListKeywords($value) as $name => $arg) {
- $keys[] = [Type::T_KEYWORD, $name];
- $values[] = $arg;
- }
-
- return [Type::T_MAP, $keys, $values];
- }
-
- protected static $libIsBracketed = ['list'];
- protected function libIsBracketed($args)
- {
- $list = $args[0];
- $this->coerceList($list, ' ');
-
- if (! empty($list['enclosing']) && $list['enclosing'] === 'bracket') {
- return self::$true;
- }
-
- return self::$false;
- }
-
- /**
- * @param array $list1
- * @param array|Number|null $sep
- *
- * @return string
- * @throws CompilerException
- *
- * @deprecated
- */
- protected function listSeparatorForJoin($list1, $sep)
- {
- @trigger_error(sprintf('The "%s" method is deprecated.', __METHOD__), E_USER_DEPRECATED);
-
- if (! isset($sep)) {
- return $list1[1];
- }
-
- switch ($this->compileValue($sep)) {
- case 'comma':
- return ',';
-
- case 'space':
- return ' ';
-
- default:
- return $list1[1];
- }
- }
-
- protected static $libJoin = ['list1', 'list2', 'separator:auto', 'bracketed:auto'];
- protected function libJoin($args)
- {
- list($list1, $list2, $sep, $bracketed) = $args;
-
- $list1 = $this->coerceList($list1, ' ', true);
- $list2 = $this->coerceList($list2, ' ', true);
-
- switch ($this->compileStringContent($this->assertString($sep, 'separator'))) {
- case 'comma':
- $separator = ',';
- break;
-
- case 'space':
- $separator = ' ';
- break;
-
- case 'slash':
- $separator = '/';
- break;
-
- case 'auto':
- if ($list1[1] !== '' || count($list1[2]) > 1 || !empty($list1['enclosing']) && $list1['enclosing'] !== 'parent') {
- $separator = $list1[1] ?: ' ';
- } elseif ($list2[1] !== '' || count($list2[2]) > 1 || !empty($list2['enclosing']) && $list2['enclosing'] !== 'parent') {
- $separator = $list2[1] ?: ' ';
- } else {
- $separator = ' ';
- }
- break;
-
- default:
- throw SassScriptException::forArgument('Must be "space", "comma", "slash", or "auto".', 'separator');
- }
-
- if ($bracketed === static::$true) {
- $bracketed = true;
- } elseif ($bracketed === static::$false) {
- $bracketed = false;
- } elseif ($bracketed === [Type::T_KEYWORD, 'auto']) {
- $bracketed = 'auto';
- } elseif ($bracketed === static::$null) {
- $bracketed = false;
- } else {
- $bracketed = $this->compileValue($bracketed);
- $bracketed = ! ! $bracketed;
-
- if ($bracketed === true) {
- $bracketed = true;
- }
- }
-
- if ($bracketed === 'auto') {
- $bracketed = false;
-
- if (! empty($list1['enclosing']) && $list1['enclosing'] === 'bracket') {
- $bracketed = true;
- }
- }
-
- $res = [Type::T_LIST, $separator, array_merge($list1[2], $list2[2])];
-
- if ($bracketed) {
- $res['enclosing'] = 'bracket';
- }
-
- return $res;
- }
-
- protected static $libAppend = ['list', 'val', 'separator:auto'];
- protected function libAppend($args)
- {
- list($list1, $value, $sep) = $args;
-
- $list1 = $this->coerceList($list1, ' ', true);
-
- switch ($this->compileStringContent($this->assertString($sep, 'separator'))) {
- case 'comma':
- $separator = ',';
- break;
-
- case 'space':
- $separator = ' ';
- break;
-
- case 'slash':
- $separator = '/';
- break;
-
- case 'auto':
- $separator = $list1[1] === '' && \count($list1[2]) <= 1 && (empty($list1['enclosing']) || $list1['enclosing'] === 'parent') ? ' ' : $list1[1];
- break;
-
- default:
- throw SassScriptException::forArgument('Must be "space", "comma", "slash", or "auto".', 'separator');
- }
-
- $res = [Type::T_LIST, $separator, array_merge($list1[2], [$value])];
-
- if (isset($list1['enclosing'])) {
- $res['enclosing'] = $list1['enclosing'];
- }
-
- return $res;
- }
-
- protected static $libZip = ['lists...'];
- protected function libZip($args)
- {
- $argLists = [];
- foreach ($args[0][2] as $arg) {
- $argLists[] = $this->coerceList($arg);
- }
-
- $lists = [];
- $firstList = array_shift($argLists);
-
- $result = [Type::T_LIST, ',', $lists];
- if (! \is_null($firstList)) {
- foreach ($firstList[2] as $key => $item) {
- $list = [Type::T_LIST, ' ', [$item]];
-
- foreach ($argLists as $arg) {
- if (isset($arg[2][$key])) {
- $list[2][] = $arg[2][$key];
- } else {
- break 2;
- }
- }
-
- $lists[] = $list;
- }
-
- $result[2] = $lists;
- } else {
- $result['enclosing'] = 'parent';
- }
-
- return $result;
- }
-
- protected static $libTypeOf = ['value'];
- protected function libTypeOf($args)
- {
- $value = $args[0];
-
- return [Type::T_KEYWORD, $this->getTypeOf($value)];
- }
-
- /**
- * @param array|Number $value
- *
- * @return string
- */
- private function getTypeOf($value)
- {
- switch ($value[0]) {
- case Type::T_KEYWORD:
- if ($value === static::$true || $value === static::$false) {
- return 'bool';
- }
-
- if ($this->coerceColor($value)) {
- return 'color';
- }
-
- // fall-thru
- case Type::T_FUNCTION:
- return 'string';
-
- case Type::T_FUNCTION_REFERENCE:
- return 'function';
-
- case Type::T_LIST:
- if (isset($value[3]) && \is_array($value[3])) {
- return 'arglist';
- }
-
- // fall-thru
- default:
- return $value[0];
- }
- }
-
- protected static $libUnit = ['number'];
- protected function libUnit($args)
- {
- $num = $this->assertNumber($args[0], 'number');
-
- return [Type::T_STRING, '"', [$num->unitStr()]];
- }
-
- protected static $libUnitless = ['number'];
- protected function libUnitless($args)
- {
- $value = $this->assertNumber($args[0], 'number');
-
- return $this->toBool($value->unitless());
- }
-
- protected static $libComparable = [
- ['number1', 'number2'],
- ['number-1', 'number-2']
- ];
- protected function libComparable($args)
- {
- list($number1, $number2) = $args;
-
- if (
- ! $number1 instanceof Number ||
- ! $number2 instanceof Number
- ) {
- throw $this->error('Invalid argument(s) for "comparable"');
- }
-
- return $this->toBool($number1->isComparableTo($number2));
- }
-
- protected static $libStrIndex = ['string', 'substring'];
- protected function libStrIndex($args)
- {
- $string = $this->assertString($args[0], 'string');
- $stringContent = $this->compileStringContent($string);
-
- $substring = $this->assertString($args[1], 'substring');
- $substringContent = $this->compileStringContent($substring);
-
- if (! \strlen($substringContent)) {
- $result = 0;
- } else {
- $result = Util::mbStrpos($stringContent, $substringContent);
- }
-
- return $result === false ? static::$null : new Number($result + 1, '');
- }
-
- protected static $libStrInsert = ['string', 'insert', 'index'];
- protected function libStrInsert($args)
- {
- $string = $this->assertString($args[0], 'string');
- $stringContent = $this->compileStringContent($string);
-
- $insert = $this->assertString($args[1], 'insert');
- $insertContent = $this->compileStringContent($insert);
-
- $index = $this->assertInteger($args[2], 'index');
- if ($index > 0) {
- $index = $index - 1;
- }
- if ($index < 0) {
- $index = max(Util::mbStrlen($stringContent) + 1 + $index, 0);
- }
-
- $string[2] = [
- Util::mbSubstr($stringContent, 0, $index),
- $insertContent,
- Util::mbSubstr($stringContent, $index)
- ];
-
- return $string;
- }
-
- protected static $libStrLength = ['string'];
- protected function libStrLength($args)
- {
- $string = $this->assertString($args[0], 'string');
- $stringContent = $this->compileStringContent($string);
-
- return new Number(Util::mbStrlen($stringContent), '');
- }
-
- protected static $libStrSlice = ['string', 'start-at', 'end-at:-1'];
- protected function libStrSlice($args)
- {
- $string = $this->assertString($args[0], 'string');
- $stringContent = $this->compileStringContent($string);
-
- $start = $this->assertNumber($args[1], 'start-at');
- $start->assertNoUnits('start-at');
- $startInt = $this->assertInteger($start, 'start-at');
- $end = $this->assertNumber($args[2], 'end-at');
- $end->assertNoUnits('end-at');
- $endInt = $this->assertInteger($end, 'end-at');
-
- if ($endInt === 0) {
- return [Type::T_STRING, $string[1], []];
- }
-
- if ($startInt > 0) {
- $startInt--;
- }
-
- if ($endInt < 0) {
- $endInt = Util::mbStrlen($stringContent) + $endInt;
- } else {
- $endInt--;
- }
-
- if ($endInt < $startInt) {
- return [Type::T_STRING, $string[1], []];
- }
-
- $length = $endInt - $startInt + 1; // The end of the slice is inclusive
-
- $string[2] = [Util::mbSubstr($stringContent, $startInt, $length)];
-
- return $string;
- }
-
- protected static $libToLowerCase = ['string'];
- protected function libToLowerCase($args)
- {
- $string = $this->assertString($args[0], 'string');
- $stringContent = $this->compileStringContent($string);
-
- $string[2] = [$this->stringTransformAsciiOnly($stringContent, 'strtolower')];
-
- return $string;
- }
-
- protected static $libToUpperCase = ['string'];
- protected function libToUpperCase($args)
- {
- $string = $this->assertString($args[0], 'string');
- $stringContent = $this->compileStringContent($string);
-
- $string[2] = [$this->stringTransformAsciiOnly($stringContent, 'strtoupper')];
-
- return $string;
- }
-
- /**
- * Apply a filter on a string content, only on ascii chars
- * let extended chars untouched
- *
- * @param string $stringContent
- * @param callable $filter
- * @return string
- */
- protected function stringTransformAsciiOnly($stringContent, $filter)
- {
- $mblength = Util::mbStrlen($stringContent);
- if ($mblength === strlen($stringContent)) {
- return $filter($stringContent);
- }
- $filteredString = "";
- for ($i = 0; $i < $mblength; $i++) {
- $char = Util::mbSubstr($stringContent, $i, 1);
- if (strlen($char) > 1) {
- $filteredString .= $char;
- } else {
- $filteredString .= $filter($char);
- }
- }
-
- return $filteredString;
- }
-
- protected static $libFeatureExists = ['feature'];
- protected function libFeatureExists($args)
- {
- $string = $this->assertString($args[0], 'feature');
- $name = $this->compileStringContent($string);
-
- return $this->toBool(
- \array_key_exists($name, $this->registeredFeatures) ? $this->registeredFeatures[$name] : false
- );
- }
-
- protected static $libFunctionExists = ['name'];
- protected function libFunctionExists($args)
- {
- $string = $this->assertString($args[0], 'name');
- $name = $this->compileStringContent($string);
-
- // user defined functions
- if ($this->has(static::$namespaces['function'] . $name)) {
- return self::$true;
- }
-
- $name = $this->normalizeName($name);
-
- if (isset($this->userFunctions[$name])) {
- return self::$true;
- }
-
- // built-in functions
- $f = $this->getBuiltinFunction($name);
-
- return $this->toBool(\is_callable($f));
- }
-
- protected static $libGlobalVariableExists = ['name'];
- protected function libGlobalVariableExists($args)
- {
- $string = $this->assertString($args[0], 'name');
- $name = $this->compileStringContent($string);
-
- return $this->toBool($this->has($name, $this->rootEnv));
- }
-
- protected static $libMixinExists = ['name'];
- protected function libMixinExists($args)
- {
- $string = $this->assertString($args[0], 'name');
- $name = $this->compileStringContent($string);
-
- return $this->toBool($this->has(static::$namespaces['mixin'] . $name));
- }
-
- protected static $libVariableExists = ['name'];
- protected function libVariableExists($args)
- {
- $string = $this->assertString($args[0], 'name');
- $name = $this->compileStringContent($string);
-
- return $this->toBool($this->has($name));
- }
-
- protected static $libCounter = ['args...'];
- /**
- * Workaround IE7's content counter bug.
+ * Values in scssphp are typed by being wrapped in arrays, their format is
+ * typically:
*
- * @param array $args
+ * array(type, contents [, additional_contents]*)
*
- * @return array
- */
- protected function libCounter($args)
- {
- $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()) {
- $unitString = $limit->unitStr();
- $message = <<<TXT
-random() will no longer ignore \$limit units ($limit) in a future release.
-
-Recommendation: random(\$limit / 1$unitString) * 1$unitString
-
-To preserve current behavior: random(\$limit / 1$unitString)
-
-More info: https://sass-lang.com/d/random-with-units
-
-TXT;
-
- Warn::deprecation($this->addLocationToMessage($message));
- }
-
- $n = $this->assertInteger($limit, 'limit');
-
- if ($n < 1) {
- throw new SassScriptException("\$limit: Must be greater than 0, was $n.");
- }
-
- return new Number(mt_rand(1, $n), '');
- }
-
- $max = mt_getrandmax();
- return new Number(mt_rand(0, $max - 1) / $max, '');
- }
-
- protected static $libUniqueId = [];
- protected function libUniqueId()
- {
- static $id;
-
- if (! isset($id)) {
- $id = PHP_INT_SIZE === 4
- ? mt_rand(0, pow(36, 5)) . str_pad(mt_rand(0, pow(36, 5)) % 10000000, 7, '0', STR_PAD_LEFT)
- : mt_rand(0, pow(36, 8));
- }
-
- $id += mt_rand(0, 10) + 1;
-
- return [Type::T_STRING, '', ['u' . str_pad(base_convert($id, 10, 36), 8, '0', STR_PAD_LEFT)]];
- }
-
- /**
* @param array|Number $value
- * @param bool $force_enclosing_display
- *
- * @return array
- */
- protected function inspectFormatValue($value, $force_enclosing_display = false)
- {
- if ($value === static::$null) {
- $value = [Type::T_KEYWORD, 'null'];
- }
-
- $stringValue = [$value];
-
- if ($value instanceof Number) {
- return [Type::T_STRING, '', $stringValue];
- }
-
- if ($value[0] === Type::T_LIST) {
- if (end($value[2]) === static::$null) {
- array_pop($value[2]);
- $value[2][] = [Type::T_STRING, '', ['']];
- $force_enclosing_display = true;
- }
-
- if (
- ! empty($value['enclosing']) &&
- ($force_enclosing_display ||
- ($value['enclosing'] === 'bracket') ||
- ! \count($value[2]))
- ) {
- $value['enclosing'] = 'forced_' . $value['enclosing'];
- $force_enclosing_display = true;
- } elseif (! \count($value[2])) {
- $value['enclosing'] = 'forced_parent';
- }
-
- foreach ($value[2] as $k => $listelement) {
- $value[2][$k] = $this->inspectFormatValue($listelement, $force_enclosing_display);
- }
-
- $stringValue = [$value];
- }
-
- return [Type::T_STRING, '', $stringValue];
- }
-
- protected static $libInspect = ['value'];
- protected function libInspect($args)
- {
- $value = $args[0];
-
- return $this->inspectFormatValue($value);
- }
-
- /**
- * Preprocess selector args
- *
- * @param array $arg
- * @param string|null $varname
- * @param bool $allowParent
- *
- * @return array
*/
- protected function getSelectorArg($arg, $varname = null, $allowParent = false)
+ public function compileValue($value): string
{
- static $parser = null;
-
- if (\is_null($parser)) {
- $parser = $this->parserFactory(__METHOD__);
- }
-
- if (! $this->checkSelectorArgType($arg)) {
- $var_value = $this->compileValue($arg);
- throw SassScriptException::forArgument("$var_value is not a valid selector: it must be a string, a list of strings, or a list of lists of strings", $varname);
- }
-
-
- if ($arg[0] === Type::T_STRING) {
- $arg[1] = '';
- }
- $arg = $this->compileValue($arg);
-
- $parsedSelector = [];
-
- if ($parser->parseSelector($arg, $parsedSelector, true)) {
- $selector = $this->evalSelectors($parsedSelector);
- $gluedSelector = $this->glueFunctionSelectors($selector);
-
- if (! $allowParent) {
- foreach ($gluedSelector as $selector) {
- foreach ($selector as $s) {
- if (in_array(static::$selfSelector, $s)) {
- throw SassScriptException::forArgument("Parent selectors aren't allowed here.", $varname);
- }
- }
- }
- }
-
- return $gluedSelector;
- }
-
- throw SassScriptException::forArgument("expected more input, invalid selector.", $varname);
- }
-
- /**
- * Check variable type for getSelectorArg() function
- * @param array $arg
- * @param int $maxDepth
- * @return bool
- */
- protected function checkSelectorArgType($arg, $maxDepth = 2)
- {
- if ($arg[0] === Type::T_LIST && $maxDepth > 0) {
- foreach ($arg[2] as $elt) {
- if (! $this->checkSelectorArgType($elt, $maxDepth - 1)) {
- return false;
- }
- }
- return true;
- }
- if (!in_array($arg[0], [Type::T_STRING, Type::T_KEYWORD])) {
- return false;
- }
- return true;
- }
-
- /**
- * Postprocess selector to output in right format
- *
- * @param array $selectors
- *
- * @return array
- */
- protected function formatOutputSelector($selectors)
- {
- $selectors = $this->collapseSelectorsAsList($selectors);
-
- return $selectors;
- }
-
- protected static $libIsSuperselector = ['super', 'sub'];
- protected function libIsSuperselector($args)
- {
- list($super, $sub) = $args;
-
- $super = $this->getSelectorArg($super, 'super');
- $sub = $this->getSelectorArg($sub, 'sub');
-
- return $this->toBool($this->isSuperSelector($super, $sub));
- }
-
- /**
- * Test a $super selector again $sub
- *
- * @param array $super
- * @param array $sub
- *
- * @return bool
- */
- protected function isSuperSelector($super, $sub)
- {
- // one and only one selector for each arg
- if (! $super) {
- throw $this->error('Invalid super selector for isSuperSelector()');
- }
-
- if (! $sub) {
- throw $this->error('Invalid sub selector for isSuperSelector()');
- }
-
- if (count($sub) > 1) {
- foreach ($sub as $s) {
- if (! $this->isSuperSelector($super, [$s])) {
- return false;
- }
- }
- return true;
- }
-
- if (count($super) > 1) {
- foreach ($super as $s) {
- if ($this->isSuperSelector([$s], $sub)) {
- return true;
- }
- }
- return false;
- }
-
- $super = reset($super);
- $sub = reset($sub);
-
- $i = 0;
- $nextMustMatch = false;
-
- foreach ($super as $node) {
- $compound = '';
-
- array_walk_recursive(
- $node,
- function ($value, $key) use (&$compound) {
- $compound .= $value;
- }
- );
-
- if ($this->isImmediateRelationshipCombinator($compound)) {
- if ($node !== $sub[$i]) {
- return false;
- }
-
- $nextMustMatch = true;
- $i++;
- } else {
- while ($i < \count($sub) && ! $this->isSuperPart($node, $sub[$i])) {
- if ($nextMustMatch) {
- return false;
- }
-
- $i++;
- }
-
- if ($i >= \count($sub)) {
- return false;
- }
-
- $nextMustMatch = false;
- $i++;
- }
- }
-
- return true;
- }
-
- /**
- * Test a part of super selector again a part of sub selector
- *
- * @param array $superParts
- * @param array $subParts
- *
- * @return bool
- */
- protected function isSuperPart($superParts, $subParts)
- {
- $i = 0;
-
- foreach ($superParts as $superPart) {
- while ($i < \count($subParts) && $subParts[$i] !== $superPart) {
- $i++;
- }
-
- if ($i >= \count($subParts)) {
- return false;
- }
-
- $i++;
- }
-
- return true;
- }
-
- protected static $libSelectorAppend = ['selector...'];
- protected function libSelectorAppend($args)
- {
- // get the selector... list
- $args = reset($args);
- $args = $args[2];
-
- if (\count($args) < 1) {
- throw $this->error('selector-append() needs at least 1 argument');
- }
-
- $selectors = [];
- foreach ($args as $arg) {
- $selectors[] = $this->getSelectorArg($arg, 'selector');
- }
-
- return $this->formatOutputSelector($this->selectorAppend($selectors));
- }
-
- /**
- * Append parts of the last selector in the list to the previous, recursively
- *
- * @param array $selectors
- *
- * @return array
- *
- * @throws \ScssPhp\ScssPhp\Exception\CompilerException
- */
- protected function selectorAppend($selectors)
- {
- $lastSelectors = array_pop($selectors);
-
- if (! $lastSelectors) {
- throw $this->error('Invalid selector list in selector-append()');
- }
-
- while (\count($selectors)) {
- $previousSelectors = array_pop($selectors);
-
- if (! $previousSelectors) {
- throw $this->error('Invalid selector list in selector-append()');
- }
-
- // do the trick, happening $lastSelector to $previousSelector
- $appended = [];
-
- foreach ($previousSelectors as $previousSelector) {
- foreach ($lastSelectors as $lastSelector) {
- $previous = $previousSelector;
- foreach ($previousSelector as $j => $previousSelectorParts) {
- foreach ($lastSelector as $lastSelectorParts) {
- foreach ($lastSelectorParts as $lastSelectorPart) {
- $previous[$j][] = $lastSelectorPart;
- }
- }
- }
-
- $appended[] = $previous;
- }
- }
-
- $lastSelectors = $appended;
- }
-
- return $lastSelectors;
- }
-
- protected static $libSelectorExtend = [
- ['selector', 'extendee', 'extender'],
- ['selectors', 'extendee', 'extender']
- ];
- protected function libSelectorExtend($args)
- {
- list($selectors, $extendee, $extender) = $args;
-
- $selectors = $this->getSelectorArg($selectors, 'selector');
- $extendee = $this->getSelectorArg($extendee, 'extendee');
- $extender = $this->getSelectorArg($extender, 'extender');
-
- if (! $selectors || ! $extendee || ! $extender) {
- throw $this->error('selector-extend() invalid arguments');
- }
-
- $extended = $this->extendOrReplaceSelectors($selectors, $extendee, $extender);
-
- return $this->formatOutputSelector($extended);
- }
-
- protected static $libSelectorReplace = [
- ['selector', 'original', 'replacement'],
- ['selectors', 'original', 'replacement']
- ];
- protected function libSelectorReplace($args)
- {
- list($selectors, $original, $replacement) = $args;
-
- $selectors = $this->getSelectorArg($selectors, 'selector');
- $original = $this->getSelectorArg($original, 'original');
- $replacement = $this->getSelectorArg($replacement, 'replacement');
-
- if (! $selectors || ! $original || ! $replacement) {
- throw $this->error('selector-replace() invalid arguments');
- }
-
- $replaced = $this->extendOrReplaceSelectors($selectors, $original, $replacement, true);
-
- return $this->formatOutputSelector($replaced);
- }
-
- /**
- * Extend/replace in selectors
- * used by selector-extend and selector-replace that use the same logic
- *
- * @param array $selectors
- * @param array $extendee
- * @param array $extender
- * @param bool $replace
- *
- * @return array
- */
- protected function extendOrReplaceSelectors($selectors, $extendee, $extender, $replace = false)
- {
- $saveExtends = $this->extends;
- $saveExtendsMap = $this->extendsMap;
-
- $this->extends = [];
- $this->extendsMap = [];
-
- foreach ($extendee as $es) {
- if (\count($es) !== 1) {
- throw $this->error('Can\'t extend complex selector.');
- }
-
- // only use the first one
- $this->pushExtends(reset($es), $extender, null);
- }
-
- $extended = [];
-
- foreach ($selectors as $selector) {
- if (! $replace) {
- $extended[] = $selector;
- }
-
- $n = \count($extended);
-
- $this->matchExtends($selector, $extended);
-
- // if didnt match, keep the original selector if we are in a replace operation
- if ($replace && \count($extended) === $n) {
- $extended[] = $selector;
- }
- }
-
- $this->extends = $saveExtends;
- $this->extendsMap = $saveExtendsMap;
-
- return $extended;
- }
-
- protected static $libSelectorNest = ['selector...'];
- protected function libSelectorNest($args)
- {
- // get the selector... list
- $args = reset($args);
- $args = $args[2];
-
- if (\count($args) < 1) {
- throw $this->error('selector-nest() needs at least 1 argument');
- }
-
- $selectorsMap = [];
- foreach ($args as $arg) {
- $selectorsMap[] = $this->getSelectorArg($arg, 'selector', true);
- }
-
- assert(!empty($selectorsMap));
-
- $envs = [];
-
- foreach ($selectorsMap as $selectors) {
- $env = new Environment();
- $env->selectors = $selectors;
-
- $envs[] = $env;
- }
-
- $envs = array_reverse($envs);
- $env = $this->extractEnv($envs);
- $outputSelectors = $this->multiplySelectors($env);
-
- return $this->formatOutputSelector($outputSelectors);
- }
-
- protected static $libSelectorParse = [
- ['selector'],
- ['selectors']
- ];
- protected function libSelectorParse($args)
- {
- $selectors = reset($args);
- $selectors = $this->getSelectorArg($selectors, 'selector');
-
- return $this->formatOutputSelector($selectors);
- }
-
- protected static $libSelectorUnify = ['selectors1', 'selectors2'];
- protected function libSelectorUnify($args)
- {
- list($selectors1, $selectors2) = $args;
-
- $selectors1 = $this->getSelectorArg($selectors1, 'selectors1');
- $selectors2 = $this->getSelectorArg($selectors2, 'selectors2');
-
- if (! $selectors1 || ! $selectors2) {
- throw $this->error('selector-unify() invalid arguments');
- }
-
- // only consider the first compound of each
- $compound1 = reset($selectors1);
- $compound2 = reset($selectors2);
-
- // unify them and that's it
- $unified = $this->unifyCompoundSelectors($compound1, $compound2);
-
- return $this->formatOutputSelector($unified);
- }
-
- /**
- * The selector-unify magic as its best
- * (at least works as expected on test cases)
- *
- * @param array $compound1
- * @param array $compound2
- *
- * @return array
- */
- protected function unifyCompoundSelectors($compound1, $compound2)
- {
- if (! \count($compound1)) {
- return $compound2;
- }
-
- if (! \count($compound2)) {
- return $compound1;
- }
-
- // check that last part are compatible
- $lastPart1 = array_pop($compound1);
- $lastPart2 = array_pop($compound2);
- $last = $this->mergeParts($lastPart1, $lastPart2);
-
- if (! $last) {
- return [[]];
- }
-
- $unifiedCompound = [$last];
- $unifiedSelectors = [$unifiedCompound];
-
- // do the rest
- while (\count($compound1) || \count($compound2)) {
- $part1 = end($compound1);
- $part2 = end($compound2);
-
- if ($part1 && ($match2 = $this->matchPartInCompound($part1, $compound2))) {
- list($compound2, $part2, $after2) = $match2;
-
- if ($after2) {
- $unifiedSelectors = $this->prependSelectors($unifiedSelectors, $after2);
- }
-
- $c = $this->mergeParts($part1, $part2);
- $unifiedSelectors = $this->prependSelectors($unifiedSelectors, [$c]);
-
- $part1 = $part2 = null;
-
- array_pop($compound1);
- }
-
- if ($part2 && ($match1 = $this->matchPartInCompound($part2, $compound1))) {
- list($compound1, $part1, $after1) = $match1;
-
- if ($after1) {
- $unifiedSelectors = $this->prependSelectors($unifiedSelectors, $after1);
- }
-
- $c = $this->mergeParts($part2, $part1);
- $unifiedSelectors = $this->prependSelectors($unifiedSelectors, [$c]);
-
- $part1 = $part2 = null;
-
- array_pop($compound2);
- }
-
- $new = [];
-
- if ($part1 && $part2) {
- array_pop($compound1);
- array_pop($compound2);
-
- $s = $this->prependSelectors($unifiedSelectors, [$part2]);
- $new = array_merge($new, $this->prependSelectors($s, [$part1]));
- $s = $this->prependSelectors($unifiedSelectors, [$part1]);
- $new = array_merge($new, $this->prependSelectors($s, [$part2]));
- } elseif ($part1) {
- array_pop($compound1);
-
- $new = array_merge($new, $this->prependSelectors($unifiedSelectors, [$part1]));
- } elseif ($part2) {
- array_pop($compound2);
-
- $new = array_merge($new, $this->prependSelectors($unifiedSelectors, [$part2]));
- }
-
- if ($new) {
- $unifiedSelectors = $new;
- }
- }
-
- return $unifiedSelectors;
- }
-
- /**
- * Prepend each selector from $selectors with $parts
- *
- * @param array $selectors
- * @param array $parts
- *
- * @return array
- */
- protected function prependSelectors($selectors, $parts)
- {
- $new = [];
-
- foreach ($selectors as $compoundSelector) {
- array_unshift($compoundSelector, $parts);
-
- $new[] = $compoundSelector;
- }
-
- return $new;
- }
-
- /**
- * Try to find a matching part in a compound:
- * - with same html tag name
- * - with some class or id or something in common
- *
- * @param array $part
- * @param array $compound
- *
- * @return array|false
- */
- protected function matchPartInCompound($part, $compound)
- {
- $partTag = $this->findTagName($part);
- $before = $compound;
- $after = [];
-
- // try to find a match by tag name first
- while (\count($before)) {
- $p = array_pop($before);
-
- if ($partTag && $partTag !== '*' && $partTag == $this->findTagName($p)) {
- return [$before, $p, $after];
- }
-
- $after[] = $p;
- }
-
- // try again matching a non empty intersection and a compatible tagname
- $before = $compound;
- $after = [];
-
- while (\count($before)) {
- $p = array_pop($before);
-
- if ($this->checkCompatibleTags($partTag, $this->findTagName($p))) {
- if (\count(array_intersect($part, $p))) {
- return [$before, $p, $after];
- }
- }
-
- $after[] = $p;
- }
-
- return false;
- }
-
- /**
- * Merge two part list taking care that
- * - the html tag is coming first - if any
- * - the :something are coming last
- *
- * @param array $parts1
- * @param array $parts2
- *
- * @return array
- */
- protected function mergeParts($parts1, $parts2)
- {
- $tag1 = $this->findTagName($parts1);
- $tag2 = $this->findTagName($parts2);
- $tag = $this->checkCompatibleTags($tag1, $tag2);
-
- // not compatible tags
- if ($tag === false) {
- return [];
- }
-
- if ($tag) {
- if ($tag1) {
- $parts1 = array_diff($parts1, [$tag1]);
- }
-
- if ($tag2) {
- $parts2 = array_diff($parts2, [$tag2]);
- }
- }
-
- $mergedParts = array_merge($parts1, $parts2);
- $mergedOrderedParts = [];
-
- foreach ($mergedParts as $part) {
- if (strpos($part, ':') === 0) {
- $mergedOrderedParts[] = $part;
- }
- }
-
- $mergedParts = array_diff($mergedParts, $mergedOrderedParts);
- $mergedParts = array_merge($mergedParts, $mergedOrderedParts);
-
- if ($tag) {
- array_unshift($mergedParts, $tag);
- }
-
- return $mergedParts;
- }
-
- /**
- * Check the compatibility between two tag names:
- * if both are defined they should be identical or one has to be '*'
- *
- * @param string $tag1
- * @param string $tag2
- *
- * @return array|false
- */
- protected function checkCompatibleTags($tag1, $tag2)
- {
- $tags = [$tag1, $tag2];
- $tags = array_unique($tags);
- $tags = array_filter($tags);
-
- if (\count($tags) > 1) {
- $tags = array_diff($tags, ['*']);
- }
-
- // not compatible nodes
- if (\count($tags) > 1) {
- return false;
- }
-
- return $tags;
- }
-
- /**
- * Find the html tag name in a selector parts list
- *
- * @param string[] $parts
- *
- * @return string
- */
- protected function findTagName($parts)
- {
- foreach ($parts as $part) {
- if (! preg_match('/^[\[.:#%_-]/', $part)) {
- return $part;
- }
- }
-
- return '';
- }
-
- protected static $libSimpleSelectors = ['selector'];
- protected function libSimpleSelectors($args)
- {
- $selector = reset($args);
- $selector = $this->getSelectorArg($selector, 'selector');
-
- // remove selectors list layer, keeping the first one
- $selector = reset($selector);
-
- // remove parts list layer, keeping the first part
- $part = reset($selector);
-
- $listParts = [];
-
- foreach ($part as $p) {
- $listParts[] = [Type::T_STRING, '', [$p]];
- }
-
- 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];
+ return (string) $this->legacyValueToValue($value);
}
}
diff --git a/vendor/scssphp/scssphp/src/Compiler/CachedResult.php b/vendor/scssphp/scssphp/src/Compiler/CachedResult.php
deleted file mode 100644
index a66291996..000000000
--- a/vendor/scssphp/scssphp/src/Compiler/CachedResult.php
+++ /dev/null
@@ -1,77 +0,0 @@
-<?php
-
-/**
- * SCSSPHP
- *
- * @copyright 2012-2020 Leaf Corcoran
- *
- * @license http://opensource.org/licenses/MIT MIT
- *
- * @link http://scssphp.github.io/scssphp
- */
-
-namespace ScssPhp\ScssPhp\Compiler;
-
-use ScssPhp\ScssPhp\CompilationResult;
-
-/**
- * @internal
- */
-class CachedResult
-{
- /**
- * @var CompilationResult
- */
- private $result;
-
- /**
- * @var array<string, int>
- */
- private $parsedFiles;
-
- /**
- * @var array
- * @phpstan-var list<array{currentDir: string|null, path: string, filePath: string}>
- */
- private $resolvedImports;
-
- /**
- * @param CompilationResult $result
- * @param array<string, int> $parsedFiles
- * @param array $resolvedImports
- *
- * @phpstan-param list<array{currentDir: string|null, path: string, filePath: string}> $resolvedImports
- */
- public function __construct(CompilationResult $result, array $parsedFiles, array $resolvedImports)
- {
- $this->result = $result;
- $this->parsedFiles = $parsedFiles;
- $this->resolvedImports = $resolvedImports;
- }
-
- /**
- * @return CompilationResult
- */
- public function getResult()
- {
- return $this->result;
- }
-
- /**
- * @return array<string, int>
- */
- public function getParsedFiles()
- {
- return $this->parsedFiles;
- }
-
- /**
- * @return array
- *
- * @phpstan-return list<array{currentDir: string|null, path: string, filePath: string}>
- */
- public function getResolvedImports()
- {
- return $this->resolvedImports;
- }
-}
diff --git a/vendor/scssphp/scssphp/src/Compiler/Environment.php b/vendor/scssphp/scssphp/src/Compiler/Environment.php
deleted file mode 100644
index b205a077f..000000000
--- a/vendor/scssphp/scssphp/src/Compiler/Environment.php
+++ /dev/null
@@ -1,68 +0,0 @@
-<?php
-
-/**
- * SCSSPHP
- *
- * @copyright 2012-2020 Leaf Corcoran
- *
- * @license http://opensource.org/licenses/MIT MIT
- *
- * @link http://scssphp.github.io/scssphp
- */
-
-namespace ScssPhp\ScssPhp\Compiler;
-
-/**
- * Compiler environment
- *
- * @author Anthon Pang <anthon.pang@gmail.com>
- *
- * @internal
- */
-class Environment
-{
- /**
- * @var \ScssPhp\ScssPhp\Block|null
- */
- public $block;
-
- /**
- * @var \ScssPhp\ScssPhp\Compiler\Environment|null
- */
- public $parent;
-
- /**
- * @var Environment|null
- */
- public $declarationScopeParent;
-
- /**
- * @var Environment|null
- */
- public $parentStore;
-
- /**
- * @var array|null
- */
- public $selectors;
-
- /**
- * @var string|null
- */
- public $marker;
-
- /**
- * @var array
- */
- public $store;
-
- /**
- * @var array
- */
- public $storeUnreduced;
-
- /**
- * @var int
- */
- public $depth;
-}
diff --git a/vendor/scssphp/scssphp/src/Compiler/LegacyValueVisitor.php b/vendor/scssphp/scssphp/src/Compiler/LegacyValueVisitor.php
new file mode 100644
index 000000000..d87e98107
--- /dev/null
+++ b/vendor/scssphp/scssphp/src/Compiler/LegacyValueVisitor.php
@@ -0,0 +1,117 @@
+<?php
+
+/**
+ * SCSSPHP
+ *
+ * @copyright 2012-2020 Leaf Corcoran
+ *
+ * @license http://opensource.org/licenses/MIT MIT
+ *
+ * @link http://scssphp.github.io/scssphp
+ */
+
+namespace ScssPhp\ScssPhp\Compiler;
+
+use ScssPhp\ScssPhp\Compiler;
+use ScssPhp\ScssPhp\Exception\SassScriptException;
+use ScssPhp\ScssPhp\Node\Number;
+use ScssPhp\ScssPhp\Type;
+use ScssPhp\ScssPhp\Util\NumberUtil;
+use ScssPhp\ScssPhp\Value\SassArgumentList;
+use ScssPhp\ScssPhp\Value\SassBoolean;
+use ScssPhp\ScssPhp\Value\SassCalculation;
+use ScssPhp\ScssPhp\Value\SassColor;
+use ScssPhp\ScssPhp\Value\SassFunction;
+use ScssPhp\ScssPhp\Value\SassList;
+use ScssPhp\ScssPhp\Value\SassMap;
+use ScssPhp\ScssPhp\Value\SassMixin;
+use ScssPhp\ScssPhp\Value\SassNumber;
+use ScssPhp\ScssPhp\Value\SassString;
+use ScssPhp\ScssPhp\Visitor\ValueVisitor;
+
+/**
+ * Converts values to the legacy representation.
+ *
+ * @internal
+ * @template-implements ValueVisitor<array|Number>
+ */
+final class LegacyValueVisitor implements ValueVisitor
+{
+ public function visitBoolean(SassBoolean $value)
+ {
+ return $value->getValue() ? Compiler::$true : Compiler::$false;
+ }
+
+ public function visitCalculation(SassCalculation $value)
+ {
+ return [Type::T_STRING, '', $value->toCssString()];
+ }
+
+ public function visitColor(SassColor $value)
+ {
+ if (NumberUtil::fuzzyEquals($value->getAlpha(), 1)) {
+ return [Type::T_COLOR, $value->getRed(), $value->getGreen(), $value->getBlue()];
+ }
+
+ return [Type::T_COLOR, $value->getRed(), $value->getGreen(), $value->getBlue(), $value->getAlpha()];
+ }
+
+ public function visitFunction(SassFunction $value)
+ {
+ throw new SassScriptException('Functions are not supported by the legacy value API. Migrate your custom function to the new API to accept mixins as arguments.');
+ }
+
+ public function visitMixin(SassMixin $value)
+ {
+ throw new SassScriptException('Mixins are not supported by the legacy value API. Migrate your custom function to the new API to accept mixins as arguments.');
+ }
+
+ public function visitList(SassList $value)
+ {
+ $items = [];
+ foreach ($value->asList() as $item) {
+ $items[] = $item->accept($this);
+ }
+ $list = [Type::T_LIST, $value->getSeparator()->getSeparator() ?? '', $items];
+ if ($value->hasBrackets()) {
+ $list['enclosing'] = 'bracket';
+ }
+ if ($value instanceof SassArgumentList) {
+ $keywords = [];
+ foreach ($value->getKeywords() as $keywordName => $keywordValue) {
+ $keywords[$keywordName] = $keywordValue->accept($this);
+ }
+ $list[3] = $keywords;
+ }
+
+ return $list;
+ }
+
+ public function visitMap(SassMap $value)
+ {
+ $keys = [];
+ $values = [];
+
+ foreach ($value->getContents() as $key => $item) {
+ $keys[] = $key->accept($this);
+ $values[] = $item->accept($this);
+ }
+
+ return [Type::T_MAP, $keys, $values];
+ }
+
+ public function visitNull()
+ {
+ return Compiler::$null;
+ }
+
+ public function visitNumber(SassNumber $value)
+ {
+ return new Number($value->getValue(), $value->getNumeratorUnits(), $value->getDenominatorUnits());
+ }
+
+ public function visitString(SassString $value)
+ {
+ return [Type::T_STRING, $value->hasQuotes() ? '"' : '', [$value->getText()]];
+ }
+}
diff --git a/vendor/scssphp/scssphp/src/Deprecation.php b/vendor/scssphp/scssphp/src/Deprecation.php
new file mode 100644
index 000000000..5ab0c5d86
--- /dev/null
+++ b/vendor/scssphp/scssphp/src/Deprecation.php
@@ -0,0 +1,172 @@
+<?php
+
+/**
+ * SCSSPHP
+ *
+ * @copyright 2012-2020 Leaf Corcoran
+ *
+ * @license http://opensource.org/licenses/MIT MIT
+ *
+ * @link http://scssphp.github.io/scssphp
+ */
+
+namespace ScssPhp\ScssPhp;
+
+/**
+ * A deprecated feature in the language.
+ *
+ * Code consuming this enum outside Scssphp must not rely on exhaustiveness checks. New values will be added
+ * in this enum in minor versions of the package without considering that as a BC break.
+ */
+enum Deprecation: string
+{
+ /**
+ * Deprecation for passing a string directly to meta.call().
+ */
+ case callString = 'call-string';
+
+ /**
+ * Deprecation for @elseif.
+ */
+ case elseif = 'elseif';
+
+ /**
+ * Deprecation for @-moz-document.
+ */
+ case mozDocument = 'moz-document';
+
+ /**
+ * Deprecation for declaring new variables with !global.
+ */
+ case newGlobal = 'new-global';
+
+ /**
+ * Deprecation for / operator for division.
+ */
+ case slashDiv = 'slash-div';
+
+ /**
+ * Deprecation for leading, trailing, and repeated combinators.
+ */
+ case bogusCombinators = 'bogus-combinators';
+
+ /**
+ * Deprecation for ambiguous + and - operators.
+ */
+ case strictUnary = 'strict-unary';
+
+ /**
+ * Deprecation for passing invalid units to built-in functions.
+ */
+ case functionUnits = 'function-units';
+
+ /**
+ * Deprecation for using !default or !global multiple times for one variable.
+ */
+ case duplicateVarFlags = 'duplicate-var-flags';
+
+ /**
+ * Deprecation for passing percentages to the Sass abs() function.
+ */
+ case absPercent = 'abs-percent';
+
+ /**
+ * Deprecation for function and mixin names beginning with --.
+ */
+ case cssFunctionMixin = 'css-function-mixin';
+
+ /**
+ * Deprecation for declarations after or between nested rules.
+ */
+ case mixedDecls = 'mixed-decls';
+
+ /**
+ * Deprecation for meta.feature-exists.
+ */
+ case featureExists = 'feature-exists';
+
+ /**
+ * Used for deprecations coming from user-authored code.
+ */
+ case userAuthored = 'user-authored';
+
+ public function getDescription(): ?string
+ {
+ return match ($this) {
+ self::callString => 'Passing a string directly to meta.call().',
+ self::elseif => '@elseif.',
+ self::mozDocument => '@-moz-document.',
+ self::newGlobal => 'Declaring new variables with !global.',
+ self::slashDiv => '/ operator for division.',
+ self::bogusCombinators => 'Leading, trailing, and repeated combinators.',
+ self::strictUnary => 'Ambiguous + and - operators.',
+ self::functionUnits => 'Passing invalid units to built-in functions.',
+ self::duplicateVarFlags => 'Using !default or !global multiple times for one variable.',
+ self::absPercent => 'Passing percentages to the Sass abs() function.',
+ self::cssFunctionMixin => 'Function and mixin names beginning with --.',
+ self::mixedDecls => 'Declarations after or between nested rules.',
+ self::featureExists => 'meta.feature-exists',
+ self::userAuthored => null,
+ };
+ }
+
+ /**
+ * The version in which this feature was first deprecated.
+ */
+ public function getDeprecatedIn(): ?string
+ {
+ return match ($this) {
+ self::callString => '1.2.0',
+ self::elseif => '2.0.0',
+ self::mozDocument => '2.0.0',
+ self::newGlobal => '2.0.0',
+ self::slashDiv => null,
+ self::bogusCombinators => '2.0.0',
+ self::strictUnary => '2.0.0',
+ self::functionUnits => '2.0.0',
+ self::duplicateVarFlags => '2.0.0',
+ self::absPercent => '2.0.0',
+ self::cssFunctionMixin => '2.0.0',
+ self::mixedDecls => '2.0.0',
+ self::featureExists => '2.0.0',
+ self::userAuthored => null,
+ };
+ }
+
+ /**
+ * The version this feature was fully removed in, making the
+ * deprecation obsolete.
+ *
+ * For deprecations that are not yet obsolete, this should be null.
+ */
+ public function getObsoleteIn(): ?string
+ {
+ return null; // For now, no deprecation is obsolete
+ }
+
+ public function isFuture(): bool
+ {
+ if ($this === self::userAuthored) {
+ return false;
+ }
+
+ return $this->getDeprecatedIn() === null;
+ }
+
+ public function getStatus(): DeprecationStatus
+ {
+ if ($this === self::userAuthored) {
+ return DeprecationStatus::user;
+ }
+
+ if ($this->isFuture()) {
+ return DeprecationStatus::future;
+ }
+
+ if ($this->getObsoleteIn() !== null) {
+ return DeprecationStatus::obsolete;
+ }
+
+ return DeprecationStatus::active;
+ }
+}
diff --git a/vendor/scssphp/scssphp/src/DeprecationStatus.php b/vendor/scssphp/scssphp/src/DeprecationStatus.php
new file mode 100644
index 000000000..fe39a41cb
--- /dev/null
+++ b/vendor/scssphp/scssphp/src/DeprecationStatus.php
@@ -0,0 +1,11 @@
+<?php
+
+namespace ScssPhp\ScssPhp;
+
+enum DeprecationStatus
+{
+ case active;
+ case user;
+ case future;
+ case obsolete;
+}
diff --git a/vendor/scssphp/scssphp/src/Evaluation/ArgumentResults.php b/vendor/scssphp/scssphp/src/Evaluation/ArgumentResults.php
new file mode 100644
index 000000000..f56b6c907
--- /dev/null
+++ b/vendor/scssphp/scssphp/src/Evaluation/ArgumentResults.php
@@ -0,0 +1,105 @@
+<?php
+
+/**
+ * SCSSPHP
+ *
+ * @copyright 2012-2020 Leaf Corcoran
+ *
+ * @license http://opensource.org/licenses/MIT MIT
+ *
+ * @link http://scssphp.github.io/scssphp
+ */
+
+namespace ScssPhp\ScssPhp\Evaluation;
+
+use ScssPhp\ScssPhp\Ast\AstNode;
+use ScssPhp\ScssPhp\Value\ListSeparator;
+use ScssPhp\ScssPhp\Value\Value;
+
+/**
+ * The result of evaluating arguments to a function or mixin.
+ *
+ * @internal
+ */
+final class ArgumentResults
+{
+ /**
+ * Arguments passed by position.
+ *
+ * @var list<Value>
+ */
+ private readonly array $positional;
+
+ /**
+ * The {@see AstNode}s that hold the spans for each {@see positional} argument.
+ *
+ * @var list<AstNode>
+ */
+ private readonly array $positionalNodes;
+
+ /**
+ * @var array<string, Value>
+ */
+ private readonly array $named;
+
+ /**
+ * The {@see AstNode}s that hold the spans for each {@see named} argument.
+ *
+ * @var array<string, AstNode>
+ */
+ private readonly array $namedNodes;
+
+ private readonly ListSeparator $separator;
+
+ /**
+ * @param list<Value> $positional
+ * @param list<AstNode> $positionalNodes
+ * @param array<string, Value> $named
+ * @param array<string, AstNode> $namedNodes
+ */
+ public function __construct(array $positional, array $positionalNodes, array $named, array $namedNodes, ListSeparator $separator)
+ {
+ $this->positional = $positional;
+ $this->positionalNodes = $positionalNodes;
+ $this->named = $named;
+ $this->namedNodes = $namedNodes;
+ $this->separator = $separator;
+ }
+
+ /**
+ * @return list<Value>
+ */
+ public function getPositional(): array
+ {
+ return $this->positional;
+ }
+
+ /**
+ * @return list<AstNode>
+ */
+ public function getPositionalNodes(): array
+ {
+ return $this->positionalNodes;
+ }
+
+ /**
+ * @return array<string, Value>
+ */
+ public function getNamed(): array
+ {
+ return $this->named;
+ }
+
+ /**
+ * @return array<string, AstNode>
+ */
+ public function getNamedNodes(): array
+ {
+ return $this->namedNodes;
+ }
+
+ public function getSeparator(): ListSeparator
+ {
+ return $this->separator;
+ }
+}
diff --git a/vendor/scssphp/scssphp/src/Evaluation/Environment.php b/vendor/scssphp/scssphp/src/Evaluation/Environment.php
new file mode 100644
index 000000000..492606325
--- /dev/null
+++ b/vendor/scssphp/scssphp/src/Evaluation/Environment.php
@@ -0,0 +1,551 @@
+<?php
+
+/**
+ * SCSSPHP
+ *
+ * @copyright 2012-2020 Leaf Corcoran
+ *
+ * @license http://opensource.org/licenses/MIT MIT
+ *
+ * @link http://scssphp.github.io/scssphp
+ */
+
+namespace ScssPhp\ScssPhp\Evaluation;
+
+use ScssPhp\ScssPhp\Ast\AstNode;
+use ScssPhp\ScssPhp\SassCallable\SassCallable;
+use ScssPhp\ScssPhp\SassCallable\UserDefinedCallable;
+use ScssPhp\ScssPhp\Value\Value;
+use SourceSpan\FileSpan;
+
+/**
+ * The lexical environment in which Sass is executed.
+ *
+ * This tracks lexically-scoped information, such as variables, functions, and
+ * mixins.
+ *
+ * @internal
+ */
+final class Environment
+{
+ /**
+ * A list of variables defined at each lexical scope level.
+ *
+ * Each scope maps the names of declared variables to their values.
+ *
+ * The first element is the global scope, and each successive element is
+ * deeper in the tree.
+ *
+ * @var array<int, \ArrayObject<string, Value>>
+ */
+ private array $variables;
+
+ /**
+ * The nodes where each variable in {@see variables} was defined.
+ *
+ * This stores {@see AstNode}s rather than {@see FileSpan}s so it can avoid calling
+ * {@see AstNode::getSspan} if the span isn't required, since some nodes need to do
+ * real work to manufacture a source span.
+ *
+ * @var array<int, \ArrayObject<string, AstNode>>
+ */
+ private array $variableNodes;
+
+ /**
+ * A map of variable names to their indices in {@see variables}.
+ *
+ * This map is filled in as-needed, and may not be complete.
+ *
+ * @var array<string, int>
+ */
+ private array $variableIndices = [];
+
+ /**
+ * A list of functions defined at each lexical scope level.
+ *
+ * Each scope maps the names of declared functions to their values.
+ *
+ * The first element is the global scope, and each successive element is
+ * deeper in the tree.
+ *
+ * @var array<int, \ArrayObject<string, SassCallable>>
+ */
+ private array $functions;
+
+ /**
+ * A map of function names to their indices in {@see functions}.
+ *
+ * This map is filled in as-needed, and may not be complete.
+ *
+ * @var array<string, int>
+ */
+ private array $functionIndices = [];
+
+ /**
+ * A list of mixins defined at each lexical scope level.
+ *
+ * Each scope maps the names of declared mixins to their values.
+ *
+ * The first element is the global scope, and each successive element is
+ * deeper in the tree.
+ *
+ * @var array<int, \ArrayObject<string, SassCallable>>
+ */
+ private array $mixins;
+
+ /**
+ * A map of mixin names to their indices in {@see mixins}.
+ *
+ * This map is filled in as-needed, and may not be complete.
+ *
+ * @var array<string, int>
+ */
+ private array $mixinIndices = [];
+
+ /**
+ * The content block passed to the lexically-enclosing mixin, or `null` if
+ * this is not in a mixin, or if no content block was passed.
+ */
+ private ?UserDefinedCallable $content;
+
+ /**
+ * Whether the environment is lexically within a mixin.
+ */
+ private bool $inMixin = false;
+
+ /**
+ * Whether the environment is currently in a global or semi-global scope.
+ *
+ * A semi-global scope can assign to global variables, but it doesn't declare
+ * them by default.
+ */
+ private bool $inSemiGlobalScope = true;
+
+ /**
+ * The name of the last variable that was accessed.
+ *
+ * This is cached to speed up repeated references to the same variable, as
+ * well as references to the last variable's {@see FileSpan}.
+ */
+ private ?string $lastVariableName = null;
+
+ /**
+ * The index in {@see variables} of the last variable that was accessed.
+ */
+ private ?int $lastVariableIndex = null;
+
+ public static function create(): Environment
+ {
+ return new Environment([new \ArrayObject()], [new \ArrayObject()], [new \ArrayObject()], [new \ArrayObject()]);
+ }
+
+ /**
+ * @param array<int, \ArrayObject<string, Value>> $variables
+ * @param array<int, \ArrayObject<string, AstNode>> $variableNodes
+ * @param array<int, \ArrayObject<string, SassCallable>> $functions
+ * @param array<int, \ArrayObject<string, SassCallable>> $mixins
+ */
+ private function __construct(array $variables, array $variableNodes, array $functions, array $mixins, ?UserDefinedCallable $content = null)
+ {
+ $this->variables = $variables;
+ $this->variableNodes = $variableNodes;
+ $this->functions = $functions;
+ $this->mixins = $mixins;
+ $this->content = $content;
+ }
+
+ public function getContent(): ?UserDefinedCallable
+ {
+ return $this->content;
+ }
+
+ /**
+ * Whether the environment is lexically at the root of the document.
+ */
+ public function atRoot(): bool
+ {
+ return \count($this->variables) === 1;
+ }
+
+ public function isInMixin(): bool
+ {
+ return $this->inMixin;
+ }
+
+ /**
+ * Creates a closure based on this environment.
+ *
+ * Any scope changes in this environment will not affect the closure.
+ * However, any new declarations or assignments in scopes that are visible
+ * when the closure was created will be reflected.
+ */
+ public function closure(): Environment
+ {
+ return new Environment($this->variables, $this->variableNodes, $this->functions, $this->mixins, $this->content);
+ }
+
+ /**
+ * Returns a new environment to use for an imported file.
+ *
+ * The returned environment shares this environment's variables, functions,
+ * and mixins, but excludes most modules (except for global modules that
+ * result from importing a file with forwards).
+ */
+ public function forImport(): Environment
+ {
+ return new Environment($this->variables, $this->variableNodes, $this->functions, $this->mixins, $this->content);
+ }
+
+ public function getVariable(string $name): ?Value
+ {
+ if ($this->lastVariableName === $name) {
+ assert($this->lastVariableIndex !== null);
+
+ return $this->variables[$this->lastVariableIndex][$name] ?? null;
+ }
+
+ $index = $this->variableIndices[$name] ?? null;
+
+ if ($index !== null) {
+ $this->lastVariableName = $name;
+ $this->lastVariableIndex = $index;
+
+ return $this->variables[$index][$name] ?? null;
+ }
+
+ $index = $this->variableIndex($name);
+
+ if ($index === null) {
+ return null;
+ }
+
+ $this->lastVariableName = $name;
+ $this->lastVariableIndex = $index;
+ $this->variableIndices[$name] = $index;
+
+ return $this->variables[$index][$name] ?? null;
+ }
+
+ public function getVariableNode(string $name): ?AstNode
+ {
+ if ($this->lastVariableName === $name) {
+ assert($this->lastVariableIndex !== null);
+
+ return $this->variableNodes[$this->lastVariableIndex][$name] ?? null;
+ }
+
+ $index = $this->variableIndices[$name] ?? null;
+
+ if ($index !== null) {
+ $this->lastVariableName = $name;
+ $this->lastVariableIndex = $index;
+
+ return $this->variableNodes[$index][$name] ?? null;
+ }
+
+ $index = $this->variableIndex($name);
+
+ if ($index === null) {
+ return null;
+ }
+
+ $this->lastVariableName = $name;
+ $this->lastVariableIndex = $index;
+ $this->variableIndices[$name] = $index;
+
+ return $this->variableNodes[$index][$name] ?? null;
+ }
+
+ /**
+ * Returns whether a variable named $name exists.
+ */
+ public function variableExists(string $name): bool
+ {
+ return $this->getVariable($name) !== null;
+ }
+
+ /**
+ * Returns whether a global variable named $name exists.
+ */
+ public function globalVariableExists(string $name): bool
+ {
+ return isset($this->variables[0][$name]);
+ }
+
+ /**
+ * Returns the index of the last map in {@see variables} that has a $name key,
+ * or `null` if none exists.
+ */
+ private function variableIndex(string $name): ?int
+ {
+ for ($i = \count($this->variables) - 1; $i >= 0; $i--) {
+ if (isset($this->variables[$i][$name])) {
+ return $i;
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Sets the variable named $name to $value.
+ *
+ * If $global is `true`, this sets the variable at the top-level scope.
+ * Otherwise, if the variable was already defined, it'll set it in the
+ * previous scope. If it's undefined, it'll set it in the current scope.
+ */
+ public function setVariable(string $name, Value $value, AstNode $nodeWithSpan, bool $global = false): void
+ {
+ if ($global || $this->atRoot()) {
+ // Don't set the index if there's already a variable with the given name,
+ // since local accesses should still return the local variable.
+ if (!isset($this->variableIndices[$name])) {
+ $this->lastVariableName = $name;
+ $this->lastVariableIndex = 0;
+ $this->variableIndices[$name] = 0;
+ }
+
+ $this->variables[0][$name] = $value;
+ $this->variableNodes[0][$name] = $nodeWithSpan;
+ return;
+ }
+
+ if ($this->lastVariableName === $name) {
+ assert($this->lastVariableIndex !== null);
+ $index = $this->lastVariableIndex;
+ } else {
+ if (!isset($this->variableIndices[$name])) {
+ $this->variableIndices[$name] = $this->variableIndex($name) ?? \count($this->variables) - 1;
+ }
+ $index = $this->variableIndices[$name];
+ }
+
+ if (!$this->inSemiGlobalScope && $index === 0) {
+ $index = \count($this->variables) - 1;
+ $this->variableIndices[$name] = $index;
+ }
+
+ $this->lastVariableName = $name;
+ $this->lastVariableIndex = $index;
+ $this->variables[$index][$name] = $value;
+ $this->variableNodes[$index][$name] = $nodeWithSpan;
+ }
+
+ /**
+ * Sets the variable named $name to $value.
+ *
+ * Unlike {@see setVariable}, this will declare the variable in the current scope
+ * even if a declaration already exists in an outer scope.
+ */
+ public function setLocalVariable(string $name, Value $value, AstNode $nodeWithSpan): void
+ {
+ $index = \count($this->variables) - 1;
+ $this->lastVariableName = $name;
+ $this->lastVariableIndex = $index;
+ $this->variableIndices[$name] = $index;
+ $this->variables[$index][$name] = $value;
+ $this->variableNodes[$index][$name] = $nodeWithSpan;
+ }
+
+ public function getFunction(string $name): ?SassCallable
+ {
+ $index = $this->functionIndices[$name] ?? null;
+
+ if ($index !== null) {
+ return $this->functions[$index][$name] ?? null;
+ }
+
+ $index = $this->functionIndex($name);
+ if ($index === null) {
+ return null;
+ }
+
+ $this->functionIndices[$name] = $index;
+
+ return $this->functions[$index][$name] ?? null;
+ }
+
+ /**
+ * Returns the index of the last map in {@see functions} that has a $name key,
+ * or `null` if none exists.
+ */
+ private function functionIndex(string $name): ?int
+ {
+ for ($i = \count($this->functions) - 1; $i >= 0; $i--) {
+ if (isset($this->functions[$i][$name])) {
+ return $i;
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Returns whether a function named $name exists.
+ */
+ public function functionExists(string $name): bool
+ {
+ return $this->getFunction($name) !== null;
+ }
+
+ public function setFunction(SassCallable $callable): void
+ {
+ $index = \count($this->functions) - 1;
+ $name = $callable->getName();
+ $this->functionIndices[$name] = $index;
+ $this->functions[$index][$name] = $callable;
+ }
+
+ public function getMixin(string $name): ?SassCallable
+ {
+ $index = $this->mixinIndices[$name] ?? null;
+
+ if ($index !== null) {
+ return $this->mixins[$index][$name] ?? null;
+ }
+
+ $index = $this->mixinIndex($name);
+ if ($index === null) {
+ return null;
+ }
+
+ $this->mixinIndices[$name] = $index;
+
+ return $this->mixins[$index][$name] ?? null;
+ }
+
+ /**
+ * Returns the index of the last map in {@see mixins} that has a $name key,
+ * or `null` if none exists.
+ */
+ private function mixinIndex(string $name): ?int
+ {
+ for ($i = \count($this->mixins) - 1; $i >= 0; $i--) {
+ if (isset($this->mixins[$i][$name])) {
+ return $i;
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Returns whether a mixin named $name exists.
+ */
+ public function mixinExists(string $name): bool
+ {
+ return $this->getMixin($name) !== null;
+ }
+
+ public function setMixin(SassCallable $callable): void
+ {
+ $index = \count($this->mixins) - 1;
+ $name = $callable->getName();
+ $this->mixinIndices[$name] = $index;
+ $this->mixins[$index][$name] = $callable;
+ }
+
+ /**
+ * Sets $content as {@see content} for the duration of $callback.
+ *
+ * @param callable(): void $callback
+ *
+ * @param-immediately-invoked-callable $callback
+ */
+ public function withContent(?UserDefinedCallable $content, callable $callback): void
+ {
+ $oldContent = $this->content;
+ $this->content = $content;
+ $callback();
+ $this->content = $oldContent;
+ }
+
+ /**
+ * Sets {@see inMixin} to `true` for the duration of $callback.
+ *
+ * @param callable(): void $callback
+ *
+ * @param-immediately-invoked-callable $callback
+ */
+ public function asMixin(callable $callback): void
+ {
+ $oldInMixin = $this->inMixin;
+ $this->inMixin = true;
+ $callback();
+ $this->inMixin = $oldInMixin;
+ }
+
+ /**
+ * Runs $callback in a new scope.
+ *
+ * Variables, functions, and mixins declared in a given scope are
+ * inaccessible outside of it. If $semiGlobal is passed, this scope can
+ * assign to global variables without a `!global` declaration.
+ *
+ * If $when is false, this doesn't create a new scope and instead just
+ * executes $callback and returns its result.
+ *
+ * @template T
+ *
+ * @param callable(): T $callback
+ *
+ * @return T
+ *
+ * @param-immediately-invoked-callable $callback
+ */
+ public function scope(callable $callback, bool $when = true, bool $semiGlobal = false)
+ {
+ // We have to track semi-globalness even if `!$when` so that
+ //
+ // div {
+ // @if ... {
+ // $x: y;
+ // }
+ // }
+ //
+ // doesn't assign to the global scope.
+ $semiGlobal = $semiGlobal && $this->inSemiGlobalScope;
+ $wasInSemiGlobalScope = $this->inSemiGlobalScope;
+ $this->inSemiGlobalScope = $semiGlobal;
+
+ if (!$when) {
+ try {
+ return $callback();
+ } finally {
+ $this->inSemiGlobalScope = $wasInSemiGlobalScope;
+ }
+ }
+
+ $this->variables[] = new \ArrayObject();
+ $this->variableNodes[] = new \ArrayObject();
+ $this->functions[] = new \ArrayObject();
+ $this->mixins[] = new \ArrayObject();
+
+ try {
+ return $callback();
+ } finally {
+ $this->inSemiGlobalScope = $wasInSemiGlobalScope;
+ $this->lastVariableName = null;
+ $this->lastVariableIndex = null;
+
+ $removedVariables = array_pop($this->variables);
+ assert($removedVariables !== null);
+ foreach ($removedVariables as $name => $_) {
+ unset($this->variableIndices[$name]);
+ }
+ array_pop($this->variableNodes);
+
+ $removedFunctions = array_pop($this->functions);
+ assert($removedFunctions !== null);
+ foreach ($removedFunctions as $name => $_) {
+ unset($this->functionIndices[$name]);
+ }
+
+ $removedMixins = array_pop($this->mixins);
+ assert($removedMixins !== null);
+ foreach ($removedMixins as $name => $_) {
+ unset($this->mixinIndices[$name]);
+ }
+ }
+ }
+}
diff --git a/vendor/scssphp/scssphp/src/Evaluation/EvaluateResult.php b/vendor/scssphp/scssphp/src/Evaluation/EvaluateResult.php
new file mode 100644
index 000000000..f9e6bf206
--- /dev/null
+++ b/vendor/scssphp/scssphp/src/Evaluation/EvaluateResult.php
@@ -0,0 +1,53 @@
+<?php
+
+/**
+ * SCSSPHP
+ *
+ * @copyright 2012-2020 Leaf Corcoran
+ *
+ * @license http://opensource.org/licenses/MIT MIT
+ *
+ * @link http://scssphp.github.io/scssphp
+ */
+
+namespace ScssPhp\ScssPhp\Evaluation;
+
+use ScssPhp\ScssPhp\Ast\Css\CssStylesheet;
+
+/**
+ * The result of compiling a Sass document to a CSS tree, along with metadata
+ * about the compilation process.
+ *
+ * @internal
+ */
+final class EvaluateResult
+{
+ private readonly CssStylesheet $stylesheet;
+
+ /**
+ * @var list<string>
+ */
+ private readonly array $loadedUrls;
+
+ /**
+ * @param list<string> $loadedUrls
+ */
+ public function __construct(CssStylesheet $stylesheet, array $loadedUrls)
+ {
+ $this->stylesheet = $stylesheet;
+ $this->loadedUrls = $loadedUrls;
+ }
+
+ public function getStylesheet(): CssStylesheet
+ {
+ return $this->stylesheet;
+ }
+
+ /**
+ * @return list<string>
+ */
+ public function getLoadedUrls(): array
+ {
+ return $this->loadedUrls;
+ }
+}
diff --git a/vendor/scssphp/scssphp/src/Evaluation/EvaluateVisitor.php b/vendor/scssphp/scssphp/src/Evaluation/EvaluateVisitor.php
new file mode 100644
index 000000000..9ee17d9a7
--- /dev/null
+++ b/vendor/scssphp/scssphp/src/Evaluation/EvaluateVisitor.php
@@ -0,0 +1,3529 @@
+<?php
+
+/**
+ * SCSSPHP
+ *
+ * @copyright 2012-2020 Leaf Corcoran
+ *
+ * @license http://opensource.org/licenses/MIT MIT
+ *
+ * @link http://scssphp.github.io/scssphp
+ */
+
+namespace ScssPhp\ScssPhp\Evaluation;
+
+use League\Uri\Uri;
+use ScssPhp\ScssPhp\Ast\AstNode;
+use ScssPhp\ScssPhp\Ast\Css\CssAtRule;
+use ScssPhp\ScssPhp\Ast\Css\CssComment;
+use ScssPhp\ScssPhp\Ast\Css\CssKeyframeBlock;
+use ScssPhp\ScssPhp\Ast\Css\CssMediaQuery;
+use ScssPhp\ScssPhp\Ast\Css\CssMediaRule;
+use ScssPhp\ScssPhp\Ast\Css\CssNode;
+use ScssPhp\ScssPhp\Ast\Css\CssStyleRule;
+use ScssPhp\ScssPhp\Ast\Css\CssStylesheet;
+use ScssPhp\ScssPhp\Ast\Css\CssValue;
+use ScssPhp\ScssPhp\Ast\Css\MediaQuerySingletonMergeResult;
+use ScssPhp\ScssPhp\Ast\Css\ModifiableCssAtRule;
+use ScssPhp\ScssPhp\Ast\Css\ModifiableCssComment;
+use ScssPhp\ScssPhp\Ast\Css\ModifiableCssDeclaration;
+use ScssPhp\ScssPhp\Ast\Css\ModifiableCssImport;
+use ScssPhp\ScssPhp\Ast\Css\ModifiableCssKeyframeBlock;
+use ScssPhp\ScssPhp\Ast\Css\ModifiableCssMediaRule;
+use ScssPhp\ScssPhp\Ast\Css\ModifiableCssNode;
+use ScssPhp\ScssPhp\Ast\Css\ModifiableCssParentNode;
+use ScssPhp\ScssPhp\Ast\Css\ModifiableCssStyleRule;
+use ScssPhp\ScssPhp\Ast\Css\ModifiableCssStylesheet;
+use ScssPhp\ScssPhp\Ast\Css\ModifiableCssSupportsRule;
+use ScssPhp\ScssPhp\Ast\FakeAstNode;
+use ScssPhp\ScssPhp\Ast\Sass\ArgumentDeclaration;
+use ScssPhp\ScssPhp\Ast\Sass\ArgumentInvocation;
+use ScssPhp\ScssPhp\Ast\Sass\AtRootQuery;
+use ScssPhp\ScssPhp\Ast\Sass\CallableInvocation;
+use ScssPhp\ScssPhp\Ast\Sass\Expression;
+use ScssPhp\ScssPhp\Ast\Sass\Expression\BinaryOperationExpression;
+use ScssPhp\ScssPhp\Ast\Sass\Expression\BinaryOperator;
+use ScssPhp\ScssPhp\Ast\Sass\Expression\BooleanExpression;
+use ScssPhp\ScssPhp\Ast\Sass\Expression\ColorExpression;
+use ScssPhp\ScssPhp\Ast\Sass\Expression\FunctionExpression;
+use ScssPhp\ScssPhp\Ast\Sass\Expression\IfExpression;
+use ScssPhp\ScssPhp\Ast\Sass\Expression\InterpolatedFunctionExpression;
+use ScssPhp\ScssPhp\Ast\Sass\Expression\IsCalculationSafeVisitor;
+use ScssPhp\ScssPhp\Ast\Sass\Expression\ListExpression;
+use ScssPhp\ScssPhp\Ast\Sass\Expression\MapExpression;
+use ScssPhp\ScssPhp\Ast\Sass\Expression\NullExpression;
+use ScssPhp\ScssPhp\Ast\Sass\Expression\NumberExpression;
+use ScssPhp\ScssPhp\Ast\Sass\Expression\ParenthesizedExpression;
+use ScssPhp\ScssPhp\Ast\Sass\Expression\SelectorExpression;
+use ScssPhp\ScssPhp\Ast\Sass\Expression\StringExpression;
+use ScssPhp\ScssPhp\Ast\Sass\Expression\SupportsExpression;
+use ScssPhp\ScssPhp\Ast\Sass\Expression\UnaryOperationExpression;
+use ScssPhp\ScssPhp\Ast\Sass\Expression\UnaryOperator;
+use ScssPhp\ScssPhp\Ast\Sass\Expression\ValueExpression;
+use ScssPhp\ScssPhp\Ast\Sass\Expression\VariableExpression;
+use ScssPhp\ScssPhp\Ast\Sass\Import\DynamicImport;
+use ScssPhp\ScssPhp\Ast\Sass\Import\StaticImport;
+use ScssPhp\ScssPhp\Ast\Sass\Interpolation;
+use ScssPhp\ScssPhp\Ast\Sass\Statement;
+use ScssPhp\ScssPhp\Ast\Sass\Statement\AtRootRule;
+use ScssPhp\ScssPhp\Ast\Sass\Statement\AtRule;
+use ScssPhp\ScssPhp\Ast\Sass\Statement\ContentBlock;
+use ScssPhp\ScssPhp\Ast\Sass\Statement\ContentRule;
+use ScssPhp\ScssPhp\Ast\Sass\Statement\DebugRule;
+use ScssPhp\ScssPhp\Ast\Sass\Statement\Declaration;
+use ScssPhp\ScssPhp\Ast\Sass\Statement\EachRule;
+use ScssPhp\ScssPhp\Ast\Sass\Statement\ErrorRule;
+use ScssPhp\ScssPhp\Ast\Sass\Statement\ExtendRule;
+use ScssPhp\ScssPhp\Ast\Sass\Statement\ForRule;
+use ScssPhp\ScssPhp\Ast\Sass\Statement\FunctionRule;
+use ScssPhp\ScssPhp\Ast\Sass\Statement\IfRule;
+use ScssPhp\ScssPhp\Ast\Sass\Statement\ImportRule;
+use ScssPhp\ScssPhp\Ast\Sass\Statement\IncludeRule;
+use ScssPhp\ScssPhp\Ast\Sass\Statement\LoudComment;
+use ScssPhp\ScssPhp\Ast\Sass\Statement\MediaRule;
+use ScssPhp\ScssPhp\Ast\Sass\Statement\MixinRule;
+use ScssPhp\ScssPhp\Ast\Sass\Statement\ReturnRule;
+use ScssPhp\ScssPhp\Ast\Sass\Statement\SilentComment;
+use ScssPhp\ScssPhp\Ast\Sass\Statement\StyleRule;
+use ScssPhp\ScssPhp\Ast\Sass\Statement\Stylesheet;
+use ScssPhp\ScssPhp\Ast\Sass\Statement\SupportsRule;
+use ScssPhp\ScssPhp\Ast\Sass\Statement\VariableDeclaration;
+use ScssPhp\ScssPhp\Ast\Sass\Statement\WarnRule;
+use ScssPhp\ScssPhp\Ast\Sass\Statement\WhileRule;
+use ScssPhp\ScssPhp\Ast\Sass\SupportsCondition;
+use ScssPhp\ScssPhp\Ast\Sass\SupportsCondition\SupportsAnything;
+use ScssPhp\ScssPhp\Ast\Sass\SupportsCondition\SupportsDeclaration;
+use ScssPhp\ScssPhp\Ast\Sass\SupportsCondition\SupportsFunction;
+use ScssPhp\ScssPhp\Ast\Sass\SupportsCondition\SupportsInterpolation;
+use ScssPhp\ScssPhp\Ast\Sass\SupportsCondition\SupportsNegation;
+use ScssPhp\ScssPhp\Ast\Sass\SupportsCondition\SupportsOperation;
+use ScssPhp\ScssPhp\Ast\Selector\SelectorList;
+use ScssPhp\ScssPhp\Ast\Selector\SimpleSelector;
+use ScssPhp\ScssPhp\Collection\Map;
+use ScssPhp\ScssPhp\Colors;
+use ScssPhp\ScssPhp\Deprecation;
+use ScssPhp\ScssPhp\Exception\MultiSpanSassRuntimeException;
+use ScssPhp\ScssPhp\Exception\SassException;
+use ScssPhp\ScssPhp\Exception\SassRuntimeException;
+use ScssPhp\ScssPhp\Exception\SassScriptException;
+use ScssPhp\ScssPhp\Exception\SimpleSassException;
+use ScssPhp\ScssPhp\Exception\SimpleSassFormatException;
+use ScssPhp\ScssPhp\Exception\SimpleSassRuntimeException;
+use ScssPhp\ScssPhp\Extend\ConcreteExtensionStore;
+use ScssPhp\ScssPhp\Extend\Extension;
+use ScssPhp\ScssPhp\Extend\ExtensionStore;
+use ScssPhp\ScssPhp\Function\FunctionRegistry;
+use ScssPhp\ScssPhp\Importer\ImportCache;
+use ScssPhp\ScssPhp\Importer\Importer;
+use ScssPhp\ScssPhp\Logger\LoggerInterface;
+use ScssPhp\ScssPhp\Parser\InterpolationMap;
+use ScssPhp\ScssPhp\Parser\KeyframeSelectorParser;
+use ScssPhp\ScssPhp\SassCallable\BuiltInCallable;
+use ScssPhp\ScssPhp\SassCallable\PlainCssCallable;
+use ScssPhp\ScssPhp\SassCallable\SassCallable;
+use ScssPhp\ScssPhp\SassCallable\UserDefinedCallable;
+use ScssPhp\ScssPhp\SourceSpan\MultiSpan;
+use ScssPhp\ScssPhp\StackTrace\Frame;
+use ScssPhp\ScssPhp\StackTrace\Trace;
+use ScssPhp\ScssPhp\Util;
+use ScssPhp\ScssPhp\Util\AstUtil;
+use ScssPhp\ScssPhp\Util\Character;
+use ScssPhp\ScssPhp\Util\EquatableUtil;
+use ScssPhp\ScssPhp\Util\IterableUtil;
+use ScssPhp\ScssPhp\Util\ListUtil;
+use ScssPhp\ScssPhp\Util\LoggerUtil;
+use ScssPhp\ScssPhp\Util\SpanUtil;
+use ScssPhp\ScssPhp\Util\StringUtil;
+use ScssPhp\ScssPhp\Value\CalculationOperation;
+use ScssPhp\ScssPhp\Value\CalculationOperator;
+use ScssPhp\ScssPhp\Value\ListSeparator;
+use ScssPhp\ScssPhp\Value\SassArgumentList;
+use ScssPhp\ScssPhp\Value\SassBoolean;
+use ScssPhp\ScssPhp\Value\SassCalculation;
+use ScssPhp\ScssPhp\Value\SassColor;
+use ScssPhp\ScssPhp\Value\SassFunction;
+use ScssPhp\ScssPhp\Value\SassList;
+use ScssPhp\ScssPhp\Value\SassMap;
+use ScssPhp\ScssPhp\Value\SassMixin;
+use ScssPhp\ScssPhp\Value\SassNull;
+use ScssPhp\ScssPhp\Value\SassNumber;
+use ScssPhp\ScssPhp\Value\SassString;
+use ScssPhp\ScssPhp\Value\Value;
+use ScssPhp\ScssPhp\Visitor\ExpressionVisitor;
+use ScssPhp\ScssPhp\Visitor\StatementVisitor;
+use ScssPhp\ScssPhp\Warn;
+use SourceSpan\FileSpan;
+use SourceSpan\SourceFile;
+use SourceSpan\SimpleSourceLocation;
+
+/**
+ * A visitor that executes Sass code to produce a CSS tree.
+ *
+ * @template-implements StatementVisitor<Value|null>
+ * @template-implements ExpressionVisitor<Value>
+ *
+ * @internal
+ */
+class EvaluateVisitor implements StatementVisitor, ExpressionVisitor
+{
+ /**
+ * The import cache used to import other stylesheets.
+ */
+ private readonly ImportCache $importCache;
+
+ /**
+ * @var array<string, SassCallable>
+ */
+ private array $builtInFunctions = [];
+
+ private readonly LoggerInterface $logger;
+
+ /**
+ * A set of message/location pairs for warnings that have been emitted via
+ * {@see warn}.
+ *
+ * We only want to emit one warning per location, to avoid blowing up users'
+ * consoles with redundant warnings.
+ *
+ * @var array<string, array<string, true>>
+ */
+ private array $warningsEmitted = [];
+
+ /**
+ * Whether to avoid emitting warnings for files loaded from dependencies.
+ */
+ private readonly bool $quietDeps;
+
+ /**
+ * Whether to track source map information.
+ */
+ private readonly bool $sourceMap;
+
+ /**
+ * The current lexical environment.
+ */
+ private Environment $environment;
+
+ /**
+ * The style rule that defines the current parent selector, if any.
+ *
+ * This doesn't take into consideration any intermediate `@at-root` rules. In
+ * the common case where those rules are relevant, use {@see getStyleRule} instead.
+ */
+ private ?ModifiableCssStyleRule $styleRuleIgnoringAtRoot = null;
+
+ /**
+ * The current media queries, if any.
+ *
+ * @var list<CssMediaQuery>|null
+ */
+ private ?array $mediaQueries = null;
+
+ /**
+ * The set of media queries that were merged together to create
+ * {@see $mediaQueries}.
+ *
+ * This will be non-null if and only if {@see $mediaQueries} is non-null, but it
+ * will be empty if {@see $mediaQueries} isn't the result of a merge.
+ *
+ * @var CssMediaQuery[]|null
+ */
+ private ?array $mediaQuerySources = null;
+
+ private ?ModifiableCssParentNode $parent = null;
+
+ /**
+ * The name of the current declaration parent.
+ */
+ private ?string $declarationName = null;
+
+ /**
+ * The human-readable name of the current stack frame.
+ */
+ private string $member = "root stylesheet";
+
+ /**
+ * The innermost user-defined callable that's being invoked.
+ */
+ private ?UserDefinedCallable $currentCallable = null;
+
+ /**
+ * The node for the innermost callable that's being invoked.
+ *
+ * This is used to produce warnings for function calls. It's stored as an
+ * {@see AstNode} rather than a {@see FileSpan} so we can avoid calling {@see AstNode::getSpan}
+ * if the span isn't required, since some nodes need to do real work to
+ * manufacture a source span.
+ */
+ private ?AstNode $callableNode = null;
+
+ /**
+ * The span for the current import that's being resolved.
+ *
+ * This is used to produce warnings for importers.
+ */
+ private ?FileSpan $importSpan = null;
+
+ /**
+ * Whether we're currently executing a function.
+ */
+ private bool $inFunction = false;
+
+ /**
+ * Whether we're currently building the output of an unknown at rule.
+ */
+ private bool $inUnknownAtRule = false;
+
+ /**
+ * Whether we're directly within an `@at-root` rule that excludes style rules.
+ */
+ private bool $atRootExcludingStyleRule = false;
+
+ /**
+ * Whether we're currently building the output of a `@keyframes` rule.
+ */
+ private bool $inKeyFrames = false;
+
+ /**
+ * Whether we're currently evaluating a {@see SupportsDeclaration}.
+ *
+ * When this is true, calculations will not be simplified.
+ */
+ private bool $inSupportsDeclaration = false;
+
+ /**
+ * The canonical URLs of all stylesheets loaded during compilation.
+ *
+ * @var array<string, true>
+ */
+ private array $loadedUrls = [];
+
+ /**
+ * A map from canonical URLs for modules (or imported files) that are
+ * currently being evaluated to AST nodes whose spans indicate the original
+ * loads for those modules.
+ *
+ * Map values may be `null`, which indicates an active module that doesn't
+ * have a source span associated with its original load (such as the
+ * entrypoint module).
+ *
+ * This is used to ensure that we don't get into an infinite load loop.
+ *
+ * @var array<string, AstNode|null>
+ */
+ private array $activeModules = [];
+
+ /**
+ * The dynamic call stack representing function invocations, mixin
+ * invocations, and imports surrounding the current context.
+ *
+ * Each member is a tuple of the span where the stack trace starts and the
+ * name of the member being invoked.
+ *
+ * This stores {@see AstNode}s rather than {@see FileSpan}s so it can avoid calling
+ * {@see AstNode::getSpan} if the span isn't required, since some nodes need to do
+ * real work to manufacture a source span.
+ *
+ * @var list<array{string, AstNode}>
+ */
+ private array $stack = [];
+
+ /**
+ * The importer that's currently being used to resolve relative imports.
+ *
+ * If this is `null`, relative imports aren't supported in the current
+ * stylesheet.
+ */
+ private ?Importer $importer = null;
+
+ /**
+ * Whether we're in a dependency.
+ *
+ * A dependency is defined as a stylesheet imported by an importer other than
+ * the original.
+ */
+ private bool $inDependency = false;
+
+ private ?Stylesheet $stylesheet = null;
+
+ private ?ModifiableCssStylesheet $root = null;
+
+ private ?int $endOfImports = null;
+
+ /**
+ * Plain-CSS imports that didn't appear in the initial block of CSS imports.
+ *
+ * These are added to the initial CSS import block by {@see visitStylesheet} after
+ * the stylesheet has been fully performed.
+ *
+ * This is `null` unless there are any out-of-order imports in the current
+ * stylesheet.
+ *
+ * @var list<ModifiableCssImport>|null
+ */
+ private ?array $outOfOrderImports = null;
+
+ private ?ExtensionStore $extensionStore = null;
+
+ /**
+ * @param SassCallable[] $functions
+ */
+ public function __construct(ImportCache $importCache, array $functions, LoggerInterface $logger, bool $quietDeps = false, bool $sourceMap = false)
+ {
+ $this->importCache = $importCache;
+ $this->logger = $logger;
+ $this->quietDeps = $quietDeps;
+ $this->sourceMap = $sourceMap;
+ $this->environment = Environment::create();
+
+ $sassMetaUri = Uri::new('sass:meta');
+ // These functions are defined in the context of the evaluator because
+ // they need access to the environment or other local state.
+ // When adding a new function here, its name must also be added in {@see FunctionRegistry::SPECIAL_META_GLOBAL_FUNCTIONS}.
+ $metaFunctions = [
+ BuiltInCallable::function('global-variable-exists', '$name, $module: null', function ($arguments) {
+ $variable = $arguments[0]->assertString('name');
+ $module = $arguments[1]->realNull()?->assertString('module');
+
+ if ($module !== null) {
+ // TODO remove this when implementing modules
+ throw new SassScriptException('Sass modules are not implemented yet.');
+ }
+
+ return SassBoolean::create($this->environment->globalVariableExists(str_replace('_', '-', $variable->getText())));
+ }, $sassMetaUri),
+ BuiltInCallable::function('variable-exists', '$name', function ($arguments) {
+ $variable = $arguments[0]->assertString('name');
+
+ return SassBoolean::create($this->environment->variableExists(str_replace('_', '-', $variable->getText())));
+ }, $sassMetaUri),
+ BuiltInCallable::function('function-exists', '$name, $module: null', function ($arguments) {
+ $variable = $arguments[0]->assertString('name');
+ $module = $arguments[1]->realNull()?->assertString('module');
+
+ if ($module !== null) {
+ // TODO remove this when implementing modules
+ throw new SassScriptException('Sass modules are not implemented yet.');
+ }
+
+ return SassBoolean::create($this->environment->functionExists(str_replace('_', '-', $variable->getText())) || isset($this->builtInFunctions[$variable->getText()]) || FunctionRegistry::has($variable->getText()));
+ }, $sassMetaUri),
+ BuiltInCallable::function('mixin-exists', '$name, $module: null', function ($arguments) {
+ $variable = $arguments[0]->assertString('name');
+ $module = $arguments[1]->realNull()?->assertString('module');
+
+ if ($module !== null) {
+ // TODO remove this when implementing modules
+ throw new SassScriptException('Sass modules are not implemented yet.');
+ }
+
+ return SassBoolean::create($this->environment->mixinExists(str_replace('_', '-', $variable->getText())));
+ }, $sassMetaUri),
+ BuiltInCallable::function('content-exists', '', function ($arguments) {
+ if (! $this->environment->isInMixin()) {
+ throw new SassScriptException('content-exists() may only be called within a mixin.');
+ }
+
+ return SassBoolean::create($this->environment->getContent() !== null);
+ }, $sassMetaUri),
+ BuiltInCallable::function('get-function', '$name, $css: false, $module: null', function ($arguments) {
+ $name = $arguments[0]->assertString('name');
+ $css = $arguments[1]->isTruthy();
+ $module = $arguments[2]->realNull()?->assertString('module');
+
+ if ($css) {
+ if ($module !== null) {
+ throw new SassScriptException('$css and $module may not both be passed at once.');
+ }
+
+ return new SassFunction(new PlainCssCallable($name->getText()));
+ }
+
+ \assert($this->callableNode !== null);
+ $callable = $this->addExceptionSpan($this->callableNode, function () use ($name, $module) {
+ $normalizedName = str_replace('_', '-', $name->getText());
+ $namespace = $module?->getText();
+
+ if ($namespace !== null) {
+ // TODO remove this when implementing modules
+ throw new SassScriptException('Sass modules are not implemented yet.');
+ }
+
+ $local = $this->environment->getFunction($normalizedName);
+
+ if ($local !== null) {
+ return $local;
+ }
+
+ return $this->getBuiltinFunction($normalizedName);
+ });
+
+ if ($callable === null) {
+ throw new SassScriptException("Function not found: $name");
+ }
+
+ return new SassFunction($callable);
+ }, $sassMetaUri),
+ BuiltInCallable::function('get-mixin', '$name, $module: null', function ($arguments) {
+ $name = $arguments[0]->assertString('name');
+ $module = $arguments[1]->realNull()?->assertString('module');
+
+ \assert($this->callableNode !== null);
+ $callable = $this->addExceptionSpan($this->callableNode, function () use ($name, $module) {
+ if ($module !== null) {
+ // TODO remove this when implementing modules
+ throw new SassScriptException('Sass modules are not implemented yet.');
+ }
+
+ return $this->environment->getMixin(str_replace('_', '-', $name->getText()));
+ });
+
+ if ($callable === null) {
+ throw new SassScriptException("Mixin not found: $name");
+ }
+
+ return new SassMixin($callable);
+ }, $sassMetaUri),
+ BuiltInCallable::function('call', '$function, $args...', function ($arguments) {
+ $function = $arguments[0];
+ $args = $arguments[1];
+ \assert($args instanceof SassArgumentList);
+
+ $callableNode = $this->callableNode;
+ \assert($callableNode !== null);
+
+ if (\count($args->getKeywords()) === 0) {
+ $keywordRest = null;
+ } else {
+ $keywordArgs = new Map();
+ foreach ($args->getKeywords() as $name => $value) {
+ $keywordArgs->put(new SassString($name, false), $value);
+ }
+
+ $keywordRest = new ValueExpression(SassMap::create($keywordArgs), $callableNode->getSpan());
+ }
+
+ $invocation = new ArgumentInvocation([], [], $callableNode->getSpan(), new ValueExpression($args, $callableNode->getSpan()), $keywordRest);
+
+ if ($function instanceof SassString) {
+ Warn::forDeprecation("Passing a string to call() is deprecated and will be illegal in Dart Sass 2.0.0.\n\nRecommendation: call(get-function($function))", Deprecation::callString);
+ $expression = new FunctionExpression($function->getText(), $invocation, $callableNode->getSpan());
+
+ return $expression->accept($this);
+ }
+
+ $callable = $function->assertFunction('function')->getCallable();
+
+ return $this->runFunctionCallable($invocation, $callable, $callableNode);
+ }, $sassMetaUri),
+ ];
+
+ foreach ($functions as $function) {
+ $this->builtInFunctions[str_replace('_', '-', $function->getName())] = $function;
+ }
+ foreach ($metaFunctions as $function) {
+ $this->builtInFunctions[$function->getName()] = $function;
+ }
+ }
+
+ public function getCallableNode(): ?AstNode
+ {
+ return $this->callableNode;
+ }
+
+ public function getImportSpan(): ?FileSpan
+ {
+ return $this->importSpan;
+ }
+
+ /**
+ * The current parent node in the output CSS tree.
+ */
+ private function getParent(): ModifiableCssParentNode
+ {
+ if ($this->parent === null) {
+ throw new \LogicException('Cannot access "getParent" outside of a module.');
+ }
+
+ return $this->parent;
+ }
+
+ private function getStyleRule(): ?ModifiableCssStyleRule
+ {
+ return $this->atRootExcludingStyleRule ? null : $this->styleRuleIgnoringAtRoot;
+ }
+
+ /**
+ * The stylesheet that's currently being evaluated.
+ */
+ private function getStylesheet(): Stylesheet
+ {
+ if ($this->stylesheet === null) {
+ throw new \LogicException('Cannot access "getStylesheet" outside of a module.');
+ }
+
+ return $this->stylesheet;
+ }
+
+ /**
+ * The root stylesheet node.
+ */
+ private function getRoot(): ModifiableCssStylesheet
+ {
+ if ($this->root === null) {
+ throw new \LogicException('Cannot access "getRoot" outside of a module.');
+ }
+
+ return $this->root;
+ }
+
+ /**
+ * The first index in `$this->getRoot()->getChildren()` after the initial block of CSS imports.
+ */
+ private function getEndOfImports(): int
+ {
+ if ($this->endOfImports === null) {
+ throw new \LogicException('Cannot access "getEndOfImports" outside of a module.');
+ }
+
+ return $this->endOfImports;
+ }
+
+ /**
+ * The extension store that tracks extensions and style rules for the current
+ * module.
+ */
+ private function getExtensionStore(): ExtensionStore
+ {
+ if ($this->extensionStore === null) {
+ throw new \LogicException('Cannot access "getExtensionStore" outside of a module.');
+ }
+
+ return $this->extensionStore;
+ }
+
+ /**
+ * @param array<string, Value> $initialVariables
+ */
+ public function run(?Importer $importer, Stylesheet $node, array $initialVariables = []): EvaluateResult
+ {
+ return EvaluationContext::withEvaluationContext(new VisitorEvaluationContext($this, $node), function () use ($importer, $node, $initialVariables) {
+ $url = $node->getSpan()->getSourceUrl();
+
+ if ($url !== null) {
+ $urlString = (string) $url;
+ $this->activeModules[$urlString] = null;
+ // TODO check how to handle stdin
+ $this->loadedUrls[$urlString] = true;
+ }
+
+ /** @var ExtensionStore $extensionStore */
+ [$css, $extensionStore] = $this->addExceptionTrace(fn() => $this->execute($importer, $node, $initialVariables));
+ $selectors = $extensionStore->getSimpleSelectors();
+ $unsatisfiedExtension = IterableUtil::firstOrNull($extensionStore->extensionsWhereTarget(fn (SimpleSelector $target) => !EquatableUtil::iterableContains($selectors, $target)));
+ if ($unsatisfiedExtension !== null) {
+ $this->throwForUnsatisfiedExtension($unsatisfiedExtension);
+ }
+
+ return new EvaluateResult($css, array_keys($this->loadedUrls));
+ });
+ }
+
+ /**
+ * @param array<string, Value> $initialVariables
+ *
+ * @return array{CssStylesheet, ExtensionStore}
+ */
+ private function execute(?Importer $importer, Stylesheet $stylesheet, array $initialVariables = []): array
+ {
+ $environment = Environment::create();
+ foreach ($initialVariables as $variableName => $initialVariable) {
+ $environment->setVariable($variableName, $initialVariable, new FakeAstNode(fn () => SourceFile::fromString('')->span(0)));
+ }
+
+ $css = null;
+
+ $extensionStore = ConcreteExtensionStore::create();
+
+ $this->withEnvironment($environment, function () use ($importer, $stylesheet, $extensionStore, &$css) {
+ $oldImporter = $this->importer;
+ $oldStylesheet = $this->stylesheet;
+ $oldRoot = $this->root;
+ $oldParent = $this->parent;
+ $oldEndOfImports = $this->endOfImports;
+ $oldOutOfOrderImports = $this->outOfOrderImports;
+ $oldExtensionStore = $this->extensionStore;
+ $oldStyleRule = $this->getStyleRule();
+ $oldMediaQueries = $this->mediaQueries;
+ $oldDeclarationName = $this->declarationName;
+ $oldInUnknownAtRule = $this->inUnknownAtRule;
+ $oldAtRootExcludingStyleRule = $this->atRootExcludingStyleRule;
+ $oldInKeyframes = $this->inKeyFrames;
+
+ $this->importer = $importer;
+ $this->stylesheet = $stylesheet;
+ $this->root = $root = new ModifiableCssStylesheet($stylesheet->getSpan());
+ $this->parent = $root;
+ $this->endOfImports = 0;
+ $this->outOfOrderImports = null;
+ $this->extensionStore = $extensionStore;
+ $this->styleRuleIgnoringAtRoot = null;
+ $this->mediaQueries = null;
+ $this->declarationName = null;
+ $this->inUnknownAtRule = false;
+ $this->atRootExcludingStyleRule = false;
+ $this->inKeyFrames = false;
+
+ $this->visitStylesheet($stylesheet);
+ $css = $this->outOfOrderImports === null ? $root : new ModifiableCssStylesheet($stylesheet->getSpan(), $this->addOutOfOrderImports());
+
+ $this->importer = $oldImporter;
+ $this->stylesheet = $oldStylesheet;
+ $this->root = $oldRoot;
+ $this->parent = $oldParent;
+ $this->endOfImports = $oldEndOfImports;
+ $this->outOfOrderImports = $oldOutOfOrderImports;
+ $this->extensionStore = $oldExtensionStore;
+ $this->styleRuleIgnoringAtRoot = $oldStyleRule;
+ $this->mediaQueries = $oldMediaQueries;
+ $this->declarationName = $oldDeclarationName;
+ $this->inUnknownAtRule = $oldInUnknownAtRule;
+ $this->atRootExcludingStyleRule = $oldAtRootExcludingStyleRule;
+ $this->inKeyFrames = $oldInKeyframes;
+ });
+
+ assert($css instanceof CssStylesheet);
+
+ return [$css, $extensionStore];
+ }
+
+ /**
+ * Returns a copy of `$this->getRoot()->getChildren` with {@see outOfOrderImports} inserted
+ * after {@see endOfImports}, if necessary.
+ *
+ * @return list<ModifiableCssNode>
+ */
+ private function addOutOfOrderImports(): array
+ {
+ if ($this->outOfOrderImports === null) {
+ return $this->getRoot()->getChildren();
+ }
+
+ $children = $this->getRoot()->getChildren();
+
+ array_splice($children, $this->getEndOfImports(), 0, $this->outOfOrderImports);
+
+ return array_values($children);
+ }
+
+ /**
+ * Throws an exception indicating that $extension is unsatisfied.
+ */
+ private function throwForUnsatisfiedExtension(Extension $extension): never
+ {
+ throw new SimpleSassException(
+ "The target selector was not found.\nUse \"@extend $extension->target !optional\" to avoid this error.",
+ $extension->span,
+ );
+ }
+
+ /**
+ * @phpstan-impure
+ */
+ public function visitStylesheet(Stylesheet $node): ?Value
+ {
+ foreach ($node->getChildren() as $child) {
+ $child->accept($this);
+ }
+
+ return null;
+ }
+
+ public function visitAtRootRule(AtRootRule $node): ?Value
+ {
+ $unparsedQuery = $node->getQuery();
+
+ if ($unparsedQuery !== null) {
+ [$resolved, $map] = $this->performInterpolationWithMap($unparsedQuery, true);
+ $query = AtRootQuery::parse($resolved, $this->logger, null, $map);
+ } else {
+ $query = AtRootQuery::getDefault();
+ }
+
+ $parent = $this->getParent();
+ /** @var ModifiableCssParentNode[] $included */
+ $included = [];
+
+ while (!$parent instanceof CssStylesheet) {
+ if (!$query->excludes($parent)) {
+ $included[] = $parent;
+ }
+
+ $grandParent = $parent->getParent();
+ if ($grandParent === null) {
+ throw new \LogicException('CssNodes must have a CssStylesheet transitive parent node.');
+ }
+
+ $parent = $grandParent;
+ }
+
+ $root = $this->trimIncluded($included);
+
+ // If we didn't exclude any rules, we don't need to use the copies we might
+ // have created.
+ if ($root === $this->getParent()) {
+ $this->environment->scope(function () use ($node) {
+ foreach ($node->getChildren() as $child) {
+ $child->accept($this);
+ }
+ }, $node->hasDeclarations());
+
+ return null;
+ }
+
+ $innerCopy = $root;
+ if (!empty($included)) {
+ $innerCopy = $included[0]->copyWithoutChildren();
+ $outerCopy = $innerCopy;
+
+ foreach (array_slice($included, 1) as $includedNode) {
+ $copy = $includedNode->copyWithoutChildren();
+ $copy->addChild($outerCopy);
+ $outerCopy = $copy;
+ }
+
+ $root->addChild($outerCopy);
+ }
+
+ $scope = $this->scopeForAtRoot($node, $innerCopy, $query, $included);
+ $scope(function () use ($node) {
+ foreach ($node->getChildren() as $child) {
+ $child->accept($this);
+ }
+ });
+
+ return null;
+ }
+
+ /**
+ * Returns a scope callback for $query.
+ *
+ * This returns a callback that adjusts various instance variables for its
+ * duration, based on which rules are excluded by $query. It always assigns
+ * {@see parent} to $newParent.
+ *
+ * @param ModifiableCssParentNode[] $included
+ *
+ * @return callable((callable(): void)): void
+ */
+ private function scopeForAtRoot(AtRootRule $node, ModifiableCssParentNode $newParent, AtRootQuery $query, array $included): callable
+ {
+ $scope = function (callable $callback) use ($newParent, $node) {
+ // We can't use *rent here because it'll add the node to the tree
+ // in the wrong place.
+ $oldParent = $this->parent;
+ $this->parent = $newParent;
+ $this->environment->scope($callback, $node->hasDeclarations());
+ $this->parent = $oldParent;
+ };
+
+ if ($query->excludesStyleRules()) {
+ $innerScope = $scope;
+ $scope = function (callable $callback) use ($innerScope) {
+ $oldAtRootExcludingStyleRule = $this->atRootExcludingStyleRule;
+ $this->atRootExcludingStyleRule = true;
+ $innerScope($callback);
+ $this->atRootExcludingStyleRule = $oldAtRootExcludingStyleRule;
+ };
+ }
+
+ if ($this->mediaQueries !== null && $query->excludesName('media')) {
+ $innerScope = $scope;
+ $scope = function (callable $callback) use ($innerScope) {
+ $this->withMediaQueries(null, null, function () use ($innerScope, $callback) {
+ $innerScope($callback);
+ });
+ };
+ }
+
+ if ($this->inKeyFrames && $query->excludesName('keyframes')) {
+ $innerScope = $scope;
+ $scope = function (callable $callback) use ($innerScope) {
+ $wasInKeyframes = $this->inKeyFrames;
+ $this->inKeyFrames = false;
+ $innerScope($callback);
+ $this->inKeyFrames = $wasInKeyframes;
+ };
+ }
+
+ if ($this->inUnknownAtRule && !IterableUtil::any($included, fn($parent) => $parent instanceof CssAtRule)) {
+ $innerScope = $scope;
+ $scope = function (callable $callback) use ($innerScope) {
+ $wasInUnknownAtRule = $this->inUnknownAtRule;
+ $this->inUnknownAtRule = false;
+ $innerScope($callback);
+ $this->inUnknownAtRule = $wasInUnknownAtRule;
+ };
+ }
+
+ return $scope;
+ }
+
+ /**
+ * Destructively trims a trailing sublist from $nodes that matches the
+ * current list of parents.
+ *
+ * $nodes should be a list of parents included by an `@at-root` rule, from
+ * innermost to outermost. If it contains a trailing sublist that's
+ * contiguous—meaning that each node is a direct parent of the node before
+ * it—and whose final node is a direct child of {@see getRoot}, this removes that
+ * sublist and returns the innermost removed parent.
+ *
+ * Otherwise, this leaves $nodes as-is and returns {@see getRoot}.
+ *
+ * @param ModifiableCssParentNode[] $nodes
+ */
+ private function trimIncluded(array &$nodes): ModifiableCssParentNode
+ {
+ if (empty($nodes)) {
+ return $this->getRoot();
+ }
+
+ $parent = $this->getParent();
+ $innermostContiguous = null;
+
+ foreach ($nodes as $i => $node) {
+ while ($parent !== $node) {
+ $innermostContiguous = null;
+
+ $grandParent = $parent->getParent();
+ if ($grandParent === null) {
+ throw new \LogicException('Expected the node to be an ancestor.');
+ }
+
+ $parent = $grandParent;
+ }
+
+ $innermostContiguous = $innermostContiguous ?? $i;
+
+ $grandParent = $parent->getParent();
+ if ($grandParent === null) {
+ throw new \LogicException('Expected the node to be an ancestor.');
+ }
+
+ $parent = $grandParent;
+ }
+
+ if ($parent !== $this->getRoot()) {
+ return $this->getRoot();
+ }
+
+ $root = $nodes[$innermostContiguous];
+ array_splice($nodes, $innermostContiguous);
+
+ return $root;
+ }
+
+ public function visitContentBlock(ContentBlock $node): ?Value
+ {
+ throw new \BadMethodCallException('Evaluation handles @include and its content block together.');
+ }
+
+ public function visitContentRule(ContentRule $node): ?Value
+ {
+ $content = $this->environment->getContent();
+
+ if ($content === null) {
+ return null;
+ }
+
+ $this->runUserDefinedCallable($node->getArguments(), $content, $node, function () use ($content) {
+ foreach ($content->getDeclaration()->getChildren() as $statement) {
+ $statement->accept($this);
+ }
+
+ return null;
+ });
+
+ return null;
+ }
+
+ public function visitDebugRule(DebugRule $node): ?Value
+ {
+ $value = $node->getExpression()->accept($this);
+ $this->logger->debug($value instanceof SassString ? $value->getText() : (string) $value, $node->getSpan());
+
+ return null;
+ }
+
+ public function visitDeclaration(Declaration $node): ?Value
+ {
+ if ($this->getStyleRule() === null && !$this->inUnknownAtRule && !$this->inKeyFrames) {
+ throw $this->exception('Declarations may only be used within style rules.', $node->getSpan());
+ }
+
+ if ($this->declarationName !== null && $node->isCustomProperty()) {
+ throw $this->exception('Declarations whose names begin with "--" may not be nested.', $node->getSpan());
+ }
+
+ \assert($this->getParent()->getParent() !== null);
+ $siblings = $this->getParent()->getParent()->getChildren();
+ $interleavedRules = [];
+
+ if (
+ ListUtil::last($siblings) !== $this->getParent()
+ // Reproduce this condition from {@see warn} so that we don't add anything to
+ // $interleavedRules for declarations in dependencies.
+ && !($this->quietDeps && ($this->inDependency || ($this->currentCallable?->isInDependency() ?? false)))
+ ) {
+ $parentOffset = array_search($this->getParent(), $siblings, true);
+ if ($parentOffset === false) {
+ $parentOffset = -1;
+ }
+ foreach (array_slice($siblings, $parentOffset + 1) as $sibling) {
+ if ($sibling instanceof CssComment) {
+ continue;
+ }
+
+ if ($sibling instanceof CssStyleRule) {
+ $interleavedRules[] = $sibling;
+ continue;
+ }
+
+ // Always warn for siblings that aren't style rules, because they
+ // add no specificity and they're nested in the same parent as this
+ // declaration.
+ $this->warn(
+ <<<'MESSAGE'
+ Sass's behavior for declarations that appear after nested
+ rules will be changing to match the behavior specified by CSS in an upcoming
+ version. To keep the existing behavior, move the declaration above the nested
+ rule. To opt into the new behavior, wrap the declaration in `& {}`.
+
+ More info: https://sass-lang.com/d/mixed-decls
+ MESSAGE,
+ new MultiSpan($node->getSpan(), 'declaration', [
+ 'nested rule' => $sibling->getSpan(),
+ ]),
+ Deprecation::mixedDecls
+ );
+ $interleavedRules = [];
+ }
+ }
+
+ $name = $this->interpolationToValue($node->getName(), true);
+
+ if ($this->declarationName !== null) {
+ $name = new CssValue($this->declarationName . '-' . $name->getValue(), $name->getSpan());
+ }
+
+ $expression = $node->getValue();
+ if ($expression !== null) {
+ $value = $expression->accept($this);
+
+ // If the value is an empty list, preserve it, because converting it to CSS
+ // will throw an error that we want the user to see.
+
+ if (!$value->isBlank() || empty($value->asList())) {
+ $valueSpanForMap = null;
+ if ($this->sourceMap && $node->getValue() !== null) {
+ $valueSpanForMap = $this->expressionNode($node->getValue())->getSpan();
+ }
+
+ $this->getParent()->addChild(new ModifiableCssDeclaration(
+ $name,
+ new CssValue($value, $expression->getSpan()),
+ $node->getSpan(),
+ $node->isCustomProperty(),
+ $interleavedRules,
+ $interleavedRules === [] ? null : $this->stackTrace($node->getSpan()),
+ $valueSpanForMap,
+ ));
+ } elseif (str_starts_with($name->getValue(), '--')) {
+ throw $this->exception('Custom property values may not be empty.', $expression->getSpan());
+ }
+ }
+
+ $children = $node->getChildren();
+ if ($children !== null) {
+ $oldDeclarationName = $this->declarationName;
+ $this->declarationName = $name->getValue();
+ $this->environment->scope(function () use ($children) {
+ foreach ($children as $child) {
+ $child->accept($this);
+ }
+ }, $node->hasDeclarations());
+ $this->declarationName = $oldDeclarationName;
+ }
+
+ return null;
+ }
+
+ public function visitEachRule(EachRule $node): ?Value
+ {
+ $list = $node->getList()->accept($this);
+ $nodeWithSpan = $this->expressionNode($node->getList());
+
+ if (\count($node->getVariables()) === 1) {
+ $variableName = $node->getVariables()[0];
+ $setVariables = function (Value $value) use ($variableName, $nodeWithSpan) {
+ $this->environment->setLocalVariable($variableName, $this->withoutSlash($value, $nodeWithSpan), $nodeWithSpan);
+ };
+ } else {
+ $variables = $node->getVariables();
+ $setVariables = function (Value $value) use ($variables, $nodeWithSpan) {
+ $this->setMultipleVariables($variables, $value, $nodeWithSpan);
+ };
+ }
+
+ return $this->environment->scope(function () use ($list, $setVariables, $node) {
+ return $this->handleReturn($list->asList(), function ($element) use ($setVariables, $node) {
+ $setVariables($element);
+
+ return $this->handleReturn($node->getChildren(), fn(Statement $child) => $child->accept($this));
+ });
+ }, true, true);
+ }
+
+ /**
+ * Destructures $value and assigns it to $variables, as in an `@each`
+ * statement.
+ *
+ * @param list<string> $variables
+ */
+ private function setMultipleVariables(array $variables, Value $value, AstNode $nodeWithSpan): void
+ {
+ $list = $value->asList();
+ $minLength = min(\count($variables), \count($list));
+
+ for ($i = 0; $i < $minLength; $i++) {
+ $this->environment->setLocalVariable($variables[$i], $this->withoutSlash($list[$i], $nodeWithSpan), $nodeWithSpan);
+ }
+
+ for ($i = $minLength; $i < \count($variables); $i++) {
+ $this->environment->setLocalVariable($variables[$i], SassNull::create(), $nodeWithSpan);
+ }
+ }
+
+ public function visitErrorRule(ErrorRule $node): ?Value
+ {
+ throw $this->exception((string) $node->getExpression()->accept($this), $node->getSpan());
+ }
+
+ public function visitExtendRule(ExtendRule $node): ?Value
+ {
+ $styleRule = $this->getStyleRule();
+ if ($styleRule === null || $this->declarationName !== null) {
+ throw $this->exception('@extend may only be used within style rules.', $node->getSpan());
+ }
+
+ foreach ($styleRule->getOriginalSelector()->getComponents() as $complex) {
+ if (!$complex->isBogus()) {
+ continue;
+ }
+
+ $selectorString = trim($complex);
+ $verb = $complex->isUseless() ? "can't" : "shouldn't";
+
+ $this->warn(
+ "The selector \"$selectorString\" is invalid CSS and $verb be an extender.\nThis will be an error in Dart Sass 2.0.0.\n\nMore info: https://sass-lang.com/d/bogus-combinators",
+ new MultiSpan(SpanUtil::trimRight($complex->getSpan()), 'invalid selector', [
+ '@extend rule' => $node->getSpan(),
+ ]),
+ Deprecation::bogusCombinators
+ );
+ }
+
+ [$targetText, $targetMap] = $this->performInterpolationWithMap($node->getSelector(), true);
+ $list = SelectorList::parse(StringUtil::trimAscii($targetText, true), $this->logger, $targetMap, null, false);
+
+ foreach ($list->getComponents() as $complex) {
+ $compound = $complex->getSingleCompound();
+ if ($compound === null) {
+ // If the selector was a compound selector but not a simple
+ // selector, emit a more explicit error.
+ throw new SimpleSassFormatException('complex selectors may not be extended.', $complex->getSpan());
+ }
+
+ $simple = $compound->getSingleSimple();
+ if ($simple === null) {
+ $alternativeString = implode(', ', $compound->getComponents());
+ throw new SimpleSassFormatException("compound selectors may no longer be extended.\nConsider `@extend $alternativeString` instead.\nSee https://sass-lang.com/d/extend-compound for details.\n", $compound->getSpan());
+ }
+
+ $this->getExtensionStore()->addExtension($styleRule->getSelector(), $simple, $node, $this->mediaQueries);
+ }
+
+ return null;
+ }
+
+ public function visitAtRule(AtRule $node): ?Value
+ {
+ if ($this->declarationName !== null) {
+ throw $this->exception('At-rules may not be used within nested declarations.', $node->getSpan());
+ }
+
+ $name = $this->interpolationToValue($node->getName());
+ $value = $node->getValue() !== null ? $this->interpolationToValue($node->getValue(), true, true) : null;
+ $children = $node->getChildren();
+
+ if ($children === null) {
+ $this->getParent()->addChild(new ModifiableCssAtRule($name, $node->getSpan(), true, $value));
+
+ return null;
+ }
+
+ $wasInKeyframes = $this->inKeyFrames;
+ $wasInUnknownAtRule = $this->inUnknownAtRule;
+
+ if (Util::unvendor($name->getValue()) === 'keyframes') {
+ $this->inKeyFrames = true;
+ } else {
+ $this->inUnknownAtRule = true;
+ }
+
+ $this->withParent(
+ new ModifiableCssAtRule($name, $node->getSpan(), false, $value),
+ function () use ($children, $name) {
+ $styleRule = $this->getStyleRule();
+
+ if ($styleRule === null || $this->inKeyFrames || $name->getValue() === 'font-face') {
+ // Special-cased at-rules within style blocks are pulled out to the
+ // root. Equivalent to prepending "@at-root" on them.
+ foreach ($children as $child) {
+ $child->accept($this);
+ }
+ } else {
+ // If we're in a style rule, copy it into the at-rule so that
+ // declarations immediately inside it have somewhere to go.
+ //
+ // For example, "a {@foo {b: c}}" should produce "@foo {a {b: c}}".
+ $this->withParent($styleRule->copyWithoutChildren(), function () use ($children) {
+ foreach ($children as $child) {
+ $child->accept($this);
+ }
+ }, null, false);
+ }
+ },
+ function ($node) {
+ return $node instanceof CssStyleRule;
+ },
+ $node->hasDeclarations()
+ );
+
+ $this->inUnknownAtRule = $wasInUnknownAtRule;
+ $this->inKeyFrames = $wasInKeyframes;
+
+ return null;
+ }
+
+ public function visitForRule(ForRule $node): ?Value
+ {
+ /** @var SassNumber $fromNumber */
+ $fromNumber = $this->addExceptionSpan($node->getFrom(), function () use ($node) {
+ return $node->getFrom()->accept($this)->assertNumber();
+ });
+ /** @var SassNumber $toNumber */
+ $toNumber = $this->addExceptionSpan($node->getTo(), function () use ($node) {
+ return $node->getTo()->accept($this)->assertNumber();
+ });
+
+ $from = $this->addExceptionSpan($node->getFrom(), function () use ($fromNumber) {
+ return $fromNumber->assertInt();
+ });
+ $to = $this->addExceptionSpan($node->getTo(), function () use ($toNumber, $fromNumber) {
+ return $toNumber->coerce($fromNumber->getNumeratorUnits(), $fromNumber->getDenominatorUnits())->assertInt();
+ });
+
+ $direction = $from > $to ? -1 : 1;
+ if (!$node->isExclusive()) {
+ $to += $direction;
+ }
+
+ if ($from === $to) {
+ return null;
+ }
+
+ return $this->environment->scope(function () use ($node, $from, $to, $direction, $fromNumber) {
+ $nodeWithSpan = $this->expressionNode($node->getFrom());
+
+ for ($i = $from; $i !== $to; $i += $direction) {
+ $this->environment->setLocalVariable($node->getVariable(), SassNumber::withUnits($i, $fromNumber->getNumeratorUnits(), $fromNumber->getDenominatorUnits()), $nodeWithSpan);
+ $result = $this->handleReturn($node->getChildren(), function (Statement $child) {
+ return $child->accept($this);
+ });
+
+ if ($result !== null) {
+ return $result;
+ }
+ }
+
+ return null;
+ }, true, true);
+ }
+
+ public function visitFunctionRule(FunctionRule $node): ?Value
+ {
+ $this->environment->setFunction(new UserDefinedCallable($node, $this->environment->closure(), $this->inDependency));
+
+ return null;
+ }
+
+ public function visitIfRule(IfRule $node): ?Value
+ {
+ $clause = $node->getLastClause();
+
+ foreach ($node->getClauses() as $clauseToCheck) {
+ if ($clauseToCheck->getExpression()->accept($this)->isTruthy()) {
+ $clause = $clauseToCheck;
+ break;
+ }
+ }
+
+ if ($clause === null) {
+ return null;
+ }
+
+ return $this->environment->scope(function () use ($clause) {
+ return $this->handleReturn($clause->getChildren(), function (Statement $child) {
+ return $child->accept($this);
+ });
+ }, $clause->hasDeclarations(), true);
+ }
+
+ public function visitImportRule(ImportRule $node): ?Value
+ {
+ foreach ($node->getImports() as $import) {
+ if ($import instanceof DynamicImport) {
+ $this->visitDynamicImport($import);
+ } else {
+ assert($import instanceof StaticImport);
+ $this->visitStaticImport($import);
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Adds the stylesheet imported by $import to the current document.
+ */
+ private function visitDynamicImport(DynamicImport $import): void
+ {
+ $this->withStackFrame('@import', $import, function () use ($import) {
+ $result = $this->loadStylesheet($import->getUrlString(), $import->getSpan(), true);
+ $stylesheet = $result->getStylesheet();
+
+ $url = $stylesheet->getSpan()->getSourceUrl();
+
+ if ($url !== null) {
+ $urlString = (string) $url;
+ if (array_key_exists($urlString, $this->activeModules)) {
+ $previousLoad = $this->activeModules[$urlString];
+ if ($previousLoad !== null) {
+ throw $this->multiSpanException('This file is already being loaded.', 'new load', ['original load' => $previousLoad->getSpan()]);
+ }
+
+ throw $this->exception('This file is already being loaded.');
+ }
+
+ $this->activeModules[$urlString] = $import;
+ }
+
+ $oldImporter = $this->importer;
+ $oldStylesheet = $this->stylesheet;
+ $oldInDependency = $this->inDependency;
+ $this->importer = $result->getImporter();
+ $this->stylesheet = $stylesheet;
+ $this->inDependency = $result->isDependency();
+ $this->visitStylesheet($stylesheet);
+ $this->importer = $oldImporter;
+ $this->stylesheet = $oldStylesheet;
+ $this->inDependency = $oldInDependency;
+
+ if ($url !== null) {
+ unset($this->activeModules[(string) $url]);
+ }
+ });
+ }
+
+ private function loadStylesheet(string $url, FileSpan $span, bool $forImport = false): LoadedStylesheet
+ {
+ try {
+ assert($this->importSpan === null);
+ $this->importSpan = $span;
+
+ $baseUrlString = $this->getStylesheet()->getSpan()->getSourceUrl();
+ $baseUrl = $baseUrlString === null ? null : Uri::new($baseUrlString);
+
+ $result = $this->importCache->canonicalize(Uri::new($url), $this->importer, $baseUrl, $forImport);
+ if ($result !== null) {
+ $canonicalUrl = $result->canonicalUrl;
+ $importer = $result->importer;
+ $originalUrl = $result->originalUrl;
+
+ // Make sure we record the canonical URL as "loaded" even if the
+ // actual load fails, because watchers should watch it to see if it
+ // changes in a way that allows the load to succeed.
+ $this->loadedUrls[$canonicalUrl->toString()] = true;
+
+ $isDependency = $this->inDependency || $importer !== $this->importer;
+
+ $stylesheet = $this->importCache->importCanonical($importer, $canonicalUrl, $originalUrl, $this->quietDeps && $isDependency);
+
+ if ($stylesheet !== null) {
+ return new LoadedStylesheet($stylesheet, $importer, $isDependency);
+ }
+ }
+
+ throw new \Exception("Can't find stylesheet to import.");
+ } catch (SassException $e) {
+ throw $e;
+ } catch (\Throwable $e) {
+ throw $this->exception($e->getMessage(), null, $e);
+ } finally {
+ $this->importSpan = null;
+ }
+ }
+
+ /**
+ * Adds a CSS import for $import.
+ */
+ private function visitStaticImport(StaticImport $import): void
+ {
+ $url = $this->interpolationToValue($import->getUrl());
+ $modifiers = $import->getModifiers() !== null ? $this->interpolationToValue($import->getModifiers()) : null;
+
+ $node = new ModifiableCssImport($url, $import->getSpan(), $modifiers);
+
+ if ($this->getParent() !== $this->getRoot()) {
+ $this->getParent()->addChild($node);
+ } elseif ($this->getEndOfImports() === \count($this->getRoot()->getChildren())) {
+ $this->getRoot()->addChild($node);
+ $this->endOfImports++;
+ } else {
+ $this->outOfOrderImports[] = $node;
+ }
+ }
+
+ /**
+ * Evaluate a given $mixin with $arguments and $contentCallable
+ */
+ private function applyMixin(?SassCallable $mixin, ?UserDefinedCallable $contentCallable, ArgumentInvocation $arguments, AstNode $nodeWithSpan, AstNode $nodeWithSpanWithoutContent): void
+ {
+ if ($mixin === null) {
+ throw $this->exception('Undefined mixin.', $nodeWithSpan->getSpan());
+ }
+
+ if ($mixin instanceof BuiltInCallable && !$mixin->acceptsContent() && $contentCallable !== null) {
+ $evaluated = $this->evaluateArguments($arguments);
+ /** @var ArgumentDeclaration $overload */
+ [$overload,] = $mixin->callbackFor(\count($evaluated->getPositional()), $evaluated->getNamed());
+ throw new MultiSpanSassRuntimeException(
+ "Mixin doesn't accept a content block.",
+ $nodeWithSpanWithoutContent->getSpan(),
+ 'invocation',
+ ['declaration' => $overload->getSpanWithName()],
+ $this->stackTrace($nodeWithSpanWithoutContent->getSpan())
+ );
+ }
+
+ if ($mixin instanceof BuiltInCallable) {
+ $this->environment->withContent($contentCallable, fn() => $this->environment->asMixin(function () use ($arguments, $mixin, $nodeWithSpanWithoutContent) {
+ $this->runBuiltInCallable($arguments, $mixin, $nodeWithSpanWithoutContent);
+ }));
+ } elseif ($mixin instanceof UserDefinedCallable) {
+ $declaration = $mixin->getDeclaration();
+ assert($declaration instanceof MixinRule);
+
+ if ($contentCallable !== null && !$declaration->hasContent()) {
+ throw new MultiSpanSassRuntimeException(
+ "Mixin doesn't accept a content block.",
+ $nodeWithSpanWithoutContent->getSpan(),
+ 'invocation',
+ ['declaration' => $mixin->getDeclaration()->getArguments()->getSpanWithName()],
+ $this->stackTrace($nodeWithSpanWithoutContent->getSpan())
+ );
+ }
+
+ $this->runUserDefinedCallable($arguments, $mixin, $nodeWithSpanWithoutContent, function () use ($contentCallable, $declaration, $nodeWithSpanWithoutContent) {
+ $this->environment->withContent($contentCallable, fn() => $this->environment->asMixin(function () use ($declaration, $nodeWithSpanWithoutContent) {
+ foreach ($declaration->getChildren() as $statement) {
+ $this->addErrorSpan($nodeWithSpanWithoutContent, fn() => $statement->accept($this));
+ }
+ }));
+
+ return null;
+ });
+ } else {
+ throw new \LogicException('Unknown callable type ' . get_class($mixin));
+ }
+ }
+
+ public function visitIncludeRule(IncludeRule $node): ?Value
+ {
+ $mixin = $this->addExceptionSpan($node, function () use ($node) {
+ return $this->environment->getMixin($node->getName());
+ });
+
+ if (str_starts_with($node->getOriginalName(), '--') && $mixin instanceof UserDefinedCallable && !str_starts_with($mixin->getDeclaration()->getOriginalName(), '--')) {
+ $this->warn("Sass @mixin names beginning with -- are deprecated for forward-compatibility with plain CSS mixins.\n\nFor details, see https://sass-lang.com/d/css-function-mixin", $node->getNameSpan(), Deprecation::cssFunctionMixin);
+ }
+
+ $contentCallable = null;
+ if ($node->getContent() !== null) {
+ $contentCallable = new UserDefinedCallable($node->getContent(), $this->environment->closure(), $this->inDependency);
+ }
+
+ $nodeWithSpanWithoutContent = new FakeAstNode(function () use ($node) {
+ return $node->getSpanWithoutContent();
+ });
+
+ $this->applyMixin($mixin, $contentCallable, $node->getArguments(), $node, $nodeWithSpanWithoutContent);
+
+ return null;
+ }
+
+ public function visitMixinRule(MixinRule $node): ?Value
+ {
+ $this->environment->setMixin(new UserDefinedCallable($node, $this->environment->closure(), $this->inDependency));
+
+ return null;
+ }
+
+ public function visitLoudComment(LoudComment $node): ?Value
+ {
+ if ($this->inFunction) {
+ return null;
+ }
+
+ // Comments are allowed to appear between CSS imports.
+ if ($this->getParent() === $this->getRoot() && $this->getEndOfImports() === \count($this->getRoot()->getChildren())) {
+ $this->endOfImports++;
+ }
+
+ $text = $this->performInterpolation($node->getText());
+ // Indented syntax doesn't require */
+ if (!str_ends_with($text, '*/')) {
+ $text .= ' */';
+ }
+
+ $this->getParent()->addChild(new ModifiableCssComment($text, $node->getSpan()));
+
+ return null;
+ }
+
+ public function visitMediaRule(MediaRule $node): ?Value
+ {
+ if ($this->declarationName !== null) {
+ throw $this->exception('Media rules may not be used within nested declarations.', $node->getSpan());
+ }
+
+ $queries = $this->visitMediaQueries($node->getQuery());
+ $mergedQueries = $this->mediaQueries !== null ? $this->mergeMediaQueries($this->mediaQueries, $queries) : null;
+
+ if ($mergedQueries === []) {
+ return null;
+ }
+
+ if ($mergedQueries === null) {
+ $mergedSources = [];
+ } else {
+ assert($this->mediaQuerySources !== null);
+ assert($this->mediaQueries !== null);
+
+ $mergedSources = array_merge($this->mediaQuerySources, $this->mediaQueries, $queries);
+ }
+
+ $this->withParent(
+ new ModifiableCssMediaRule($mergedQueries ?? $queries, $node->getSpan()),
+ function () use ($mergedQueries, $mergedSources, $queries, $node) {
+ $this->withMediaQueries($mergedQueries ?? $queries, $mergedSources, function () use ($node) {
+ $styleRule = $this->getStyleRule();
+
+ if ($styleRule !== null) {
+ // If we're in a style rule, copy it into the media query so that
+ // declarations immediately inside @media have somewhere to go.
+ //
+ // For example, "a {@media screen {b: c}}" should produce
+ // "@media screen {a {b: c}}".
+ $this->withParent($styleRule->copyWithoutChildren(), function () use ($node) {
+ foreach ($node->getChildren() as $child) {
+ $child->accept($this);
+ }
+ }, null, false);
+ } else {
+ foreach ($node->getChildren() as $child) {
+ $child->accept($this);
+ }
+ }
+ });
+ },
+ function ($node) use ($mergedSources) {
+ if ($node instanceof CssStyleRule) {
+ return true;
+ }
+
+ if ($mergedSources !== [] && $node instanceof CssMediaRule) {
+ return IterableUtil::every($node->getQueries(), function (CssMediaQuery $query) use ($mergedSources) {
+ return \in_array($query, $mergedSources, true);
+ });
+ }
+
+ return false;
+ },
+ $node->hasDeclarations()
+ );
+
+ return null;
+ }
+
+ /**
+ * @param Interpolation $interpolation
+ *
+ * @return list<CssMediaQuery>
+ */
+ private function visitMediaQueries(Interpolation $interpolation): array
+ {
+ [$resolved, $map] = $this->performInterpolationWithMap($interpolation, true);
+
+ return CssMediaQuery::parseList($resolved, $this->logger, null, $map);
+ }
+
+ /**
+ * Returns a list of queries that selects for contexts that match both
+ * $queries1 and $queries2.
+ *
+ * Returns the empty list if there are no contexts that match both $queries1
+ * and $queries2, or `null` if there are contexts that can't be represented
+ * by media queries.
+ *
+ * @param CssMediaQuery[] $queries1
+ * @param CssMediaQuery[] $queries2
+ *
+ * @return list<CssMediaQuery>|null
+ */
+ private function mergeMediaQueries(array $queries1, array $queries2): ?array
+ {
+ $queries = [];
+
+ foreach ($queries1 as $query1) {
+ foreach ($queries2 as $query2) {
+ $result = $query1->merge($query2);
+
+ if ($result === MediaQuerySingletonMergeResult::empty) {
+ continue;
+ }
+
+ if ($result === MediaQuerySingletonMergeResult::unrepresentable) {
+ return null;
+ }
+
+ // Always true but not detected due to https://github.com/jiripudil/phpstan-sealed-classes/issues/2
+ \assert($result instanceof CssMediaQuery);
+
+ $queries[] = $result;
+ }
+ }
+
+ return $queries;
+ }
+
+ public function visitReturnRule(ReturnRule $node): ?Value
+ {
+ return $this->withoutSlash($node->getExpression()->accept($this), $node->getExpression());
+ }
+
+ public function visitSilentComment(SilentComment $node): ?Value
+ {
+ return null;
+ }
+
+ public function visitStyleRule(StyleRule $node): ?Value
+ {
+ if ($this->declarationName !== null) {
+ throw $this->exception('Style rules may not be used within nested declarations.', $node->getSpan());
+ }
+
+ if ($this->inKeyFrames && $this->getParent() instanceof CssKeyframeBlock) {
+ throw $this->exception('Style rules may not be used within keyframe blocks.', $node->getSpan());
+ }
+
+ [$selectorText, $selectorMap] = $this->performInterpolationWithMap($node->getSelector(), true);
+
+ if ($this->inKeyFrames) {
+ $parsedSelector = (new KeyframeSelectorParser($selectorText, $this->logger, null, $selectorMap))->parse();
+ $rule = new ModifiableCssKeyframeBlock(new CssValue($parsedSelector, $node->getSelector()->getSpan()), $node->getSpan());
+
+ $this->withParent(
+ $rule,
+ function () use ($node) {
+ foreach ($node->getChildren() as $child) {
+ $child->accept($this);
+ }
+ },
+ function ($node) {
+ return $node instanceof CssStyleRule;
+ },
+ $node->hasDeclarations()
+ );
+
+ return null;
+ }
+
+ $parsedSelector = SelectorList::parse($selectorText, $this->logger, $selectorMap, plainCss: $this->getStylesheet()->isPlainCss());
+ $nest = !($this->getStyleRule()?->isFromPlainCss() ?? false);
+ if ($nest) {
+ if ($this->getStylesheet()->isPlainCss()) {
+ foreach ($parsedSelector->getComponents() as $complex) {
+ if (\count($complex->getLeadingCombinators()) > 0) {
+ throw $this->exception("Top-level leading combinators aren't allowed in plain CSS.", $complex->getLeadingCombinators()[0]->getSpan());
+ }
+ }
+ }
+
+ $parsedSelector = $parsedSelector->nestWithin(
+ $this->styleRuleIgnoringAtRoot?->getOriginalSelector(),
+ !$this->atRootExcludingStyleRule,
+ $this->getStylesheet()->isPlainCss()
+ );
+ }
+
+ $selector = $this->getExtensionStore()->addSelector($parsedSelector, $this->mediaQueries);
+ $rule = new ModifiableCssStyleRule($selector, $node->getSpan(), $parsedSelector, $this->getStylesheet()->isPlainCss());
+ $oldAtRootExcludingStyleRule = $this->atRootExcludingStyleRule;
+ $this->atRootExcludingStyleRule = false;
+ $this->withParent(
+ $rule,
+ function () use ($rule, $node) {
+ $this->withStyleRule($rule, function () use ($node) {
+ foreach ($node->getChildren() as $child) {
+ $child->accept($this);
+ }
+ });
+ },
+ $nest ? fn($node) => $node instanceof CssStyleRule : null,
+ $node->hasDeclarations()
+ );
+ $this->atRootExcludingStyleRule = $oldAtRootExcludingStyleRule;
+
+ $this->warnForBogusCombinators($rule);
+
+ if ($this->getStyleRule() === null && \count($this->getParent()->getChildren()) > 0) {
+ $lastChild = ListUtil::last($this->getParent()->getChildren());
+ $lastChild->setGroupEnd(true);
+ }
+
+ return null;
+ }
+
+ private function warnForBogusCombinators(CssStyleRule $rule): void
+ {
+ if (!$rule->isInvisibleOtherThanBogusCombinators()) {
+ foreach ($rule->getSelector()->getComponents() as $complex) {
+ if (!$complex->isBogus()) {
+ continue;
+ }
+
+ $selectorString = trim($complex);
+
+ if ($complex->isUseless()) {
+ $this->warn(
+ "The selector \"$selectorString\" is invalid CSS. It will be omitted from the generated CSS.\nThis will be an error in Dart Sass 2.0.0.\n\nMore info: https://sass-lang.com/d/bogus-combinators",
+ SpanUtil::trimRight($complex->getSpan()),
+ Deprecation::bogusCombinators
+ );
+ } elseif (\count($complex->getLeadingCombinators()) > 0) {
+ if (!$this->getStylesheet()->isPlainCss()) {
+ $this->warn(
+ "The selector \"$selectorString\" is invalid CSS.\nThis will be an error in Dart Sass 2.0.0.\n\nMore info: https://sass-lang.com/d/bogus-combinators",
+ SpanUtil::trimRight($complex->getSpan()),
+ Deprecation::bogusCombinators
+ );
+ }
+ } else {
+ $omittedMessage = $complex->isBogusOtherThanLeadingCombinator() ? ' It will be omitted from the generated CSS.' : '';
+ $suffix = IterableUtil::every($rule->getChildren(), fn (CssNode $child) => $child instanceof CssComment) ? "\n(try converting to a //-style comment)" : '';
+ $this->warn(
+ "The selector \"$selectorString\" is only valid for nesting and shouldn't\nhave children other than style rules.$omittedMessage\nThis will be an error in Dart Sass 2.0.0.\n\nMore info: https://sass-lang.com/d/bogus-combinators",
+ new MultiSpan(SpanUtil::trimRight($complex->getSpan()), 'invalid selector', [
+ 'this is not a style rule' . $suffix => $rule->getChildren()[0]->getSpan(),
+ ]),
+ Deprecation::bogusCombinators
+ );
+ }
+ }
+ }
+ }
+
+ public function visitSupportsRule(SupportsRule $node): ?Value
+ {
+ if ($this->declarationName !== null) {
+ throw $this->exception('Supports rules may not be used within nested declarations.', $node->getSpan());
+ }
+
+ $condition = new CssValue($this->visitSupportsCondition($node->getCondition()), $node->getCondition()->getSpan());
+
+ $this->withParent(
+ new ModifiableCssSupportsRule($condition, $node->getSpan()),
+ function () use ($node) {
+ $styleRule = $this->getStyleRule();
+
+ if ($styleRule !== null) {
+ // If we're in a style rule, copy it into the supports rule so that
+ // declarations immediately inside @supports have somewhere to go.
+ //
+ // For example, "a {@supports (a: b) {b: c}}" should produce "@supports
+ // (a: b) {a {b: c}}".
+ $this->withParent($styleRule->copyWithoutChildren(), function () use ($node) {
+ foreach ($node->getChildren() as $child) {
+ $child->accept($this);
+ }
+ });
+ } else {
+ foreach ($node->getChildren() as $child) {
+ $child->accept($this);
+ }
+ }
+ },
+ function ($node) {
+ return $node instanceof CssStyleRule;
+ },
+ $node->hasDeclarations()
+ );
+
+ return null;
+ }
+
+ private function visitSupportsCondition(SupportsCondition $condition): string
+ {
+ if ($condition instanceof SupportsOperation) {
+ return sprintf('%s %s %s', $this->parenthesize($condition->getLeft(), $condition->getOperator()), $condition->getOperator(), $this->parenthesize($condition->getRight(), $condition->getOperator()));
+ }
+
+ if ($condition instanceof SupportsNegation) {
+ return 'not ' . $this->parenthesize($condition->getCondition());
+ }
+
+ if ($condition instanceof SupportsInterpolation) {
+ return $this->evaluateToCss($condition->getExpression(), false);
+ }
+
+ if ($condition instanceof SupportsDeclaration) {
+ return $this->withSupportsDeclaration(function () use ($condition) {
+ return sprintf('(%s:%s%s)', $this->evaluateToCss($condition->getName()), $condition->isCustomProperty() ? '' : ' ', $this->evaluateToCss($condition->getValue()));
+ });
+ }
+
+ if ($condition instanceof SupportsFunction) {
+ return sprintf('%s(%s)', $this->performInterpolation($condition->getName()), $this->performInterpolation($condition->getArguments()));
+ }
+
+ if ($condition instanceof SupportsAnything) {
+ return '(' . $this->performInterpolation($condition->getContents()) . ')';
+ }
+
+ throw new \InvalidArgumentException('Unknown supports condition type ' . get_class($condition));
+ }
+
+ /**
+ * Runs $callback in a context where {@see $inSupportsDeclaration} is true.
+ *
+ * @template T
+ *
+ * @param callable(): T $callback
+ * @return T
+ *
+ * @param-immediately-invoked-callable $callback
+ */
+ private function withSupportsDeclaration(callable $callback)
+ {
+ $oldInSupportsDeclaration = $this->inSupportsDeclaration;
+ $this->inSupportsDeclaration = true;
+ try {
+ return $callback();
+ } finally {
+ $this->inSupportsDeclaration = $oldInSupportsDeclaration;
+ }
+ }
+
+ private function parenthesize(SupportsCondition $condition, ?string $operator = null): string
+ {
+ if ($condition instanceof SupportsNegation || $condition instanceof SupportsOperation && $operator !== $condition->getOperator()) {
+ return '(' . $this->visitSupportsCondition($condition) . ')';
+ }
+
+ return $this->visitSupportsCondition($condition);
+ }
+
+ public function visitVariableDeclaration(VariableDeclaration $node): ?Value
+ {
+ if ($node->isGuarded()) {
+ $value = $this->addExceptionSpan($node, function () use ($node) {
+ return $this->environment->getVariable($node->getName());
+ });
+
+ if ($value !== null && $value !== SassNull::create()) {
+ return null;
+ }
+ }
+
+ if ($node->isGlobal() && !$this->environment->globalVariableExists($node->getName())) {
+ $this->warn(
+ $this->environment->atRoot()
+ ? "As of Dart Sass 2.0.0, !global assignments won't be able to declare new variables.\n\nSince this assignment is at the root of the stylesheet, the !global flag is\nunnecessary and can safely be removed."
+ : "As of Dart Sass 2.0.0, !global assignments won't be able to declare new variables.\n\nRecommendation: add `{$node->getOriginalName()}: null` at the stylesheet root.",
+ $node->getSpan(),
+ Deprecation::newGlobal
+ );
+ }
+
+ $value = $this->withoutSlash($node->getExpression()->accept($this), $node->getExpression());
+ $this->addExceptionSpan($node, function () use ($value, $node) {
+ $this->environment->setVariable($node->getName(), $value, $this->expressionNode($node->getExpression()), $node->isGlobal());
+ });
+
+ return null;
+ }
+
+ public function visitWarnRule(WarnRule $node): ?Value
+ {
+ $value = $this->addExceptionSpan($node, function () use ($node) {
+ return $node->getExpression()->accept($this);
+ });
+ $this->logger->warn($value instanceof SassString ? $value->getText() : $this->serialize($value, $node->getExpression()), null, null, $this->stackTrace($node->getSpan()));
+
+ return null;
+ }
+
+ public function visitWhileRule(WhileRule $node): ?Value
+ {
+ return $this->environment->scope(function () use ($node) {
+ while ($node->getCondition()->accept($this)->isTruthy()) {
+ $result = $this->handleReturn($node->getChildren(), function (Statement $child) {
+ return $child->accept($this);
+ });
+
+ if ($result !== null) {
+ return $result;
+ }
+ }
+
+ return null;
+ }, $node->hasDeclarations(), true);
+ }
+
+ // ## Expressions
+
+ public function visitBinaryOperationExpression(BinaryOperationExpression $node): Value
+ {
+ if ($this->getStylesheet()->isPlainCss() && $node->getOperator() !== BinaryOperator::SINGLE_EQUALS && $node->getOperator() !== BinaryOperator::DIVIDED_BY) {
+ throw $this->exception("Operators aren't allowed in plain CSS.", $node->getOperatorSpan());
+ }
+
+ return $this->addExceptionSpan($node, function () use ($node) {
+ $left = $node->getLeft()->accept($this);
+
+ return match ($node->getOperator()) {
+ BinaryOperator::SINGLE_EQUALS => $left->singleEquals($node->getRight()->accept($this)),
+ BinaryOperator::OR => $left->isTruthy() ? $left : $node->getRight()->accept($this),
+ BinaryOperator::AND => $left->isTruthy() ? $node->getRight()->accept($this) : $left,
+ BinaryOperator::EQUALS => SassBoolean::create($left->equals($node->getRight()->accept($this))),
+ BinaryOperator::NOT_EQUALS => SassBoolean::create(!$left->equals($node->getRight()->accept($this))),
+ BinaryOperator::GREATER_THAN => $left->greaterThan($node->getRight()->accept($this)),
+ BinaryOperator::GREATER_THAN_OR_EQUALS => $left->greaterThanOrEquals($node->getRight()->accept($this)),
+ BinaryOperator::LESS_THAN => $left->lessThan($node->getRight()->accept($this)),
+ BinaryOperator::LESS_THAN_OR_EQUALS => $left->lessThanOrEquals($node->getRight()->accept($this)),
+ BinaryOperator::PLUS => $left->plus($node->getRight()->accept($this)),
+ BinaryOperator::MINUS => $left->minus($node->getRight()->accept($this)),
+ BinaryOperator::TIMES => $left->times($node->getRight()->accept($this)),
+ BinaryOperator::DIVIDED_BY => $this->slash($left, $node->getRight()->accept($this), $node),
+ BinaryOperator::MODULO => $left->modulo($node->getRight()->accept($this)),
+ };
+ });
+ }
+
+ /**
+ * Returns the result of the SassScript `/` operation between $left and
+ * $right in $node.
+ */
+ private function slash(Value $left, Value $right, BinaryOperationExpression $node): Value
+ {
+ $result = $left->dividedBy($right);
+
+ if ($left instanceof SassNumber && $right instanceof SassNumber && $node->allowsSlash() && $this->operandAllowsSlash($node->getLeft()) && $this->operandAllowsSlash($node->getRight())) {
+ assert($result instanceof SassNumber);
+
+ return $result->withSlash($left, $right);
+ }
+
+ if ($left instanceof SassNumber && $right instanceof SassNumber) {
+ $recommendation = function (Expression $expression) use (&$recommendation): string {
+ if ($expression instanceof BinaryOperationExpression && $expression->getOperator() === BinaryOperator::DIVIDED_BY) {
+ $leftRecommendation = $recommendation($expression->getLeft());
+ $rightRecommendation = $recommendation($expression->getRight());
+
+ return "math.div($leftRecommendation, $rightRecommendation)";
+ }
+
+ if ($expression instanceof ParenthesizedExpression) {
+ return (string) $expression->getExpression();
+ }
+
+ return (string) $expression;
+ };
+
+ $calcRecommendation = AstUtil::expressionToCalc($node);
+
+ $message = <<<WARNING
+Using / for division outside of calc() is deprecated and will be removed in Dart Sass 2.0.0.
+
+Recommendation: {$recommendation($node)} or $calcRecommendation
+
+More info and automated migrator: https://sass-lang.com/d/slash-div
+WARNING;
+
+ $this->warn($message, $node->getSpan(), Deprecation::slashDiv);
+
+ return $result;
+ }
+
+ return $result;
+ }
+
+ /**
+ * Returns whether $node can be used as a component of a slash-separated
+ * number.
+ *
+ * Although this logic is mostly resolved at parse-time, we can't tell
+ * whether operands will be evaluated as calculations until evaluation-time.
+ */
+ private function operandAllowsSlash(Expression $node): bool
+ {
+ if (!$node instanceof FunctionExpression) {
+ return true;
+ }
+
+ if ($node->getNamespace() !== null) {
+ return false;
+ }
+
+ return \in_array(strtolower($node->getName()), ['calc', 'clamp', 'hypot', 'sin', 'cos', 'tan', 'asin', 'acos', 'atan', 'sqrt', 'exp', 'sign', 'mod', 'rem', 'atan2', 'pow', 'log'], true) && $this->environment->getFunction($node->getName()) === null;
+ }
+
+ public function visitValueExpression(ValueExpression $node): Value
+ {
+ return $node->getValue();
+ }
+
+ public function visitVariableExpression(VariableExpression $node): Value
+ {
+ $result = $this->addExceptionSpan($node, function () use ($node) {
+ return $this->environment->getVariable($node->getName());
+ });
+
+ if ($result !== null) {
+ return $result;
+ }
+
+ throw $this->exception('Undefined variable.', $node->getSpan());
+ }
+
+ public function visitUnaryOperationExpression(UnaryOperationExpression $node): Value
+ {
+ $operand = $node->getOperand()->accept($this);
+
+ return $this->addExceptionSpan($node, fn() => match ($node->getOperator()) {
+ UnaryOperator::PLUS => $operand->unaryPlus(),
+ UnaryOperator::MINUS => $operand->unaryMinus(),
+ UnaryOperator::DIVIDE => $operand->unaryDivide(),
+ UnaryOperator::NOT => $operand->unaryNot(),
+ });
+ }
+
+ public function visitBooleanExpression(BooleanExpression $node): Value
+ {
+ return SassBoolean::create($node->getValue());
+ }
+
+ public function visitIfExpression(IfExpression $node): Value
+ {
+ [$positional, $named] = $this->evaluateMacroArguments($node);
+
+ $this->verifyArguments(\count($positional), $named, IfExpression::getDeclaration(), $node);
+
+ $condition = $positional[0] ?? $named['condition'];
+ $ifTrue = $positional[1] ?? $named['if-true'];
+ $ifFalse = $positional[2] ?? $named['if-false'];
+
+ $result = $condition->accept($this)->isTruthy() ? $ifTrue : $ifFalse;
+
+ return $this->withoutSlash($result->accept($this), $this->expressionNode($result));
+ }
+
+ public function visitNullExpression(NullExpression $node): Value
+ {
+ return SassNull::create();
+ }
+
+ public function visitNumberExpression(NumberExpression $node): Value
+ {
+ return SassNumber::create($node->getValue(), $node->getUnit());
+ }
+
+ public function visitParenthesizedExpression(ParenthesizedExpression $node): Value
+ {
+ if ($this->getStylesheet()->isPlainCss()) {
+ throw $this->exception("Parentheses aren't allowed in plain CSS.", $node->getSpan());
+ }
+
+ return $node->getExpression()->accept($this);
+ }
+
+ public function visitColorExpression(ColorExpression $node): Value
+ {
+ return $node->getValue();
+ }
+
+ public function visitListExpression(ListExpression $node): Value
+ {
+ return new SassList(array_map(function (Expression $expression) {
+ return $expression->accept($this);
+ }, $node->getContents()), $node->getSeparator(), $node->hasBrackets());
+ }
+
+ public function visitMapExpression(MapExpression $node): Value
+ {
+ /** @var Map<Value> $map */
+ $map = new Map();
+ /** @var Map<AstNode> $keyNodes */
+ $keyNodes = new Map();
+
+ foreach ($node->getPairs() as $pair) {
+ $keyValue = $pair[0]->accept($this);
+ $valueValue = $pair[1]->accept($this);
+
+ $oldValue = $map->get($keyValue);
+
+ if ($oldValue !== null) {
+ $oldValueSpan = $keyNodes->get($keyValue)?->getSpan();
+ throw new MultiSpanSassRuntimeException(
+ 'Duplicate key.',
+ $pair[0]->getSpan(),
+ 'second key',
+ $oldValueSpan !== null ? ['first key' => $oldValueSpan] : [],
+ $this->stackTrace($pair[0]->getSpan())
+ );
+ }
+
+ $map->put($keyValue, $valueValue);
+ $keyNodes->put($keyValue, $pair[0]);
+ }
+
+ return SassMap::create($map);
+ }
+
+ private function getBuiltinFunction(string $name): ?SassCallable
+ {
+ if (!isset($this->builtInFunctions[$name]) && FunctionRegistry::has($name)) {
+ $this->builtInFunctions[$name] = FunctionRegistry::get($name);
+ }
+
+ return $this->builtInFunctions[$name] ?? null;
+ }
+
+ public function visitFunctionExpression(FunctionExpression $node): Value
+ {
+ $function = $this->getStylesheet()->isPlainCss() ? null : $this->addExceptionSpan($node, function () use ($node) {
+ return $this->environment->getFunction($node->getName());
+ });
+
+ if ($function === null) {
+ if ($node->getNamespace() !== null) {
+ throw $this->exception('Undefined function.', $node->getSpan());
+ }
+
+ switch (strtolower($node->getName())) {
+ case 'min':
+ case 'max':
+ case 'round':
+ case 'abs':
+ if (
+ $node->getArguments()->getNamed() === []
+ && $node->getArguments()->getRest() === null
+ && IterableUtil::every($node->getArguments()->getPositional(), function (Expression $argument) {
+ return $argument->accept(new IsCalculationSafeVisitor());
+ })
+ ) {
+ return $this->visitCalculation($node, true);
+ }
+ break;
+
+ case 'calc':
+ case 'clamp':
+ case 'hypot':
+ case 'sin':
+ case 'cos':
+ case 'tan':
+ case 'asin':
+ case 'acos':
+ case 'atan':
+ case 'sqrt':
+ case 'exp':
+ case 'sign':
+ case 'mod':
+ case 'rem':
+ case 'atan2':
+ case 'pow':
+ case 'log':
+ return $this->visitCalculation($node);
+ }
+
+ $function = ($this->getStylesheet()->isPlainCss() ? null : $this->getBuiltinFunction($node->getName())) ?? new PlainCssCallable($node->getOriginalName());
+ }
+
+ if (str_starts_with($node->getOriginalName(), '--') && $function instanceof UserDefinedCallable && !str_starts_with($function->getDeclaration()->getOriginalName(), '--')) {
+ $this->warn("Sass @function names beginning with -- are deprecated for forward-compatibility with plain CSS functions.\n\nFor details, see https://sass-lang.com/d/css-function-mixin", $node->getNameSpan(), Deprecation::cssFunctionMixin);
+ }
+
+ $oldInFunction = $this->inFunction;
+ $this->inFunction = true;
+ $result = $this->addErrorSpan($node, function () use ($function, $node) {
+ return $this->runFunctionCallable($node->getArguments(), $function, $node);
+ });
+ $this->inFunction = $oldInFunction;
+
+ return $result;
+ }
+
+ private function visitCalculation(FunctionExpression $node, bool $inLegacySassFunction = false): Value
+ {
+ if ($node->getArguments()->getNamed() !== []) {
+ throw $this->exception("Keyword arguments can't be used with calculations.", $node->getSpan());
+ }
+
+ if ($node->getArguments()->getRest() !== null) {
+ throw $this->exception("Rest arguments can't be used with calculations.", $node->getSpan());
+ }
+
+ $this->checkCalculationArguments($node);
+ $arguments = array_map(function ($argument) use ($inLegacySassFunction) {
+ return $this->visitCalculationExpression($argument, $inLegacySassFunction);
+ }, $node->getArguments()->getPositional());
+
+ if ($this->inSupportsDeclaration) {
+ return SassCalculation::unsimplified($node->getName(), $arguments);
+ }
+
+ $oldCallableNode = $this->callableNode;
+ $this->callableNode = $node;
+
+ try {
+ return match (strtolower($node->getName())) {
+ 'calc' => SassCalculation::calc($arguments[0]),
+ 'sqrt' => SassCalculation::sqrt($arguments[0]),
+ 'sin' => SassCalculation::sin($arguments[0]),
+ 'cos' => SassCalculation::cos($arguments[0]),
+ 'tan' => SassCalculation::tan($arguments[0]),
+ 'asin' => SassCalculation::asin($arguments[0]),
+ 'acos' => SassCalculation::acos($arguments[0]),
+ 'atan' => SassCalculation::atan($arguments[0]),
+ 'abs' => SassCalculation::abs($arguments[0]),
+ 'exp' => SassCalculation::exp($arguments[0]),
+ 'sign' => SassCalculation::sign($arguments[0]),
+ 'min' => SassCalculation::min($arguments),
+ 'max' => SassCalculation::max($arguments),
+ 'hypot' => SassCalculation::hypot($arguments),
+ 'pow' => SassCalculation::pow($arguments[0], $arguments[1] ?? null),
+ 'atan2' => SassCalculation::atan2($arguments[0], $arguments[1] ?? null),
+ 'log' => SassCalculation::log($arguments[0], $arguments[1] ?? null),
+ 'mod' => SassCalculation::mod($arguments[0], $arguments[1] ?? null),
+ 'rem' => SassCalculation::rem($arguments[0], $arguments[1] ?? null),
+ 'round' => SassCalculation::round($arguments[0], $arguments[1] ?? null, $arguments[2] ?? null),
+ 'clamp' => SassCalculation::clamp($arguments[0], $arguments[1] ?? null, $arguments[2] ?? null),
+ default => throw new \UnexpectedValueException(sprintf('Unknown calculation name "%s".', $node->getName())),
+ };
+ } catch (SassScriptException $e) {
+ // The simplification logic in the SassCalculation static methods will
+ // throw an error if the arguments aren't compatible, but we have access
+ // to the original spans so we can throw a more informative error.
+ if (str_contains($e->getMessage(), 'compatible')) {
+ $this->verifyCompatibleNumbers($arguments, $node->getArguments()->getPositional());
+ }
+
+ throw $this->exception($e->getMessage(), $node->getSpan(), $e);
+ } finally {
+ $this->callableNode = $oldCallableNode;
+ }
+ }
+
+ private function checkCalculationArguments(FunctionExpression $node): void
+ {
+ $check = function (?int $maxArgs = null) use ($node) {
+ if ($node->getArguments()->getPositional() === []) {
+ throw $this->exception('Missing argument.', $node->getSpan());
+ }
+
+ if ($maxArgs !== null && \count($node->getArguments()->getPositional()) > $maxArgs) {
+ throw $this->exception(sprintf(
+ 'Only %d %s allowed, but %d %s passed.',
+ $maxArgs,
+ StringUtil::pluralize('argument', $maxArgs),
+ \count($node->getArguments()->getPositional()),
+ StringUtil::pluralize('was', \count($node->getArguments()->getPositional()), 'were')
+ ), $node->getSpan());
+ }
+ };
+
+ switch (strtolower($node->getName())) {
+ case 'calc':
+ case 'sqrt':
+ case 'sin':
+ case 'cos':
+ case 'tan':
+ case 'asin':
+ case 'acos':
+ case 'atan':
+ case 'abs':
+ case 'exp':
+ case 'sign':
+ $check(1);
+ break;
+ case 'min':
+ case 'max':
+ case 'hypot':
+ $check();
+ break;
+ case 'pow':
+ case 'atan2':
+ case 'log':
+ case 'mod':
+ case 'rem':
+ $check(2);
+ break;
+ case 'round':
+ case 'clamp':
+ $check(3);
+ break;
+ default:
+ throw new \UnexpectedValueException(sprintf('Unknown calculation name "%s".', $node->getName()));
+ }
+ }
+
+ /**
+ * Verifies that $args all have compatible units that can be used for CSS
+ * calculations, and throws a {@see SassException} if not.
+ *
+ * The $nodesWithSpans should correspond to the spans for $args.
+ *
+ * @param object[] $args
+ * @param AstNode[] $nodesWithSpans
+ *
+ * @throws SassException
+ */
+ private function verifyCompatibleNumbers(array $args, array $nodesWithSpans): void
+ {
+ for ($i = 0; $i < \count($args); $i++) {
+ $arg = $args[$i];
+ if ($arg instanceof SassNumber && $arg->hasComplexUnits()) {
+ throw $this->exception("Number $arg isn't compatible with CSS calculations.", $nodesWithSpans[$i]->getSpan());
+ }
+ }
+
+ 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 MultiSpanSassRuntimeException(
+ "$number1 and $number2 are incompatible.",
+ $nodesWithSpans[$i]->getSpan(),
+ (string) $number1,
+ [(string) $number2 => $nodesWithSpans[$j]->getSpan()],
+ $this->stackTrace($nodesWithSpans[$i]->getSpan())
+ );
+ }
+ }
+ }
+
+ /**
+ * Evaluates $node as a component of a calculation.
+ *
+ * If $inLegacySassFunction is `true`, this allows unitless numbers to be added and
+ * subtracted with numbers with units, for backwards-compatibility with the
+ * old global `min()`, `max()`, `round()` and `abs()` functions.
+ *
+ * @return SassNumber|CalculationOperation|SassString|SassCalculation|Value
+ */
+ private function visitCalculationExpression(Expression $node, bool $inLegacySassFunction): object
+ {
+ if ($node instanceof ParenthesizedExpression) {
+ $result = $this->visitCalculationExpression($node->getExpression(), $inLegacySassFunction);
+
+ return $result instanceof SassString
+ ? new SassString('(' . $result->getText() . ')', false)
+ : $result;
+ }
+
+ if ($node instanceof StringExpression) {
+ if (!$node->accept(new IsCalculationSafeVisitor())) {
+ throw $this->exception("This expression can't be used in a calculation.", $node->getSpan());
+ }
+
+ assert(!$node->hasQuotes());
+
+ $text = $node->getText()->getAsPlain();
+ if ($text === null) {
+ return new SassString($this->performInterpolation($node->getText()), false);
+ }
+
+ return match (strtolower($text)) {
+ 'pi' => SassNumber::create(M_PI),
+ 'e' => SassNumber::create(M_E),
+ 'infinity' => SassNumber::create(INF),
+ '-infinity' => SassNumber::create(-INF),
+ 'nan' => SassNumber::create(NAN),
+ default => new SassString($text, false),
+ };
+ }
+
+ if ($node instanceof BinaryOperationExpression) {
+ $this->checkWhitespaceAroundCalculationOperator($node);
+ return $this->addExceptionSpan($node, function () use ($node, $inLegacySassFunction) {
+ return SassCalculation::operateInternal(
+ $this->binaryOperatorToCalculationOperator($node->getOperator(), $node),
+ $this->visitCalculationExpression($node->getLeft(), $inLegacySassFunction),
+ $this->visitCalculationExpression($node->getRight(), $inLegacySassFunction),
+ $inLegacySassFunction,
+ !$this->inSupportsDeclaration
+ );
+ });
+ }
+
+ if ($node instanceof NumberExpression || $node instanceof VariableExpression || $node instanceof FunctionExpression || $node instanceof IfExpression) {
+ $result = $node->accept($this);
+
+ if ($result instanceof SassNumber || $result instanceof SassCalculation) {
+ return $result;
+ }
+
+ if ($result instanceof SassString && !$result->hasQuotes()) {
+ return $result;
+ }
+
+ throw $this->exception("Value $result can't be used in a calculation.", $node->getSpan());
+ }
+
+ if ($node instanceof ListExpression && !$node->hasBrackets() && $node->getSeparator() === ListSeparator::SPACE && \count($node->getContents()) > 1) {
+ $elements = [];
+ foreach ($node->getContents() as $element) {
+ $elements[] = $this->visitCalculationExpression($element, $inLegacySassFunction);
+ }
+
+ $this->checkAdjacentCalculationValues($elements, $node);
+
+ foreach ($elements as $i => $element) {
+ if ($element instanceof CalculationOperation && $node->getContents()[$i] instanceof ParenthesizedExpression) {
+ $elements[$i] = new SassString("($element)", false);
+ }
+ }
+
+ return new SassString(implode(' ', $elements), false);
+ }
+
+ \assert(!$node->accept(new IsCalculationSafeVisitor()));
+
+ throw $this->exception("This expression can't be used in a calculation.", $node->getSpan());
+ }
+
+ /**
+ * Throws an error if $node requires whitespace around its operator in a
+ * calculation but doesn't have it.
+ */
+ private function checkWhitespaceAroundCalculationOperator(BinaryOperationExpression $node): void
+ {
+ if ($node->getOperator() !== BinaryOperator::PLUS && $node->getOperator() !== BinaryOperator::MINUS) {
+ return;
+ }
+
+ // We _should_ never be able to violate these conditions since we always
+ // parse binary operations from a single file, but it's better to be safe
+ // than have this crash bizarrely.
+ if ($node->getLeft()->getSpan()->getFile() !== $node->getRight()->getSpan()->getFile()) {
+ return;
+ }
+ if ($node->getLeft()->getSpan()->getEnd()->getOffset() >= $node->getRight()->getSpan()->getStart()->getOffset()) {
+ return;
+ }
+ $textBetweenOperands = $node->getLeft()->getSpan()->getFile()->getText($node->getLeft()->getSpan()->getEnd()->getOffset(), $node->getRight()->getSpan()->getStart()->getOffset());
+
+ $first = $textBetweenOperands[0];
+ $last = $textBetweenOperands[\strlen($textBetweenOperands) - 1];
+
+ if (!(Character::isWhitespace($first) || $first === '/') || !(Character::isWhitespace($last) || $last === '/')) {
+ throw $this->exception('"+" and "-" must be surrounded by whitespace in calculations.', $node->getOperatorSpan());
+ }
+ }
+
+ /**
+ * Returns the {@see CalculationOperator} that corresponds to $operator.
+ */
+ private function binaryOperatorToCalculationOperator(BinaryOperator $operator, BinaryOperationExpression $node): CalculationOperator
+ {
+ return match ($operator) {
+ BinaryOperator::PLUS => CalculationOperator::PLUS,
+ BinaryOperator::MINUS => CalculationOperator::MINUS,
+ BinaryOperator::TIMES => CalculationOperator::TIMES,
+ BinaryOperator::DIVIDED_BY => CalculationOperator::DIVIDED_BY,
+ default => throw $this->exception("This operation can't be used in a calculation.", $node->getOperatorSpan()),
+ };
+ }
+
+ /**
+ * @param list<object> $elements
+ */
+ private function checkAdjacentCalculationValues(array $elements, ListExpression $node): void
+ {
+ \assert(\count($elements) > 1);
+
+ for ($i = 1; $i < \count($elements); $i++) {
+ $previous = $elements[$i - 1];
+ $current = $elements[$i];
+
+ if ($previous instanceof SassString || $current instanceof SassString) {
+ continue;
+ }
+
+ $previousNode = $node->getContents()[$i - 1];
+ $currentNode = $node->getContents()[$i];
+
+ if (
+ $currentNode instanceof UnaryOperationExpression && ($currentNode->getOperator() === UnaryOperator::MINUS || $currentNode->getOperator() === UnaryOperator::PLUS)
+ || $currentNode instanceof NumberExpression && $currentNode->getValue() < 0
+ ) {
+ // `calc(1 -2)` parses as a space-separated list whose second value is a
+ // unary operator or a negative number, but just saying it's an invalid
+ // expression doesn't help the user understand what's going wrong. We
+ // add special case error handling to help clarify the issue.
+ throw $this->exception('"+" and "-" must be surrounded by whitespace in calculations.', $currentNode->getSpan()->subspan(0, 1));
+ }
+
+ throw $this->exception('Missing math operator.', $previousNode->getSpan()->expand($currentNode->getSpan()));
+ }
+ }
+
+ public function visitInterpolatedFunctionExpression(InterpolatedFunctionExpression $node): Value
+ {
+ $function = new PlainCssCallable($this->performInterpolation($node->getName()));
+
+ $oldInFunction = $this->inFunction;
+ $this->inFunction = true;
+ $result = $this->addErrorSpan($node, function () use ($function, $node) {
+ return $this->runFunctionCallable($node->getArguments(), $function, $node);
+ });
+ $this->inFunction = $oldInFunction;
+
+ return $result;
+ }
+
+ /**
+ * @template V of Value|null
+ *
+ * @param callable(): V $run
+ *
+ * @return V
+ *
+ * @param-immediately-invoked-callable $run
+ */
+ private function runUserDefinedCallable(ArgumentInvocation $arguments, UserDefinedCallable $callable, AstNode $nodeWithSpan, callable $run): ?Value
+ {
+ $evaluated = $this->evaluateArguments($arguments);
+ $name = $callable->getName();
+
+ if ($name !== '@content') {
+ $name .= '()';
+ }
+
+ $oldCallable = $this->currentCallable;
+ $this->currentCallable = $callable;
+
+ $result = $this->withStackFrame($name, $nodeWithSpan, function () use ($callable, $evaluated, $nodeWithSpan, $run) {
+ // Add an extra closure() call so that modifications to the environment
+ // don't affect the underlying environment closure.
+ return $this->withEnvironment($callable->getEnvironment()->closure(), function () use ($callable, $evaluated, $nodeWithSpan, $run) {
+ return $this->environment->scope(function () use ($callable, $evaluated, $nodeWithSpan, $run) {
+ $this->verifyArguments(\count($evaluated->getPositional()), $evaluated->getNamed(), $callable->getDeclaration()->getArguments(), $nodeWithSpan);
+
+ $declaredArguments = $callable->getDeclaration()->getArguments()->getArguments();
+ $minLength = min(\count($evaluated->getPositional()), \count($declaredArguments));
+
+ for ($i = 0; $i < $minLength; $i++) {
+ $this->environment->setLocalVariable($declaredArguments[$i]->getName(), $evaluated->getPositional()[$i], $evaluated->getPositionalNodes()[$i]);
+ }
+
+ $named = $evaluated->getNamed();
+ $namedNodes = $evaluated->getNamedNodes();
+
+ for ($i = \count($evaluated->getPositional()); $i < \count($declaredArguments); $i++) {
+ $argument = $declaredArguments[$i];
+
+ if (isset($named[$argument->getName()])) {
+ $value = $named[$argument->getName()];
+ unset($named[$argument->getName()]);
+ $nodeForSpan = $namedNodes[$argument->getName()];
+ } else {
+ assert($argument->getDefaultValue() !== null);
+ $value = $this->withoutSlash($argument->getDefaultValue()->accept($this), $this->expressionNode($argument->getDefaultValue()));
+ $nodeForSpan = $this->expressionNode($argument->getDefaultValue());
+ }
+
+ $this->environment->setLocalVariable($argument->getName(), $value, $nodeForSpan);
+ }
+
+ $argumentList = null;
+ $restArgument = $callable->getDeclaration()->getArguments()->getRestArgument();
+ if ($restArgument !== null) {
+ $rest = array_values(array_slice($evaluated->getPositional(), \count($declaredArguments)));
+ $argumentList = new SassArgumentList($rest, $named, $evaluated->getSeparator() === ListSeparator::UNDECIDED ? ListSeparator::COMMA : $evaluated->getSeparator());
+ $this->environment->setLocalVariable($restArgument, $argumentList, $nodeWithSpan);
+ }
+
+ $result = $run();
+
+ if ($argumentList === null) {
+ return $result;
+ }
+ if ($named === []) {
+ return $result;
+ }
+ if ($argumentList->wereKeywordAccessed()) {
+ return $result;
+ }
+
+ $unknownNames = array_keys($named);
+ $lastName = array_pop($unknownNames);
+ $message = sprintf(
+ 'No argument%s named $%s%s.',
+ $unknownNames ? 's' : '',
+ $unknownNames ? implode(', $', $unknownNames) . ' or $' : '',
+ $lastName
+ );
+
+ throw new MultiSpanSassRuntimeException(
+ $message,
+ $nodeWithSpan->getSpan(),
+ 'invocation',
+ ['declaration' => $callable->getDeclaration()->getArguments()->getSpanWithName()],
+ $this->stackTrace($nodeWithSpan->getSpan())
+ );
+ });
+ });
+ });
+
+ $this->currentCallable = $oldCallable;
+
+ return $result;
+ }
+
+ private function runFunctionCallable(ArgumentInvocation $arguments, ?SassCallable $callable, AstNode $nodeWithSpan): Value
+ {
+ if ($callable instanceof BuiltInCallable) {
+ return $this->withoutSlash($this->runBuiltInCallable($arguments, $callable, $nodeWithSpan), $nodeWithSpan);
+ }
+
+ if ($callable instanceof UserDefinedCallable) {
+ return $this->runUserDefinedCallable($arguments, $callable, $nodeWithSpan, function () use ($callable) {
+ foreach ($callable->getDeclaration()->getChildren() as $statement) {
+ $returnValue = $statement->accept($this);
+
+ if ($returnValue instanceof Value) {
+ return $returnValue;
+ }
+ }
+
+ throw $this->exception('Function finished without @return.', $callable->getDeclaration()->getSpan());
+ });
+ }
+
+ if ($callable instanceof PlainCssCallable) {
+ if (\count($arguments->getNamed()) > 0 || $arguments->getKeywordRest() !== null) {
+ throw $this->exception("Plain CSS functions don't support keyword arguments.", $nodeWithSpan->getSpan());
+ }
+
+ $buffer = $callable->getName() . '(';
+
+ try {
+ $first = true;
+
+ foreach ($arguments->getPositional() as $argument) {
+ if ($first) {
+ $first = false;
+ } else {
+ $buffer .= ', ';
+ }
+
+ $buffer .= $this->evaluateToCss($argument);
+ }
+
+ $restArg = $arguments->getRest();
+ if ($restArg !== null) {
+ $rest = $restArg->accept($this);
+ if (!$first) {
+ $buffer .= ', ';
+ }
+
+ $buffer .= $this->serialize($rest, $restArg);
+ }
+ } catch (SassRuntimeException $e) {
+ if (!str_ends_with($e->getOriginalMessage(), "isn't a valid CSS value.")) {
+ throw $e;
+ }
+
+ throw new MultiSpanSassRuntimeException(
+ $e->getOriginalMessage(),
+ $e->getSpan(),
+ 'value',
+ ['unknown function treated as plain CSS' => $nodeWithSpan->getSpan()],
+ $e->getSassTrace()
+ );
+ }
+
+ $buffer .= ')';
+
+ return new SassString($buffer, false);
+ }
+
+ throw new \InvalidArgumentException('Unknown callable type ' . (\is_object($callable) ? get_class($callable) : gettype($callable) ) . '.');
+ }
+
+ private function runBuiltInCallable(ArgumentInvocation $arguments, BuiltInCallable $callable, AstNode $nodeWithSpan): Value
+ {
+ $evaluated = $this->evaluateArguments($arguments);
+ $oldCallableNode = $this->callableNode;
+ $this->callableNode = $nodeWithSpan;
+
+ /** @var ArgumentDeclaration $overload */
+ [$overload, $callback] = $callable->callbackFor(\count($evaluated->getPositional()), $evaluated->getNamed());
+
+ $this->addExceptionSpan($nodeWithSpan, function () use ($overload, $evaluated) {
+ $overload->verify(\count($evaluated->getPositional()), $evaluated->getNamed());
+ });
+
+ $declaredArguments = $overload->getArguments();
+
+ $positional = $evaluated->getPositional();
+ $named = $evaluated->getNamed();
+
+ for ($i = \count($positional); $i < \count($declaredArguments); $i++) {
+ $argument = $declaredArguments[$i];
+
+ if (isset($named[$argument->getName()])) {
+ $positional[] = $named[$argument->getName()];
+ unset($named[$argument->getName()]);
+ } else {
+ assert($argument->getDefaultValue() !== null);
+ $positional[] = $this->withoutSlash($argument->getDefaultValue()->accept($this), $argument->getDefaultValue());
+ }
+ }
+
+ $argumentList = null;
+ if ($overload->getRestArgument() !== null) {
+ $rest = array_values(array_splice($positional, \count($declaredArguments)));
+ \assert(array_is_list($positional));
+ $argumentList = new SassArgumentList($rest, $named, $evaluated->getSeparator() === ListSeparator::UNDECIDED ? ListSeparator::COMMA : $evaluated->getSeparator());
+ $positional[] = $argumentList;
+ }
+
+ try {
+ $result = $this->addExceptionSpan($nodeWithSpan, function () use ($callback, $positional) {
+ return $callback($positional);
+ });
+ } catch (SassException $e) {
+ throw $e;
+ } catch (\Throwable $e) {
+ throw $this->exception($e->getMessage(), $nodeWithSpan->getSpan(), $e);
+ }
+
+ $this->callableNode = $oldCallableNode;
+
+ if ($argumentList === null) {
+ return $result;
+ }
+ if ($named === []) {
+ return $result;
+ }
+ if ($argumentList->wereKeywordAccessed()) {
+ return $result;
+ }
+
+ $unknownNames = array_keys($named);
+ $lastName = array_pop($unknownNames);
+ $message = sprintf(
+ 'No argument%s named $%s%s.',
+ $unknownNames ? 's' : '',
+ $unknownNames ? implode(', $', $unknownNames) . ' or $' : '',
+ $lastName
+ );
+
+ throw new MultiSpanSassRuntimeException(
+ $message,
+ $nodeWithSpan->getSpan(),
+ 'invocation',
+ ['declaration' => $overload->getSpanWithName()],
+ $this->stackTrace($nodeWithSpan->getSpan())
+ );
+ }
+
+ private function evaluateArguments(ArgumentInvocation $arguments): ArgumentResults
+ {
+ $positional = [];
+ $positionalNodes = [];
+
+ foreach ($arguments->getPositional() as $expression) {
+ $nodeForSpan = $this->expressionNode($expression);
+ $positional[] = $this->withoutSlash($expression->accept($this), $nodeForSpan);
+ $positionalNodes[] = $nodeForSpan;
+ }
+
+ $named = [];
+ $namedNodes = [];
+
+ foreach ($arguments->getNamed() as $key => $value) {
+ $nodeForSpan = $this->expressionNode($value);
+ $named[$key] = $this->withoutSlash($value->accept($this), $nodeForSpan);
+ $namedNodes[$key] = $nodeForSpan;
+ }
+
+ $restArgs = $arguments->getRest();
+ if ($restArgs === null) {
+ return new ArgumentResults($positional, $positionalNodes, $named, $namedNodes, ListSeparator::UNDECIDED);
+ }
+
+ $rest = $restArgs->accept($this);
+ $restNodeForSpan = $this->expressionNode($restArgs);
+ $separator = ListSeparator::UNDECIDED;
+
+ if ($rest instanceof SassMap) {
+ $this->addRestMap($named, $rest, $restArgs, fn($value) => $value);
+ foreach ($rest->getContents() as $key => $_) {
+ assert($key instanceof SassString);
+ $namedNodes[$key->getText()] = $restNodeForSpan;
+ }
+ } elseif ($rest instanceof SassList) {
+ foreach ($rest->asList() as $value) {
+ $positional[] = $this->withoutSlash($value, $restNodeForSpan);
+ $positionalNodes[] = $restNodeForSpan;
+ $separator = $rest->getSeparator();
+ }
+
+ if ($rest instanceof SassArgumentList) {
+ foreach ($rest->getKeywords() as $key => $value) {
+ $named[$key] = $this->withoutSlash($value, $restNodeForSpan);
+ $namedNodes[$key] = $restNodeForSpan;
+ }
+ }
+ } else {
+ $positional[] = $this->withoutSlash($rest, $restNodeForSpan);
+ $positionalNodes[] = $restNodeForSpan;
+ }
+
+ $keywordRestArgs = $arguments->getKeywordRest();
+ if ($keywordRestArgs === null) {
+ return new ArgumentResults($positional, $positionalNodes, $named, $namedNodes, $separator);
+ }
+
+ $keywordRest = $keywordRestArgs->accept($this);
+ $keywordRestNodeForSpan = $this->expressionNode($keywordRestArgs);
+
+ if ($keywordRest instanceof SassMap) {
+ $this->addRestMap($named, $keywordRest, $keywordRestArgs, fn($value) => $value);
+ foreach ($keywordRest->getContents() as $key => $_) {
+ assert($key instanceof SassString);
+ $namedNodes[$key->getText()] = $keywordRestNodeForSpan;
+ }
+
+ return new ArgumentResults($positional, $positionalNodes, $named, $namedNodes, $separator);
+ }
+
+ throw $this->exception("Variable keyword arguments must be a map (was $keywordRest).", $keywordRestArgs->getSpan());
+ }
+
+ /**
+ * Evaluates the arguments in [arguments] only as much as necessary to
+ * separate out positional and named arguments.
+ *
+ * Returns the arguments as expressions so that they can be lazily evaluated
+ * for macros such as `if()`.
+ *
+ * @return array{list<Expression>, array<string, Expression>}
+ */
+ private function evaluateMacroArguments(CallableInvocation $invocation): array
+ {
+ $restArgs = $invocation->getArguments()->getRest();
+ if ($restArgs === null) {
+ return [$invocation->getArguments()->getPositional(), $invocation->getArguments()->getNamed()];
+ }
+
+ $positional = $invocation->getArguments()->getPositional();
+ $named = $invocation->getArguments()->getNamed();
+ $rest = $restArgs->accept($this);
+ $restNodeForSpan = $this->expressionNode($restArgs);
+
+ if ($rest instanceof SassMap) {
+ $this->addRestMap($named, $rest, $restArgs, function ($value) use ($restArgs) {
+ return new ValueExpression($value, $restArgs->getSpan());
+ });
+ } elseif ($rest instanceof SassList) {
+ foreach ($rest->asList() as $value) {
+ $positional[] = new ValueExpression($this->withoutSlash($value, $restNodeForSpan), $restArgs->getSpan());
+ }
+
+ if ($rest instanceof SassArgumentList) {
+ foreach ($rest->getKeywords() as $key => $value) {
+ $named[$key] = new ValueExpression($this->withoutSlash($value, $restNodeForSpan), $restArgs->getSpan());
+ }
+ }
+ } else {
+ $positional[] = new ValueExpression($this->withoutSlash($rest, $restNodeForSpan), $restArgs->getSpan());
+ }
+
+ $keywordRestArgs = $invocation->getArguments()->getKeywordRest();
+ if ($keywordRestArgs === null) {
+ return [$positional, $named];
+ }
+
+ $keywordRest = $keywordRestArgs->accept($this);
+ $keywordRestNodeForSpan = $this->expressionNode($keywordRestArgs);
+
+ if ($keywordRest instanceof SassMap) {
+ $this->addRestMap($named, $keywordRest, $keywordRestArgs, function ($value) use ($keywordRestArgs, $keywordRestNodeForSpan) {
+ return new ValueExpression($this->withoutSlash($value, $keywordRestNodeForSpan), $keywordRestArgs->getSpan());
+ });
+
+ return [$positional, $named];
+ }
+
+ throw $this->exception("Variable keyword arguments must be a map (was $keywordRest).", $keywordRestArgs->getSpan());
+ }
+
+ /**
+ * Adds the values in $map to $values.
+ *
+ * Throws a {@see SassRuntimeException} associated with $nodeWithSpan's source
+ * span if any $map keys aren't strings.
+ *
+ * @template T
+ *
+ * @param array<string, T> $values
+ * @param callable(Value): T $convert
+ *
+ * @param-immediately-invoked-callable $convert
+ */
+ private function addRestMap(array &$values, SassMap $map, AstNode $nodeWithSpan, callable $convert): void
+ {
+ $expressionNode = $this->expressionNode($nodeWithSpan);
+
+ foreach ($map->getContents() as $key => $value) {
+ if ($key instanceof SassString) {
+ $values[$key->getText()] = $convert($this->withoutSlash($value, $expressionNode));
+ } else {
+ throw $this->exception("Variable keyword argument map must have string keys.\n$key is not a string in $map.", $nodeWithSpan->getSpan());
+ }
+ }
+ }
+
+ /**
+ * @param array<string, mixed> $named
+ *
+ * @throws SassRuntimeException if $positional and $named aren't valid when applied to $arguments.
+ */
+ private function verifyArguments(int $positional, array $named, ArgumentDeclaration $arguments, AstNode $nodeWithSpan): void
+ {
+ $this->addExceptionSpan($nodeWithSpan, function () use ($positional, $named, $arguments) {
+ $arguments->verify($positional, $named);
+ });
+ }
+
+ public function visitSelectorExpression(SelectorExpression $node): Value
+ {
+ if ($this->styleRuleIgnoringAtRoot === null) {
+ return SassNull::create();
+ }
+
+ return $this->styleRuleIgnoringAtRoot->getOriginalSelector()->asSassList();
+ }
+
+ public function visitStringExpression(StringExpression $node): Value
+ {
+ // Don't use [performInterpolation] here because we need to get the raw text
+ // from strings, rather than the semantic value.
+ $oldInSupportsDeclaration = $this->inSupportsDeclaration;
+ $this->inSupportsDeclaration = false;
+
+ $result = new SassString(implode('', array_map(function ($value) {
+ if (\is_string($value)) {
+ return $value;
+ }
+
+ $expression = $value;
+ $result = $expression->accept($this);
+
+ if ($result instanceof SassString) {
+ return $result->getText();
+ }
+
+ return $this->serialize($result, $expression, false);
+ }, $node->getText()->getContents())), $node->hasQuotes());
+
+ $this->inSupportsDeclaration = $oldInSupportsDeclaration;
+
+ return $result;
+ }
+
+ public function visitSupportsExpression(SupportsExpression $node): Value
+ {
+ return new SassString($this->visitSupportsCondition($node->getCondition()), false);
+ }
+
+ /**
+ * Runs $callback for each value in $list until it returns a {@see Value}.
+ *
+ * Returns the value returned by $callback, or `null` if it only ever
+ * returned `null`.
+ *
+ * @template T
+ *
+ * @param T[] $list
+ * @param callable(T): ?Value $callback
+ *
+ * @param-immediately-invoked-callable $callback
+ */
+ private function handleReturn(array $list, callable $callback): ?Value
+ {
+ foreach ($list as $value) {
+ $result = $callback($value);
+
+ if ($result !== null) {
+ return $result;
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Runs $callback with $environment as the current environment.
+ *
+ * @template T
+ *
+ * @param Environment $environment
+ * @param callable(): T $callback
+ *
+ * @return T
+ *
+ * @param-immediately-invoked-callable $callback
+ */
+ private function withEnvironment(Environment $environment, callable $callback)
+ {
+ $oldEnvironment = $this->environment;
+ $this->environment = $environment;
+ $result = $callback();
+ $this->environment = $oldEnvironment;
+
+ return $result;
+ }
+
+ /**
+ * @return CssValue<string>
+ */
+ private function interpolationToValue(Interpolation $interpolation, bool $warnForColor = false, bool $trim = false): CssValue
+ {
+ $result = $this->performInterpolation($interpolation, $warnForColor);
+
+ return new CssValue($trim ? StringUtil::trimAscii($result, true) : $result, $interpolation->getSpan());
+ }
+
+ /**
+ * Evaluates $interpolation.
+ *
+ * If $warnForColor is `true`, this will emit a warning for any named color
+ * values passed into the interpolation.
+ */
+ private function performInterpolation(Interpolation $interpolation, bool $warnForColor = false): string
+ {
+ $tuple = $this->performInterpolationHelper($interpolation, false, $warnForColor);
+
+ return $tuple[0];
+ }
+
+ /**
+ * Like {@see performInterpolation}, but also returns a {@see InterpolationMap} that
+ * can map spans from the resulting string back to the original
+ * $interpolation.
+ *
+ * @return array{string, InterpolationMap}
+ */
+ private function performInterpolationWithMap(Interpolation $interpolation, bool $warnForColor = false): array
+ {
+ $tuple = $this->performInterpolationHelper($interpolation, true, $warnForColor);
+ \assert($tuple[1] !== null);
+
+ return $tuple;
+ }
+
+ /**
+ * A helper that implements the core logic of both {@see performInterpolation}
+ * and {@see performInterpolationWithMap}.
+ *
+ * @return array{string, InterpolationMap|null}
+ */
+ private function performInterpolationHelper(Interpolation $interpolation, bool $sourceMap, bool $warnForColor = false): array
+ {
+ $targetLocations = $sourceMap ? [] : null;
+
+ $oldInSupportsDeclaration = $this->inSupportsDeclaration;
+ $this->inSupportsDeclaration = false;
+
+ $buffer = '';
+ $first = true;
+
+ foreach ($interpolation->getContents() as $value) {
+ if (!$first && $targetLocations !== null) {
+ $targetLocations[] = new SimpleSourceLocation(\strlen($buffer));
+ }
+
+ $first = false;
+
+ if (\is_string($value)) {
+ $buffer .= $value;
+ continue;
+ }
+
+ $expression = $value;
+ $result = $expression->accept($this);
+
+ if ($warnForColor && $result instanceof SassColor && null !== $colorName = Colors::RGBaToColorName($result->getRed(), $result->getGreen(), $result->getBlue(), $result->getAlpha())) {
+ $alternative = new BinaryOperationExpression(
+ BinaryOperator::PLUS,
+ new StringExpression(new Interpolation([''], $interpolation->getSpan()), true),
+ $expression
+ );
+ $this->warn("You probably don't mean to use the color value $colorName in interpolation here.\nIt may end up represented as $result, which will likely produce invalid CSS.\nAlways quote color names when using them as strings or map keys (for example, \"$colorName\").\nIf you really want to use the color value here, use '$alternative'.", $expression->getSpan());
+ }
+
+ $buffer .= $this->serialize($result, $expression, false);
+ }
+
+ $this->inSupportsDeclaration = $oldInSupportsDeclaration;
+
+ return [$buffer, $targetLocations === null ? null : new InterpolationMap($interpolation, $targetLocations)];
+ }
+
+ /**
+ * Evaluates $expression and calls `toCssString()` and wraps a
+ * {@see SassScriptException} to associate it with its span.
+ */
+ private function evaluateToCss(Expression $expression, bool $quote = true): string
+ {
+ return $this->serialize($expression->accept($this), $expression, $quote);
+ }
+
+ /**
+ * Calls `value->toCssString()` and wraps a {@see SassScriptException} to associate
+ * it with $nodeWithSpan's source span.
+ *
+ * This takes an {@see AstNode} rather than a {@see FileSpan} so it can avoid calling
+ * {@see AstNode::getSpan} if the span isn't required, since some nodes need to do
+ * real work to manufacture a source span.
+ */
+ private function serialize(Value $value, AstNode $nodeWithSpan, bool $quote = true): string
+ {
+ return $this->addExceptionSpan($nodeWithSpan, function () use ($value, $quote) {
+ return $value->toCssString($quote);
+ });
+ }
+
+ /**
+ * Runs $callback with $rule as the current style rule.
+ *
+ * @template T
+ *
+ * @param ModifiableCssStyleRule $rule
+ * @param callable(): T $callback
+ *
+ * @return T
+ *
+ * @param-immediately-invoked-callable $callback
+ */
+ private function withStyleRule(ModifiableCssStyleRule $rule, callable $callback)
+ {
+ $oldRule = $this->styleRuleIgnoringAtRoot;
+ $this->styleRuleIgnoringAtRoot = $rule;
+ $result = $callback();
+ $this->styleRuleIgnoringAtRoot = $oldRule;
+
+ return $result;
+ }
+
+ /**
+ * Runs $callback with $queries as the current media queries.
+ *
+ * This also sets $sources as the current set of media queries that were
+ * merged together to create $queries. This is used to determine when it's
+ * safe to bubble one query through another.
+ *
+ * @template T
+ *
+ * @param list<CssMediaQuery>|null $queries
+ * @param CssMediaQuery[]|null $sources
+ * @param callable(): T $callback
+ *
+ * @return T
+ *
+ * @param-immediately-invoked-callable $callback
+ */
+ private function withMediaQueries(?array $queries, ?array $sources, callable $callback)
+ {
+ $oldMediaQueries = $this->mediaQueries;
+ $oldSources = $this->mediaQuerySources;
+ $this->mediaQueries = $queries;
+ $this->mediaQuerySources = $sources;
+ $result = $callback();
+ $this->mediaQueries = $oldMediaQueries;
+ $this->mediaQuerySources = $oldSources;
+
+ return $result;
+ }
+
+ /**
+ * Returns the {@see AstNode} whose span should be used for $expression.
+ *
+ * If $expression is a variable reference, {@see AstNode}'s span will be the span
+ * where that variable was originally declared. Otherwise, this will just
+ * return $expression.
+ */
+ private function expressionNode(AstNode $expression): AstNode
+ {
+ if ($expression instanceof VariableExpression) {
+ return $this->addExceptionSpan($expression, function () use ($expression) {
+ return $this->environment->getVariableNode($expression->getName()) ?? $expression;
+ });
+ }
+
+ return $expression;
+ }
+
+ /**
+ * Adds $node as a child of the current parent, then runs $callback with
+ * $node as the current parent.
+ *
+ * If $through is passed, $node is added as a child of the first parent for
+ * which $through returns `false`. That parent is copied unless it's the
+ * lattermost child of its parent.
+ *
+ * Runs $callback in a new environment scope unless $scopeWhen is false.
+ *
+ * @template S of ModifiableCssParentNode
+ * @template T
+ *
+ * @param S $node
+ * @param callable(): T $callback
+ * @param null|callable(CssNode): bool $through
+ * @param bool $scopeWhen
+ *
+ * @return T
+ *
+ * @param-immediately-invoked-callable $callback
+ * @param-immediately-invoked-callable $through
+ */
+ private function withParent(ModifiableCssParentNode $node, callable $callback, ?callable $through = null, bool $scopeWhen = true)
+ {
+ $this->addChild($node, $through);
+
+ $oldParent = $this->parent;
+ $this->parent = $node;
+ $result = $this->environment->scope($callback, $scopeWhen);
+ $this->parent = $oldParent;
+
+ return $result;
+ }
+
+ /**
+ * Adds $node as a child of the current parent.
+ *
+ * If $through is passed, $node is added as a child of the first parent for
+ * which $through returns `false` instead. That parent is copied unless it's the
+ * lattermost child of its parent.
+ *
+ * @param null|callable(CssNode): bool $through
+ *
+ * @param-immediately-invoked-callable $through
+ */
+ private function addChild(ModifiableCssNode $node, ?callable $through = null): void
+ {
+ // Go up through parents that match [through].
+ $parent = $this->getParent();
+ if ($through !== null) {
+ while ($through($parent)) {
+ $grandParent = $parent->getParent();
+
+ if ($grandParent === null) {
+ throw new \InvalidArgumentException('$through() must return false for at least one parent of the node.');
+ }
+
+ $parent = $grandParent;
+ }
+ }
+
+ // If the parent has a (visible) following sibling, we shouldn't add to
+ // the parent. Instead, we should create a copy and add it after the
+ // interstitial sibling.
+ if ($parent->hasFollowingSibling()) {
+ $grandParent = $parent->getParent();
+ // A node with siblings must have a parent
+ assert($grandParent !== null);
+ $lastChild = ListUtil::last($grandParent->getChildren());
+ if ($parent->equalsIgnoringChildren($lastChild)) {
+ \assert($lastChild instanceof ModifiableCssParentNode);
+ $parent = $lastChild;
+ } else {
+ $parent = $parent->copyWithoutChildren();
+ $grandParent->addChild($parent);
+ }
+ }
+
+ $parent->addChild($node);
+ }
+
+ /**
+ * Adds a frame to the stack with the given $member name, and $nodeWithSpan
+ * as the site of the new frame.
+ *
+ * Runs $callback with the new stack.
+ *
+ * This takes an {@see AstNode} rather than a {@see FileSpan} so it can avoid calling
+ * {@see AstNode::getSpan} if the span isn't required, since some nodes need to do
+ * real work to manufacture a source span.
+ *
+ * @template T
+ *
+ * @param callable(): T $callback
+ *
+ * @return T
+ *
+ * @param-immediately-invoked-callable $callback
+ */
+ private function withStackFrame(string $member, AstNode $nodeWithSpan, callable $callback)
+ {
+ $this->stack[] = [$this->member, $nodeWithSpan];
+ $oldMember = $this->member;
+ $this->member = $member;
+ $result = $callback();
+ $this->member = $oldMember;
+ array_pop($this->stack);
+
+ return $result;
+ }
+
+ /**
+ * Like {@see Value::withoutSlash}, but produces a deprecation warning if $value
+ * was a slash-separated number.
+ */
+ private function withoutSlash(Value $value, AstNode $nodeForSpan): Value
+ {
+ if ($value instanceof SassNumber && $value->getAsSlash() !== null) {
+ $recommendation = function (SassNumber $number) use (&$recommendation): string {
+ if ($number->getAsSlash() !== null) {
+ [$before, $after] = $number->getAsSlash();
+ return "math.div({$recommendation($before)}, {$recommendation($after)})";
+ }
+
+ return (string) $number;
+ };
+
+ $message = <<<WARNING
+Using / for division is deprecated and will be removed in Dart Sass 2.0.0.
+
+Recommendation: {$recommendation($value)}
+
+More info and automated migrator: https://sass-lang.com/d/slash-div
+WARNING;
+ $this->warn($message, $nodeForSpan->getSpan(), Deprecation::slashDiv);
+ }
+
+ return $value->withoutSlash();
+ }
+
+ /**
+ * Creates a new stack frame with location information from $member$ and
+ * $span.
+ */
+ private function stackFrame(string $member, FileSpan $span): Frame
+ {
+ $url = $span->getSourceUrl();
+
+ if ($url !== null) {
+ $url = $this->importCache->humanize($url);
+ }
+
+ return Util::frameForSpan($span, $member, $url);
+ }
+
+ /**
+ * Returns a stack trace at the current point.
+ *
+ * If $span is passed, it's used for the innermost stack frame.
+ */
+ private function stackTrace(?FileSpan $span = null): Trace
+ {
+ $frames = [];
+
+ foreach ($this->stack as [$member, $nodeWithSpan]) {
+ $frames[] = $this->stackFrame($member, $nodeWithSpan->getSpan());
+ }
+
+ if ($span !== null) {
+ $frames[] = $this->stackFrame($this->member, $span);
+ }
+
+ return new Trace(array_reverse($frames));
+ }
+
+ public function warn(string $message, FileSpan $span, ?Deprecation $deprecation = null): void
+ {
+ if ($this->quietDeps && ($this->inDependency || ($this->currentCallable !== null && $this->currentCallable->isInDependency()))) {
+ return;
+ }
+
+ $spanString = ($span->getSourceUrl() ?? '') . "\0" . $span->getStart()->getOffset() . "\0" . $span->getEnd()->getOffset();
+
+ if (isset($this->warningsEmitted[$message][$spanString])) {
+ return;
+ }
+ $this->warningsEmitted[$message][$spanString] = true;
+
+ $trace = $this->stackTrace($span);
+
+ if ($deprecation === null) {
+ $this->logger->warn($message, null, $span, $trace);
+ } else {
+ LoggerUtil::warnForDeprecation($this->logger, $deprecation, $message, $span, $trace);
+ }
+ }
+
+ /**
+ * Returns a {@see SassRuntimeException} with the given $message.
+ *
+ * If $span is passed, it's used for the innermost stack frame.
+ */
+ private function exception(string $message, ?FileSpan $span = null, ?\Throwable $previous = null): SassRuntimeException
+ {
+ return new SimpleSassRuntimeException($message, $span ?? ListUtil::last($this->stack)[1]->getSpan(), $this->stackTrace($span), $previous);
+ }
+
+ /**
+ * Returns a {@see MultiSpanSassRuntimeException} with the given $message,
+ * $primaryLabel, and $secondaryLabels.
+ *
+ * The primary span is taken from the current stack trace span.
+ *
+ * @param array<string, FileSpan> $secondarySpans
+ */
+ private function multiSpanException(string $message, string $primaryLabel, array $secondarySpans): SassRuntimeException
+ {
+ return new MultiSpanSassRuntimeException($message, ListUtil::last($this->stack)[1]->getSpan(), $primaryLabel, $secondarySpans, $this->stackTrace());
+ }
+
+ /**
+ * Runs $callback, and converts any {@see SassScriptException}s it throws to
+ * {@see SassRuntimeException}s with $nodeWithSpan's source span.
+ *
+ * This takes an {@see AstNode} rather than a {@see FileSpan} so it can avoid calling
+ * {@see AstNode::getSpan} if the span isn't required, since some nodes need to do
+ * real work to manufacture a source span.
+ *
+ * If $addStackFrame is true (the default), this will add an innermost stack
+ * frame for $nodeWithSpan. Otherwise, it will use the existing stack as-is.
+ *
+ * @template T
+ *
+ * @param callable(): T $callback
+ *
+ * @return T
+ *
+ * @throws SassRuntimeException
+ *
+ * @param-immediately-invoked-callable $callback
+ */
+ private function addExceptionSpan(AstNode $nodeWithSpan, callable $callback, bool $addStackFrame = true)
+ {
+ try {
+ return $callback();
+ } catch (SassScriptException $e) {
+ throw $e->withSpan($nodeWithSpan->getSpan())->withTrace($this->stackTrace($addStackFrame ? $nodeWithSpan->getSpan() : null), $e);
+ }
+ }
+
+ /**
+ * Runs $callback, and converts any {@see SassException}s that aren't already
+ * {@see SassRuntimeException}s to {@see SassRuntimeException}s with the current stack
+ * trace.
+ *
+ * @template T
+ *
+ * @param callable(): T $callback
+ * @return T
+ *
+ * @param-immediately-invoked-callable $callback
+ */
+ private function addExceptionTrace(callable $callback)
+ {
+ try {
+ return $callback();
+ } catch (SassRuntimeException $e) {
+ throw $e;
+ } catch (SassException $e) {
+ throw $e->withTrace($this->stackTrace($e->getSpan()), $e);
+ }
+ }
+
+ /**
+ * Runs $callback, and converts any {@see SassRuntimeException}s containing an
+ * `@error` to throw a more relevant {@see SassRuntimeException}s with $nodeWithSpan's
+ * source span.
+ *
+ * @template T
+ *
+ * @param AstNode $nodeWithSpan
+ * @param callable(): T $callback
+ *
+ * @return T
+ *
+ * @param-immediately-invoked-callable $callback
+ */
+ private function addErrorSpan(AstNode $nodeWithSpan, callable $callback)
+ {
+ try {
+ return $callback();
+ } catch (SassRuntimeException $e) {
+ if (!str_starts_with($e->getSpan()->getText(), '@error')) {
+ throw $e;
+ }
+
+ throw new SimpleSassRuntimeException($e->getOriginalMessage(), $nodeWithSpan->getSpan(), $this->stackTrace(), $e);
+ }
+ }
+}
diff --git a/vendor/scssphp/scssphp/src/Evaluation/EvaluationContext.php b/vendor/scssphp/scssphp/src/Evaluation/EvaluationContext.php
new file mode 100644
index 000000000..be765471b
--- /dev/null
+++ b/vendor/scssphp/scssphp/src/Evaluation/EvaluationContext.php
@@ -0,0 +1,82 @@
+<?php
+
+/**
+ * SCSSPHP
+ *
+ * @copyright 2012-2020 Leaf Corcoran
+ *
+ * @license http://opensource.org/licenses/MIT MIT
+ *
+ * @link http://scssphp.github.io/scssphp
+ */
+
+namespace ScssPhp\ScssPhp\Evaluation;
+
+use ScssPhp\ScssPhp\Deprecation;
+use SourceSpan\FileSpan;
+
+/**
+ * @internal
+ */
+abstract class EvaluationContext
+{
+ private static ?EvaluationContext $evaluationContext = null;
+
+ /**
+ * The current evaluation context.
+ *
+ * @throws \LogicException if there isn't a Sass stylesheet currently being
+ * evaluated.
+ */
+ public static function getCurrent(): EvaluationContext
+ {
+ if (self::$evaluationContext !== null) {
+ return self::$evaluationContext;
+ }
+
+ throw new \LogicException('No Sass stylesheet is currently being evaluated.');
+ }
+
+ /**
+ * Runs $callback with $context as {@see EvaluationContext::getCurrent()}.
+ *
+ * @template T
+ *
+ * @param callable(): T $callback
+ *
+ * @return T
+ *
+ * @param-immediately-invoked-callable $callback
+ */
+ public static function withEvaluationContext(EvaluationContext $context, callable $callback)
+ {
+ $oldContext = self::$evaluationContext;
+ self::$evaluationContext = $context;
+
+ try {
+ return $callback();
+ } finally {
+ self::$evaluationContext = $oldContext;
+ }
+ }
+
+ /**
+ * Returns the span for the currently executing callable.
+ *
+ * For normal exception reporting, this should be avoided in favor of
+ * throwing {@see SassScriptException}s. It should only be used when calling APIs
+ * that require spans.
+ *
+ * @throws \LogicException if there isn't a callable being invoked.
+ */
+ abstract public function getCurrentCallableSpan(): FileSpan;
+
+ /**
+ * Prints a warning message associated with the current `@import` or function
+ * call.
+ *
+ * If $deprecation is non-null`, the warning is emitted as a deprecation
+ * warning of that type.
+ */
+ abstract public function warn(string $message, ?Deprecation $deprecation = null): void;
+}
diff --git a/vendor/scssphp/scssphp/src/Evaluation/LoadedStylesheet.php b/vendor/scssphp/scssphp/src/Evaluation/LoadedStylesheet.php
new file mode 100644
index 000000000..5187186a3
--- /dev/null
+++ b/vendor/scssphp/scssphp/src/Evaluation/LoadedStylesheet.php
@@ -0,0 +1,61 @@
+<?php
+
+/**
+ * SCSSPHP
+ *
+ * @copyright 2012-2020 Leaf Corcoran
+ *
+ * @license http://opensource.org/licenses/MIT MIT
+ *
+ * @link http://scssphp.github.io/scssphp
+ */
+
+namespace ScssPhp\ScssPhp\Evaluation;
+
+use ScssPhp\ScssPhp\Ast\Sass\Statement\Stylesheet;
+use ScssPhp\ScssPhp\Importer\Importer;
+
+/**
+ * The result of loading a stylesheet via {@see EvaluateVisitor::loadStylesheet}.
+ *
+ * @internal
+ */
+final class LoadedStylesheet
+{
+ /**
+ * The stylesheet itself.
+ */
+ private readonly Stylesheet $stylesheet;
+
+ private readonly Importer $importer;
+
+ /**
+ * Whether this load counts as a dependency.
+ *
+ * That is, whether this was (transitively) loaded through a load path or
+ * importer rather than relative to the entrypoint.
+ */
+ private readonly bool $dependency;
+
+ public function __construct(Stylesheet $stylesheet, Importer $importer, bool $dependency)
+ {
+ $this->stylesheet = $stylesheet;
+ $this->importer = $importer;
+ $this->dependency = $dependency;
+ }
+
+ public function getStylesheet(): Stylesheet
+ {
+ return $this->stylesheet;
+ }
+
+ public function getImporter(): Importer
+ {
+ return $this->importer;
+ }
+
+ public function isDependency(): bool
+ {
+ return $this->dependency;
+ }
+}
diff --git a/vendor/scssphp/scssphp/src/Evaluation/VisitorEvaluationContext.php b/vendor/scssphp/scssphp/src/Evaluation/VisitorEvaluationContext.php
new file mode 100644
index 000000000..9dd582d23
--- /dev/null
+++ b/vendor/scssphp/scssphp/src/Evaluation/VisitorEvaluationContext.php
@@ -0,0 +1,62 @@
+<?php
+
+/**
+ * SCSSPHP
+ *
+ * @copyright 2012-2020 Leaf Corcoran
+ *
+ * @license http://opensource.org/licenses/MIT MIT
+ *
+ * @link http://scssphp.github.io/scssphp
+ */
+
+namespace ScssPhp\ScssPhp\Evaluation;
+
+use ScssPhp\ScssPhp\Ast\AstNode;
+use ScssPhp\ScssPhp\Deprecation;
+use SourceSpan\FileSpan;
+
+/**
+ * @internal
+ */
+final class VisitorEvaluationContext extends EvaluationContext
+{
+ private readonly EvaluateVisitor $visitor;
+
+ private readonly AstNode $defaultWarnNodeWithSpan;
+
+ public function __construct(EvaluateVisitor $visitor, AstNode $defaultWarnNodeWithSpan)
+ {
+ $this->visitor = $visitor;
+ $this->defaultWarnNodeWithSpan = $defaultWarnNodeWithSpan;
+ }
+
+ public function getCurrentCallableSpan(): FileSpan
+ {
+ $callableNode = $this->visitor->getCallableNode();
+
+ if ($callableNode !== null) {
+ return $callableNode->getSpan();
+ }
+
+ throw new \LogicException('No Sass callable is currently being evaluated.');
+ }
+
+ public function warn(string $message, ?Deprecation $deprecation = null): void
+ {
+ $span = $this->visitor->getImportSpan() ?? $this->maybeCurrentCallableSpan() ?? $this->defaultWarnNodeWithSpan->getSpan();
+
+ $this->visitor->warn($message, $span, $deprecation);
+ }
+
+ private function maybeCurrentCallableSpan(): ?FileSpan
+ {
+ $callableNode = $this->visitor->getCallableNode();
+
+ if ($callableNode !== null) {
+ return $callableNode->getSpan();
+ }
+
+ return null;
+ }
+}
diff --git a/vendor/scssphp/scssphp/src/Exception/MultiSpanSassException.php b/vendor/scssphp/scssphp/src/Exception/MultiSpanSassException.php
new file mode 100644
index 000000000..450f1132f
--- /dev/null
+++ b/vendor/scssphp/scssphp/src/Exception/MultiSpanSassException.php
@@ -0,0 +1,73 @@
+<?php
+
+/**
+ * SCSSPHP
+ *
+ * @copyright 2012-2020 Leaf Corcoran
+ *
+ * @license http://opensource.org/licenses/MIT MIT
+ *
+ * @link http://scssphp.github.io/scssphp
+ */
+
+namespace ScssPhp\ScssPhp\Exception;
+
+use ScssPhp\ScssPhp\StackTrace\Trace;
+use ScssPhp\ScssPhp\Util;
+use ScssPhp\ScssPhp\Util\ErrorUtil;
+use SourceSpan\FileSpan;
+
+/**
+ * @internal
+ */
+class MultiSpanSassException extends \Exception implements SassException
+{
+ public readonly string $primaryLabel;
+ /**
+ * @var array<string, FileSpan>
+ */
+ public readonly array $secondarySpans;
+ private readonly string $originalMessage;
+ private readonly FileSpan $span;
+
+ /**
+ * @param array<string, FileSpan> $secondarySpans
+ */
+ public function __construct(string $message, FileSpan $span, string $primaryLabel, array $secondarySpans, ?\Throwable $previous = null)
+ {
+ $this->originalMessage = $message;
+ $this->span = $span;
+ $this->primaryLabel = $primaryLabel;
+ $this->secondarySpans = $secondarySpans;
+
+ parent::__construct(ErrorUtil::formatErrorMessageMultiple($message, $span, $primaryLabel, $secondarySpans, $this->getSassTrace()), 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;
+ }
+
+ public function getSassTrace(): Trace
+ {
+ return new Trace([Util::frameForSpan($this->span, 'root stylesheet')]);
+ }
+
+ public function withAdditionalSpan(FileSpan $span, string $label, ?\Throwable $previous = null): MultiSpanSassException
+ {
+ return new self($this->originalMessage, $this->span, $this->primaryLabel, $this->secondarySpans + [$label => $span], $previous);
+ }
+
+ public function withTrace(Trace $trace, ?\Throwable $previous = null): MultiSpanSassRuntimeException
+ {
+ return new MultiSpanSassRuntimeException($this->originalMessage, $this->span, $this->primaryLabel, $this->secondarySpans, $trace, $previous);
+ }
+}
diff --git a/vendor/scssphp/scssphp/src/Exception/MultiSpanSassFormatException.php b/vendor/scssphp/scssphp/src/Exception/MultiSpanSassFormatException.php
new file mode 100644
index 000000000..1c015fd62
--- /dev/null
+++ b/vendor/scssphp/scssphp/src/Exception/MultiSpanSassFormatException.php
@@ -0,0 +1,23 @@
+<?php
+
+/**
+ * SCSSPHP
+ *
+ * @copyright 2012-2020 Leaf Corcoran
+ *
+ * @license http://opensource.org/licenses/MIT MIT
+ *
+ * @link http://scssphp.github.io/scssphp
+ */
+
+namespace ScssPhp\ScssPhp\Exception;
+
+use SourceSpan\FileSpan;
+
+final class MultiSpanSassFormatException extends MultiSpanSassException implements SassFormatException
+{
+ public function withAdditionalSpan(FileSpan $span, string $label, ?\Throwable $previous = null): MultiSpanSassFormatException
+ {
+ return new self($this->getOriginalMessage(), $this->getSpan(), $this->primaryLabel, $this->secondarySpans + [$label => $span], $previous);
+ }
+}
diff --git a/vendor/scssphp/scssphp/src/Exception/MultiSpanSassRuntimeException.php b/vendor/scssphp/scssphp/src/Exception/MultiSpanSassRuntimeException.php
new file mode 100644
index 000000000..107a554df
--- /dev/null
+++ b/vendor/scssphp/scssphp/src/Exception/MultiSpanSassRuntimeException.php
@@ -0,0 +1,41 @@
+<?php
+
+/**
+ * SCSSPHP
+ *
+ * @copyright 2012-2020 Leaf Corcoran
+ *
+ * @license http://opensource.org/licenses/MIT MIT
+ *
+ * @link http://scssphp.github.io/scssphp
+ */
+
+namespace ScssPhp\ScssPhp\Exception;
+
+use ScssPhp\ScssPhp\StackTrace\Trace;
+use SourceSpan\FileSpan;
+
+final class MultiSpanSassRuntimeException extends MultiSpanSassException implements SassRuntimeException
+{
+ private readonly Trace $sassTrace;
+
+ /**
+ * @param array<string, FileSpan> $secondarySpans
+ */
+ public function __construct(string $message, FileSpan $span, string $primaryLabel, array $secondarySpans, Trace $sassTrace, ?\Throwable $previous = null)
+ {
+ $this->sassTrace = $sassTrace;
+
+ parent::__construct($message, $span, $primaryLabel, $secondarySpans, $previous);
+ }
+
+ public function getSassTrace(): Trace
+ {
+ return $this->sassTrace;
+ }
+
+ public function withAdditionalSpan(FileSpan $span, string $label, ?\Throwable $previous = null): MultiSpanSassRuntimeException
+ {
+ return new self($this->getOriginalMessage(), $this->getSpan(), $this->primaryLabel, $this->secondarySpans + [$label => $span], $this->sassTrace, $previous);
+ }
+}
diff --git a/vendor/scssphp/scssphp/src/Exception/MultiSpanSassScriptException.php b/vendor/scssphp/scssphp/src/Exception/MultiSpanSassScriptException.php
new file mode 100644
index 000000000..1e741ae87
--- /dev/null
+++ b/vendor/scssphp/scssphp/src/Exception/MultiSpanSassScriptException.php
@@ -0,0 +1,48 @@
+<?php
+
+/**
+ * SCSSPHP
+ *
+ * @copyright 2012-2020 Leaf Corcoran
+ *
+ * @license http://opensource.org/licenses/MIT MIT
+ *
+ * @link http://scssphp.github.io/scssphp
+ */
+
+namespace ScssPhp\ScssPhp\Exception;
+
+use SourceSpan\FileSpan;
+
+/**
+ * @internal
+ */
+final class MultiSpanSassScriptException extends SassScriptException
+{
+ /**
+ * {@see MultiSpanSassException::$primaryLabel}
+ */
+ public readonly string $primaryLabel;
+ /**
+ * {@see MultiSpanSassException::$secondarySpans}
+ *
+ * @var array<string, FileSpan>
+ */
+ public readonly array $secondarySpans;
+
+ /**
+ * @param array<string, FileSpan> $secondarySpans
+ */
+ public function __construct(string $message, string $primaryLabel, array $secondarySpans, ?\Throwable $previous = null)
+ {
+ $this->primaryLabel = $primaryLabel;
+ $this->secondarySpans = $secondarySpans;
+
+ parent::__construct($message, 0, $previous);
+ }
+
+ public function withSpan(FileSpan $span): MultiSpanSassException
+ {
+ return new MultiSpanSassException($this->message, $span, $this->primaryLabel, $this->secondarySpans, $this);
+ }
+}
diff --git a/vendor/scssphp/scssphp/src/Exception/ParserException.php b/vendor/scssphp/scssphp/src/Exception/ParserException.php
deleted file mode 100644
index f0726698f..000000000
--- a/vendor/scssphp/scssphp/src/Exception/ParserException.php
+++ /dev/null
@@ -1,58 +0,0 @@
-<?php
-
-/**
- * SCSSPHP
- *
- * @copyright 2012-2020 Leaf Corcoran
- *
- * @license http://opensource.org/licenses/MIT MIT
- *
- * @link http://scssphp.github.io/scssphp
- */
-
-namespace ScssPhp\ScssPhp\Exception;
-
-/**
- * Parser Exception
- *
- * @author Oleksandr Savchenko <traveltino@gmail.com>
- *
- * @internal
- */
-class ParserException extends \Exception implements SassException
-{
- /**
- * @var array|null
- * @phpstan-var array{string, int, int}|null
- */
- private $sourcePosition;
-
- /**
- * Get source position
- *
- * @api
- *
- * @return array|null
- * @phpstan-return array{string, int, int}|null
- */
- public function getSourcePosition()
- {
- return $this->sourcePosition;
- }
-
- /**
- * Set source position
- *
- * @api
- *
- * @param array $sourcePosition
- *
- * @return void
- *
- * @phpstan-param array{string, int, int} $sourcePosition
- */
- public function setSourcePosition($sourcePosition)
- {
- $this->sourcePosition = $sourcePosition;
- }
-}
diff --git a/vendor/scssphp/scssphp/src/Exception/SassException.php b/vendor/scssphp/scssphp/src/Exception/SassException.php
index 9f62b3cd2..615bc7120 100644
--- a/vendor/scssphp/scssphp/src/Exception/SassException.php
+++ b/vendor/scssphp/scssphp/src/Exception/SassException.php
@@ -1,7 +1,52 @@
<?php
+/**
+ * SCSSPHP
+ *
+ * @copyright 2012-2020 Leaf Corcoran
+ *
+ * @license http://opensource.org/licenses/MIT MIT
+ *
+ * @link http://scssphp.github.io/scssphp
+ */
+
namespace ScssPhp\ScssPhp\Exception;
-interface SassException
+use ScssPhp\ScssPhp\StackTrace\Trace;
+use SourceSpan\FileSpan;
+
+interface SassException extends \Throwable
{
+ /**
+ * The span associated with this exception.
+ */
+ public function getSpan(): FileSpan;
+
+ /**
+ * Gets the original message without the location info in it.
+ */
+ public function getOriginalMessage(): string;
+
+ /**
+ * The Sass stack trace at the point this exception was thrown.
+ *
+ * This includes {@see getSpan}.
+ */
+ public function getSassTrace(): Trace;
+
+ /**
+ * Converts this to a {@see MultiSpanSassException} with the additional $span and
+ * $label.
+ *
+ * @internal
+ */
+ public function withAdditionalSpan(FileSpan $span, string $label, ?\Throwable $previous = null): MultiSpanSassException;
+
+ /**
+ * Returns a copy of this as a {@see SassRuntimeException} with $trace as its
+ * Sass stack trace.
+ *
+ * @internal
+ */
+ public function withTrace(Trace $trace, ?\Throwable $previous = null): SassRuntimeException;
}
diff --git a/vendor/scssphp/scssphp/src/Exception/RangeException.php b/vendor/scssphp/scssphp/src/Exception/SassFormatException.php
index 4be4dee70..f4d171efc 100644
--- a/vendor/scssphp/scssphp/src/Exception/RangeException.php
+++ b/vendor/scssphp/scssphp/src/Exception/SassFormatException.php
@@ -13,12 +13,8 @@
namespace ScssPhp\ScssPhp\Exception;
/**
- * Range exception
- *
- * @author Anthon Pang <anthon.pang@gmail.com>
- *
* @internal
*/
-class RangeException extends \Exception implements SassException
+interface SassFormatException extends SassException
{
}
diff --git a/vendor/scssphp/scssphp/src/Exception/SassRuntimeException.php b/vendor/scssphp/scssphp/src/Exception/SassRuntimeException.php
new file mode 100644
index 000000000..9db0ca77b
--- /dev/null
+++ b/vendor/scssphp/scssphp/src/Exception/SassRuntimeException.php
@@ -0,0 +1,23 @@
+<?php
+
+/**
+ * SCSSPHP
+ *
+ * @copyright 2012-2020 Leaf Corcoran
+ *
+ * @license http://opensource.org/licenses/MIT MIT
+ *
+ * @link http://scssphp.github.io/scssphp
+ */
+
+namespace ScssPhp\ScssPhp\Exception;
+
+use SourceSpan\FileSpan;
+
+/**
+ * @internal
+ */
+interface SassRuntimeException extends SassException
+{
+ public function withAdditionalSpan(FileSpan $span, string $label, ?\Throwable $previous = null): MultiSpanSassRuntimeException;
+}
diff --git a/vendor/scssphp/scssphp/src/Exception/SassScriptException.php b/vendor/scssphp/scssphp/src/Exception/SassScriptException.php
index 19356a7a1..37a40b1da 100644
--- a/vendor/scssphp/scssphp/src/Exception/SassScriptException.php
+++ b/vendor/scssphp/scssphp/src/Exception/SassScriptException.php
@@ -2,6 +2,9 @@
namespace ScssPhp\ScssPhp\Exception;
+use JiriPudil\SealedClasses\Sealed;
+use SourceSpan\FileSpan;
+
/**
* An exception thrown by SassScript.
*
@@ -10,6 +13,7 @@ namespace ScssPhp\ScssPhp\Exception;
* and replace it with a SassException reporting the location of the
* error.
*/
+#[Sealed([MultiSpanSassScriptException::class])]
class SassScriptException extends \Exception
{
/**
@@ -18,15 +22,22 @@ class SassScriptException extends \Exception
* This helper ensures a consistent handling of argument names in the
* error message, without duplicating it.
*
- * @param string $message
- * @param string|null $name The argument name, without $
- *
- * @return SassScriptException
+ * @param string|null $name The argument name, without $
*/
- public static function forArgument($message, $name = null)
+ public static function forArgument(string $message, ?string $name = null, ?\Throwable $previous = null): SassScriptException
{
$varDisplay = !\is_null($name) ? "\${$name}: " : '';
- return new self($varDisplay . $message);
+ return new self($varDisplay . $message, 0, $previous);
+ }
+
+ /**
+ * Converts this to a {@see SassException} with the given $span.
+ *
+ * @internal
+ */
+ public function withSpan(FileSpan $span): SassException
+ {
+ return new SimpleSassException($this->message, $span, $this);
}
}
diff --git a/vendor/scssphp/scssphp/src/Exception/ServerException.php b/vendor/scssphp/scssphp/src/Exception/ServerException.php
deleted file mode 100644
index e593c4014..000000000
--- a/vendor/scssphp/scssphp/src/Exception/ServerException.php
+++ /dev/null
@@ -1,26 +0,0 @@
-<?php
-
-/**
- * SCSSPHP
- *
- * @copyright 2012-2020 Leaf Corcoran
- *
- * @license http://opensource.org/licenses/MIT MIT
- *
- * @link http://scssphp.github.io/scssphp
- */
-
-namespace ScssPhp\ScssPhp\Exception;
-
-@trigger_error(sprintf('The "%s" class is deprecated.', ServerException::class), E_USER_DEPRECATED);
-
-/**
- * Server Exception
- *
- * @author Anthon Pang <anthon.pang@gmail.com>
- *
- * @deprecated The Scssphp server should define its own exception instead.
- */
-class ServerException extends \Exception implements SassException
-{
-}
diff --git a/vendor/scssphp/scssphp/src/Exception/SimpleSassException.php b/vendor/scssphp/scssphp/src/Exception/SimpleSassException.php
new file mode 100644
index 000000000..d91a7622d
--- /dev/null
+++ b/vendor/scssphp/scssphp/src/Exception/SimpleSassException.php
@@ -0,0 +1,61 @@
+<?php
+
+/**
+ * SCSSPHP
+ *
+ * @copyright 2012-2020 Leaf Corcoran
+ *
+ * @license http://opensource.org/licenses/MIT MIT
+ *
+ * @link http://scssphp.github.io/scssphp
+ */
+
+namespace ScssPhp\ScssPhp\Exception;
+
+use ScssPhp\ScssPhp\StackTrace\Trace;
+use ScssPhp\ScssPhp\Util;
+use ScssPhp\ScssPhp\Util\ErrorUtil;
+use SourceSpan\FileSpan;
+
+/**
+ * @internal
+ */
+final class SimpleSassException extends \Exception implements SassException
+{
+ private readonly string $originalMessage;
+
+ private readonly FileSpan $span;
+
+ public function __construct(string $message, FileSpan $span, ?\Throwable $previous = null)
+ {
+ $this->originalMessage = $message;
+ $this->span = $span;
+
+ parent::__construct(ErrorUtil::formatErrorMessage($message, $span, $this->getSassTrace()), 0, $previous);
+ }
+
+ public function getOriginalMessage(): string
+ {
+ return $this->originalMessage;
+ }
+
+ public function getSpan(): FileSpan
+ {
+ return $this->span;
+ }
+
+ public function getSassTrace(): Trace
+ {
+ return new Trace([Util::frameForSpan($this->span, 'root stylesheet')]);
+ }
+
+ public function withAdditionalSpan(FileSpan $span, string $label, ?\Throwable $previous = null): MultiSpanSassException
+ {
+ return new MultiSpanSassException($this->originalMessage, $this->span, '', [$label => $span], $previous);
+ }
+
+ public function withTrace(Trace $trace, ?\Throwable $previous = null): SassRuntimeException
+ {
+ return new SimpleSassRuntimeException($this->originalMessage, $this->span, $trace, $previous);
+ }
+}
diff --git a/vendor/scssphp/scssphp/src/Exception/SimpleSassFormatException.php b/vendor/scssphp/scssphp/src/Exception/SimpleSassFormatException.php
new file mode 100644
index 000000000..07b1c9ae8
--- /dev/null
+++ b/vendor/scssphp/scssphp/src/Exception/SimpleSassFormatException.php
@@ -0,0 +1,63 @@
+<?php
+
+/**
+ * SCSSPHP
+ *
+ * @copyright 2012-2020 Leaf Corcoran
+ *
+ * @license http://opensource.org/licenses/MIT MIT
+ *
+ * @link http://scssphp.github.io/scssphp
+ */
+
+namespace ScssPhp\ScssPhp\Exception;
+
+use ScssPhp\ScssPhp\StackTrace\Trace;
+use ScssPhp\ScssPhp\Util;
+use SourceSpan\FileSpan;
+
+/**
+ * @internal
+ */
+final class SimpleSassFormatException extends \Exception implements SassFormatException
+{
+ private readonly string $originalMessage;
+
+ private readonly FileSpan $span;
+
+ public function __construct(string $message, FileSpan $span, ?\Throwable $previous = null)
+ {
+ $this->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;
+ }
+
+ public function getSassTrace(): Trace
+ {
+ return new Trace([Util::frameForSpan($this->span, 'root stylesheet')]);
+ }
+
+ public function withAdditionalSpan(FileSpan $span, string $label, ?\Throwable $previous = null): MultiSpanSassFormatException
+ {
+ return new MultiSpanSassFormatException($this->originalMessage, $this->span, '', [$label => $span], $previous);
+ }
+
+ public function withTrace(Trace $trace, ?\Throwable $previous = null): SassRuntimeException
+ {
+ return new SimpleSassRuntimeException($this->originalMessage, $this->span, $trace, $previous);
+ }
+}
diff --git a/vendor/scssphp/scssphp/src/Exception/SimpleSassRuntimeException.php b/vendor/scssphp/scssphp/src/Exception/SimpleSassRuntimeException.php
new file mode 100644
index 000000000..cacb048a9
--- /dev/null
+++ b/vendor/scssphp/scssphp/src/Exception/SimpleSassRuntimeException.php
@@ -0,0 +1,74 @@
+<?php
+
+/**
+ * SCSSPHP
+ *
+ * @copyright 2012-2020 Leaf Corcoran
+ *
+ * @license http://opensource.org/licenses/MIT MIT
+ *
+ * @link http://scssphp.github.io/scssphp
+ */
+
+namespace ScssPhp\ScssPhp\Exception;
+
+use ScssPhp\ScssPhp\StackTrace\Trace;
+use ScssPhp\ScssPhp\Util\ErrorUtil;
+use SourceSpan\FileSpan;
+
+/**
+ * @internal
+ */
+final class SimpleSassRuntimeException extends \Exception implements SassRuntimeException
+{
+ /**
+ * @var string
+ * @readonly
+ */
+ private $originalMessage;
+
+ /**
+ * @var FileSpan
+ * @readonly
+ */
+ private $span;
+
+ private readonly Trace $sassTrace;
+
+ public function __construct(string $message, FileSpan $span, Trace $sassTrace, ?\Throwable $previous = null)
+ {
+ $this->originalMessage = $message;
+ $this->span = $span;
+ $this->sassTrace = $sassTrace;
+
+ parent::__construct(ErrorUtil::formatErrorMessage($message, $span, $this->sassTrace), 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;
+ }
+
+ public function getSassTrace(): Trace
+ {
+ return $this->sassTrace;
+ }
+
+ public function withAdditionalSpan(FileSpan $span, string $label, ?\Throwable $previous = null): MultiSpanSassRuntimeException
+ {
+ return new MultiSpanSassRuntimeException($this->originalMessage, $this->span, '', [$label => $span], $this->sassTrace, $previous);
+ }
+
+ public function withTrace(Trace $trace, ?\Throwable $previous = null): SassRuntimeException
+ {
+ return new SimpleSassRuntimeException($this->originalMessage, $this->span, $trace, $previous);
+ }
+}
diff --git a/vendor/scssphp/scssphp/src/Extend/ComplexSelectorMap.php b/vendor/scssphp/scssphp/src/Extend/ComplexSelectorMap.php
new file mode 100644
index 000000000..c2eba25fd
--- /dev/null
+++ b/vendor/scssphp/scssphp/src/Extend/ComplexSelectorMap.php
@@ -0,0 +1,41 @@
+<?php
+
+/**
+ * SCSSPHP
+ *
+ * @copyright 2012-2020 Leaf Corcoran
+ *
+ * @license http://opensource.org/licenses/MIT MIT
+ *
+ * @link http://scssphp.github.io/scssphp
+ */
+
+namespace ScssPhp\ScssPhp\Extend;
+
+use ScssPhp\ScssPhp\Ast\Selector\ComplexSelector;
+
+/**
+ * @template T
+ * @template-extends \SplObjectStorage<ComplexSelector, T>
+ *
+ * @internal
+ */
+final class ComplexSelectorMap extends \SplObjectStorage
+{
+ public function getHash(object $object): string
+ {
+ \assert($object instanceof ComplexSelector);
+ // For ComplexSelector, selectors that are equal by value semantic are exactly the ones that have the same string representation.
+ return (string) $object;
+ }
+
+ /**
+ * @return iterable<T>
+ */
+ public function getValues(): iterable
+ {
+ foreach ($this as $selector) {
+ yield $this[$selector];
+ }
+ }
+}
diff --git a/vendor/scssphp/scssphp/src/Extend/ConcreteExtensionStore.php b/vendor/scssphp/scssphp/src/Extend/ConcreteExtensionStore.php
new file mode 100644
index 000000000..5d267fde6
--- /dev/null
+++ b/vendor/scssphp/scssphp/src/Extend/ConcreteExtensionStore.php
@@ -0,0 +1,1245 @@
+<?php
+
+/**
+ * SCSSPHP
+ *
+ * @copyright 2012-2020 Leaf Corcoran
+ *
+ * @license http://opensource.org/licenses/MIT MIT
+ *
+ * @link http://scssphp.github.io/scssphp
+ */
+
+namespace ScssPhp\ScssPhp\Extend;
+
+use ScssPhp\ScssPhp\Ast\Css\CssMediaQuery;
+use ScssPhp\ScssPhp\Ast\Sass\Statement\ExtendRule;
+use ScssPhp\ScssPhp\Ast\Selector\ComplexSelector;
+use ScssPhp\ScssPhp\Ast\Selector\ComplexSelectorComponent;
+use ScssPhp\ScssPhp\Ast\Selector\CompoundSelector;
+use ScssPhp\ScssPhp\Ast\Selector\PlaceholderSelector;
+use ScssPhp\ScssPhp\Ast\Selector\PseudoSelector;
+use ScssPhp\ScssPhp\Ast\Selector\SelectorList;
+use ScssPhp\ScssPhp\Ast\Selector\SimpleSelector;
+use ScssPhp\ScssPhp\Exception\SassException;
+use ScssPhp\ScssPhp\Exception\SassScriptException;
+use ScssPhp\ScssPhp\Exception\SimpleSassException;
+use ScssPhp\ScssPhp\Util;
+use ScssPhp\ScssPhp\Util\Box;
+use ScssPhp\ScssPhp\Util\EquatableUtil;
+use ScssPhp\ScssPhp\Util\IterableUtil;
+use ScssPhp\ScssPhp\Util\ListUtil;
+use ScssPhp\ScssPhp\Util\ModifiableBox;
+use SourceSpan\FileSpan;
+
+class ConcreteExtensionStore implements ExtensionStore
+{
+ /**
+ * A map from all simple selectors in the stylesheet to the selector lists
+ * that contain them.
+ *
+ * This is used to find which selectors an `@extend` applies to and adjust
+ * them.
+ *
+ * @var SimpleSelectorMap<ObjectSet<ModifiableBox<SelectorList>>>
+ */
+ private readonly SimpleSelectorMap $selectors;
+ /**
+ * A map from all extended simple selectors to the sources of those
+ * extensions.
+ *
+ * @var SimpleSelectorMap<ComplexSelectorMap<Extension>>
+ */
+ private SimpleSelectorMap $extensions;
+ /**
+ * A map from all simple selectors in extenders to the extensions that those
+ * extenders define.
+ *
+ * @var SimpleSelectorMap<list<Extension>>
+ */
+ private SimpleSelectorMap $extensionsByExtender;
+ /**
+ * A map from CSS selectors to the media query contexts they're defined in.
+ *
+ * This tracks the contexts in which each selector's style rule is defined.
+ * If a rule is defined at the top level, it doesn't have an entry.
+ *
+ * @var \SplObjectStorage<ModifiableBox<SelectorList>, list<CssMediaQuery>>
+ */
+ private readonly \SplObjectStorage $mediaContexts;
+ /**
+ * @var \SplObjectStorage<SimpleSelector, int>
+ */
+ private \SplObjectStorage $sourceSpecificity;
+ /**
+ * @var \SplObjectStorage<ComplexSelector, mixed>
+ */
+ private readonly \SplObjectStorage $originals;
+ private readonly ExtendMode $mode;
+
+ /**
+ * Extends $selector with $source extender and $targets extendees.
+ *
+ * This works as though `source {@extend target}` were written in the
+ * stylesheet, with the exception that $target can contain compound
+ * selectors which must be extended as a unit.
+ */
+ public static function extend(SelectorList $selector, SelectorList $source, SelectorList $targets, FileSpan $span): SelectorList
+ {
+ return self::extendOrReplace($selector, $source, $targets, ExtendMode::allTargets, $span);
+ }
+
+ /**
+ * Returns a copy of $selector with $targets replaced by $source.
+ */
+ public static function replace(SelectorList $selector, SelectorList $source, SelectorList $targets, FileSpan $span): SelectorList
+ {
+ return self::extendOrReplace($selector, $source, $targets, ExtendMode::replace, $span);
+ }
+
+ /**
+ * A helper function for {@see extend} and {@see replace}.
+ */
+ private static function extendOrReplace(SelectorList $selector, SelectorList $source, SelectorList $targets, ExtendMode $mode, FileSpan $span): SelectorList
+ {
+ $extender = ConcreteExtensionStore::createForMode($mode);
+
+ if (!$selector->isInvisible()) {
+ foreach ($selector->getComponents() as $component) {
+ $extender->originals->attach($component);
+ }
+ }
+
+ foreach ($targets->getComponents() as $complex) {
+ $compound = $complex->getSingleCompound();
+
+ if ($compound === null) {
+ throw new SassScriptException("Can't extend complex selector $complex.");
+ }
+
+ $extensions = new SimpleSelectorMap();
+ foreach ($compound->getComponents() as $simple) {
+ $extensionMap = new ComplexSelectorMap();
+
+ foreach ($source->getComponents() as $sourceComplex) {
+ $extensionMap[$sourceComplex] = new Extension($sourceComplex, $simple, $span, optional: true);
+ }
+
+ $extensions[$simple] = $extensionMap;
+ }
+
+ $selector = $extender->extendList($selector, $extensions);
+ }
+
+ return $selector;
+ }
+
+ /**
+ * @param SimpleSelectorMap<ObjectSet<ModifiableBox<SelectorList>>> $selectors
+ * @param SimpleSelectorMap<ComplexSelectorMap<Extension>> $extensions
+ * @param SimpleSelectorMap<list<Extension>> $extensionsByExtender
+ * @param \SplObjectStorage<ModifiableBox<SelectorList>, list<CssMediaQuery>> $mediaContexts
+ * @param \SplObjectStorage<SimpleSelector, int> $sourceSpecificity
+ * @param \SplObjectStorage<ComplexSelector, mixed> $originals
+ */
+ private function __construct(
+ SimpleSelectorMap $selectors,
+ SimpleSelectorMap $extensions,
+ SimpleSelectorMap $extensionsByExtender,
+ \SplObjectStorage $mediaContexts,
+ \SplObjectStorage $sourceSpecificity,
+ \SplObjectStorage $originals,
+ ExtendMode $mode,
+ ) {
+ $this->selectors = $selectors;
+ $this->extensions = $extensions;
+ $this->extensionsByExtender = $extensionsByExtender;
+ $this->mediaContexts = $mediaContexts;
+ $this->sourceSpecificity = $sourceSpecificity;
+ $this->originals = $originals;
+ $this->mode = $mode;
+ }
+
+ public static function create(): self
+ {
+ return self::createForMode(ExtendMode::normal);
+ }
+
+ private static function createForMode(ExtendMode $mode): self
+ {
+ /** @var \SplObjectStorage<ModifiableBox<SelectorList>, list<CssMediaQuery>> $mediaContexts */
+ $mediaContexts = new \SplObjectStorage();
+ /** @var \SplObjectStorage<SimpleSelector, int> $sourceSpecificity */
+ $sourceSpecificity = new \SplObjectStorage();
+ /** @var \SplObjectStorage<ComplexSelector, mixed> $originals */
+ $originals = new \SplObjectStorage();
+
+ return new self(
+ new SimpleSelectorMap(),
+ new SimpleSelectorMap(),
+ new SimpleSelectorMap(),
+ $mediaContexts,
+ $sourceSpecificity,
+ $originals,
+ $mode,
+ );
+ }
+
+ public function isEmpty(): bool
+ {
+ return \count($this->extensions) === 0;
+ }
+
+ public function getSimpleSelectors(): array
+ {
+ return iterator_to_array($this->selectors);
+ }
+
+ public function extensionsWhereTarget(callable $callback): iterable
+ {
+ foreach ($this->extensions as $simple) {
+ if (!$callback($simple)) {
+ continue;
+ }
+
+ $sources = $this->extensions[$simple];
+
+ foreach ($sources->getValues() as $extension) {
+ if ($extension instanceof MergedExtension) {
+ foreach ($extension->unmerge() as $leafExtension) {
+ if (!$leafExtension->isOptional) {
+ yield $leafExtension;
+ }
+ }
+ } elseif (!$extension->isOptional) {
+ yield $extension;
+ }
+ }
+ }
+ }
+
+ public function addSelector(SelectorList $selector, ?array $mediaContext): Box
+ {
+ $originalSelector = $selector;
+
+ if (!$originalSelector->isInvisible()) {
+ foreach ($originalSelector->getComponents() as $component) {
+ $this->originals->attach($component);
+ }
+ }
+
+ if (\count($this->extensions) !== 0) {
+ try {
+ $selector = $this->extendList($originalSelector, $this->extensions, $mediaContext);
+ } catch (SassException $e) {
+ throw new SimpleSassException("From {$e->getSpan()->message('')}\n" . $e->getOriginalMessage(), $e->getSpan(), $e);
+ }
+ }
+
+ $modifiableSelector = new ModifiableBox($selector);
+
+ if ($mediaContext !== null) {
+ $this->mediaContexts->attach($modifiableSelector, $mediaContext);
+ }
+
+ $this->registerSelector($selector, $modifiableSelector);
+
+ return $modifiableSelector->seal();
+ }
+
+ /**
+ * Registers the {@see SimpleSelector}s in $list to point to $selector in
+ * {@see selectors}.
+ *
+ * @param ModifiableBox<SelectorList> $selector
+ */
+ private function registerSelector(SelectorList $list, ModifiableBox $selector): void
+ {
+ foreach ($list->getComponents() as $complex) {
+ foreach ($complex->getComponents() as $component) {
+ foreach ($component->getSelector()->getComponents() as $simple) {
+ if (!isset($this->selectors[$simple])) {
+ /** @var ObjectSet<ModifiableBox<SelectorList>> $set */
+ $set = new ObjectSet();
+ $this->selectors->attach($simple, $set);
+ }
+ $this->selectors[$simple]->add($selector);
+
+ if ($simple instanceof PseudoSelector && $simple->getSelector() !== null) {
+ $this->registerSelector($simple->getSelector(), $selector);
+ }
+ }
+ }
+ }
+ }
+
+ public function addExtension(SelectorList $extender, SimpleSelector $target, ExtendRule $extend, ?array $mediaContext): void
+ {
+ $selectors = $this->selectors[$target] ?? null;
+ $existingExtensions = $this->extensionsByExtender[$target] ?? null;
+
+ $newExtensions = null;
+ $sources = $this->extensions[$target] ??= new ComplexSelectorMap();
+
+ foreach ($extender->getComponents() as $complex) {
+ if ($complex->isUseless()) {
+ continue;
+ }
+
+ $extension = new Extension($complex, $target, $extend->getSpan(), $mediaContext, $extend->isOptional());
+
+ $existingExtension = $sources[$complex] ?? null;
+
+ if ($existingExtension !== null) {
+ // If there's already an extend from $extender to $target, we don't need
+ // to re-run the extension. We may need to mark the extension as
+ // mandatory, though.
+ $sources[$complex] = MergedExtension::merge($existingExtension, $extension);
+ continue;
+ }
+
+ $sources[$complex] = $extension;
+
+ foreach ($this->simpleSelectors($complex) as $simple) {
+ $extensionsByExtender = $this->extensionsByExtender[$simple] ?? [];
+ $extensionsByExtender[] = $extension;
+ $this->extensionsByExtender[$simple] = $extensionsByExtender;
+
+ // Only source specificity for the original selector is relevant.
+ // Selectors generated by `@extend` don't get new specificity.
+ $this->sourceSpecificity[$simple] ??= $complex->getSpecificity();
+ }
+
+ if ($selectors !== null || $existingExtensions !== null) {
+ /** @var ComplexSelectorMap<Extension> $newExtensions */
+ $newExtensions ??= new ComplexSelectorMap();
+ $newExtensions[$complex] = $extension;
+ }
+ }
+
+ if ($newExtensions === null) {
+ return;
+ }
+
+ /** @var SimpleSelectorMap<ComplexSelectorMap<Extension>> $newExtensionsByTarget */
+ $newExtensionsByTarget = new SimpleSelectorMap();
+ $newExtensionsByTarget[$target] = $newExtensions;
+
+ if ($existingExtensions !== null) {
+ // Reload the list of existing extensions as it is an array, not an object.
+ $existingExtensions = $this->extensionsByExtender[$target];
+ $additionalExtensions = $this->extendExistingExtensions($existingExtensions, $newExtensionsByTarget);
+
+ if ($additionalExtensions !== null) {
+ Util::mapAddAll2($newExtensionsByTarget, $additionalExtensions);
+ }
+ }
+
+ if ($selectors !== null) {
+ $this->extendExistingSelectors($selectors, $newExtensionsByTarget);
+ }
+ }
+
+ /**
+ * Returns an iterable of all simple selectors in $complex.
+ *
+ * @return iterable<SimpleSelector>
+ */
+ private function simpleSelectors(ComplexSelector $complex): iterable
+ {
+ foreach ($complex->getComponents() as $component) {
+ foreach ($component->getSelector()->getComponents() as $simple) {
+ yield $simple;
+
+ if ($simple instanceof PseudoSelector && $simple->getSelector() !== null) {
+ foreach ($simple->getSelector()->getComponents() as $pseudoComplex) {
+ yield from $this->simpleSelectors($pseudoComplex);
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * Extend $extensions using $newExtensions.
+ *
+ * Note that this does duplicate some work done by
+ * {@see extendExistingSelectors}, but it's necessary to expand each extension's
+ * extender separately without reference to the full selector list, so that
+ * relevant results don't get trimmed too early.
+ *
+ * Returns extensions that should be added to $newExtensions before
+ * extending selectors in order to properly handle extension loops such as:
+ *
+ * .c {x: y; @extend .a}
+ * .x.y.a {@extend .b}
+ * .z.b {@extend .c}
+ *
+ * Returns `null` if there are no extensions to add.
+ *
+ * @param list<Extension> $extensions
+ * @param SimpleSelectorMap<ComplexSelectorMap<Extension>> $newExtensions
+ * @return SimpleSelectorMap<ComplexSelectorMap<Extension>>|null
+ */
+ private function extendExistingExtensions(array $extensions, SimpleSelectorMap $newExtensions): ?SimpleSelectorMap
+ {
+ $additionalExtensions = null;
+
+ foreach ($extensions as $extension) {
+ $sources = $this->extensions[$extension->target];
+
+ try {
+ $selectors = $this->extendComplex($extension->extender->selector, $newExtensions, $extension->mediaContext);
+
+ if ($selectors === null) {
+ continue;
+ }
+ } catch (SassException $e) {
+ throw $e->withAdditionalSpan($extension->extender->selector->getSpan(), 'target selector', $e);
+ }
+
+ // If the output contains the original complex selector, there's no need
+ // to recreate it.
+ $containsExtension = EquatableUtil::equals($selectors[0], $extension->extender->selector);
+ if ($containsExtension) {
+ $selectors = array_slice($selectors, 1);
+ }
+
+ foreach ($selectors as $complex) {
+ $withExtender = $extension->withExtender($complex);
+
+ $existingExtension = $sources[$complex] ?? null;
+ if ($existingExtension !== null) {
+ $sources[$complex] = MergedExtension::merge($existingExtension, $withExtender);
+ } else {
+ $sources[$complex] = $withExtender;
+
+ foreach ($complex->getComponents() as $component) {
+ foreach ($component->getSelector()->getComponents() as $simple) {
+ $extensionsByExtender = $this->extensionsByExtender[$simple] ?? [];
+ $extensionsByExtender[] = $withExtender;
+ $this->extensionsByExtender[$simple] = $extensionsByExtender;
+ }
+ }
+
+ if ($newExtensions->contains($extension->target)) {
+ /** @var SimpleSelectorMap<ComplexSelectorMap<Extension>> $additionalExtensions */
+ $additionalExtensions ??= new SimpleSelectorMap();
+
+ if (!isset($additionalExtensions[$extension->target])) {
+ /** @var ComplexSelectorMap<Extension> $additionalSources */
+ $additionalSources = new ComplexSelectorMap();
+ $additionalExtensions[$extension->target] = $additionalSources;
+ } else {
+ $additionalSources = $additionalExtensions[$extension->target];
+ }
+
+ $additionalSources[$complex] = $withExtender;
+ }
+ }
+ }
+ }
+
+ return $additionalExtensions;
+ }
+
+ /**
+ * Extend $selectors using $newExtensions.
+ *
+ * @param ObjectSet<ModifiableBox<SelectorList>> $selectors
+ * @param SimpleSelectorMap<ComplexSelectorMap<Extension>> $newExtensions
+ */
+ private function extendExistingSelectors(ObjectSet $selectors, SimpleSelectorMap $newExtensions): void
+ {
+ foreach ($selectors as $selector) {
+ $oldValue = $selector->getValue();
+
+ try {
+ $selector->setValue($this->extendList($selector->getValue(), $newExtensions, $this->mediaContexts[$selector] ?? null));
+ } catch (SassException $e) {
+ throw new SimpleSassException("From {$e->getSpan()->message('')}\n" . $e->getOriginalMessage(), $e->getSpan(), $e);
+ }
+
+ // If no extends actually happened (for example because unification
+ // failed), we don't need to re-register the selector.
+ if ($oldValue === $selector->getValue()) {
+ continue;
+ }
+ $this->registerSelector($selector->getValue(), $selector);
+ }
+ }
+
+ /**
+ * @param iterable<ExtensionStore> $extensionStores
+ */
+ public function addExtensions(iterable $extensionStores): void
+ {
+ /** @var list<Extension>|null $extensionsToExtend */
+ $extensionsToExtend = null;
+ $selectorsToExtend = null;
+ $newExtensions = null;
+
+ foreach ($extensionStores as $extensionStore) {
+ if ($extensionStore->isEmpty()) {
+ continue;
+ }
+ \assert($extensionStore instanceof ConcreteExtensionStore);
+ $this->sourceSpecificity->addAll($extensionStore->sourceSpecificity);
+
+ foreach ($extensionStore->extensions as $target) {
+ $newSources = $extensionStore->extensions->getInfo();
+
+ // Private selectors can't be extended across module boundaries.
+ if ($target instanceof PlaceholderSelector && $target->isPrivate()) {
+ continue;
+ }
+
+ $extensionsForTarget = $this->extensionsByExtender[$target] ?? null;
+ if ($extensionsForTarget !== null) {
+ $extensionsToExtend ??= [];
+ array_push($extensionsToExtend, ...$extensionsForTarget);
+ }
+
+ // Find existing selectors to extend.
+ $selectorsForTarget = $this->selectors[$target] ?? null;
+ if ($selectorsForTarget !== null) {
+ if ($selectorsToExtend === null) {
+ /** @var ObjectSet<ModifiableBox<SelectorList>> $selectorsToExtend */
+ $selectorsToExtend = new ObjectSet();
+ }
+ $selectorsToExtend->addAll($selectorsForTarget);
+ }
+
+ $existingSources = $this->extensions[$target] ?? null;
+ if ($existingSources !== null) {
+ foreach ($newSources as $extender) {
+ $extension = $newSources->getInfo();
+
+ if (isset($existingSources[$extender])) {
+ $extension = MergedExtension::merge($existingSources[$extender], $extension);
+ }
+ $existingSources[$extender] = $extension;
+
+ if ($extensionsForTarget !== null || $selectorsForTarget !== null) {
+ /** @var SimpleSelectorMap<ComplexSelectorMap<Extension>> $newExtensions */
+ $newExtensions ??= new SimpleSelectorMap();
+
+ if (!isset($newExtensions[$target])) {
+ /** @var ComplexSelectorMap<Extension> $newMap */
+ $newMap = new ComplexSelectorMap();
+ $newExtensions[$target] = $newMap;
+ }
+ $newExtensions[$target][$extender] = $extension;
+ }
+ }
+ } else {
+ $this->extensions[$target] = clone $newSources;
+ if ($extensionsForTarget !== null || $selectorsForTarget !== null) {
+ /** @var SimpleSelectorMap<ComplexSelectorMap<Extension>> $newExtensions */
+ $newExtensions ??= new SimpleSelectorMap();
+ $newExtensions[$target] = clone $newSources;
+ }
+ }
+ }
+ }
+
+ if ($newExtensions !== null) {
+ // We can ignore the return value here because it's only useful for extend
+ // loops, which can't exist across module boundaries.
+ if ($extensionsToExtend !== null) {
+ $this->extendExistingExtensions($extensionsToExtend, $newExtensions);
+ }
+
+ if ($selectorsToExtend !== null) {
+ $this->extendExistingSelectors($selectorsToExtend, $newExtensions);
+ }
+ }
+ }
+
+ /**
+ * Extends $list using $extensions.
+ *
+ * @param SimpleSelectorMap<ComplexSelectorMap<Extension>> $extensions
+ * @param list<CssMediaQuery>|null $mediaQueryContext
+ */
+ private function extendList(SelectorList $list, SimpleSelectorMap $extensions, ?array $mediaQueryContext = null): SelectorList
+ {
+ $extended = null;
+
+ foreach ($list->getComponents() as $i => $complex) {
+ $result = $this->extendComplex($complex, $extensions, $mediaQueryContext);
+
+ \assert($result === null || \count($result) > 0, "extendComplex($complex) should return null rather than [] if extension fails.");
+
+ if ($result === null) {
+ if ($extended !== null) {
+ $extended[] = $complex;
+ }
+ } else {
+ $extended ??= $i === 0 ? [] : array_slice($list->getComponents(), 0, $i);
+ array_push($extended, ...$result);
+ }
+ }
+
+ if ($extended === null) {
+ return $list;
+ }
+
+ return new SelectorList($this->trim($extended, $this->originals->contains(...)), $list->getSpan());
+ }
+
+ /**
+ * Extends $complex using $extensions, and returns the contents of a
+ * {@see SelectorList}.
+ *
+ * @param SimpleSelectorMap<ComplexSelectorMap<Extension>> $extensions
+ * @param list<CssMediaQuery>|null $mediaQueryContext
+ * @return list<ComplexSelector>|null
+ */
+ private function extendComplex(ComplexSelector $complex, SimpleSelectorMap $extensions, ?array $mediaQueryContext): ?array
+ {
+ if (\count($complex->getLeadingCombinators()) > 1) {
+ return null;
+ }
+
+ // The complex selectors that each compound selector in $complex->getComponents()
+ // can expand to.
+ //
+ // For example, given
+ //
+ // .a .b {...}
+ // .x .y {@extend .b}
+ //
+ // this will contain
+ //
+ // [
+ // [.a],
+ // [.b, .x .y]
+ // ]
+ //
+ $extendedNotExpanded = null;
+ $isOriginal = $this->originals->contains($complex);
+
+ foreach ($complex->getComponents() as $i => $component) {
+ $extended = $this->extendCompound($component, $extensions, $mediaQueryContext, $isOriginal);
+
+ \assert($extended === null || \count($extended) > 0, "extendCompound($component) should return null rather than [] if extension fails.");
+
+ if ($extended === null) {
+ if ($extendedNotExpanded !== null) {
+ $extendedNotExpanded[] = [
+ new ComplexSelector(
+ [],
+ [$component],
+ $complex->getSpan(),
+ $complex->getLineBreak()
+ ),
+ ];
+ }
+ } elseif ($extendedNotExpanded !== null) {
+ $extendedNotExpanded[] = $extended;
+ } elseif ($i !== 0) {
+ $extendedNotExpanded = [
+ [
+ new ComplexSelector(
+ $complex->getLeadingCombinators(),
+ array_slice($complex->getComponents(), 0, $i),
+ $complex->getSpan(),
+ $complex->getLineBreak(),
+ ),
+ ],
+ $extended,
+ ];
+ } elseif (\count($complex->getLeadingCombinators()) === 0) {
+ $extendedNotExpanded = [$extended];
+ } else {
+ $newExtended = [];
+
+ foreach ($extended as $newComplex) {
+ if (
+ \count($newComplex->getLeadingCombinators()) === 0
+ || EquatableUtil::listEquals($complex->getLeadingCombinators(), $newComplex->getLeadingCombinators())
+ ) {
+ $newExtended[] = new ComplexSelector(
+ $complex->getLeadingCombinators(),
+ $newComplex->getComponents(),
+ $complex->getSpan(),
+ $complex->getLineBreak() || $newComplex->getLineBreak(),
+ );
+ }
+ }
+
+ $extendedNotExpanded = [$newExtended];
+ }
+ }
+
+ if ($extendedNotExpanded === null) {
+ return null;
+ }
+
+ $first = true;
+
+ return iterator_to_array(self::expandIterable(ExtendUtil::paths($extendedNotExpanded), function ($path) use (&$first, $complex) {
+ return array_map(function (ComplexSelector $outputComplex) use (&$first, $complex) {
+ // Make sure that copies of $complex retain their status as "original"
+ // selectors. This includes selectors that are modified because a :not()
+ // was extended into.
+ if ($first && $this->originals->contains($complex)) {
+ $this->originals->attach($outputComplex);
+ }
+
+ $first = false;
+
+ return $outputComplex;
+ }, ExtendUtil::weave($path, $complex->getSpan(), $complex->getLineBreak()));
+ }), false);
+ }
+
+ /**
+ * Extends $component using $extensions, and returns the contents of a
+ * {@see SelectorList}.
+ *
+ * The $inOriginal parameter indicates whether this is in an original
+ * complex selector, meaning that the compound should not be trimmed out.
+ *
+ * @param SimpleSelectorMap<ComplexSelectorMap<Extension>> $extensions
+ * @param list<CssMediaQuery>|null $mediaQueryContext
+ * @return list<ComplexSelector>|null
+ */
+ private function extendCompound(ComplexSelectorComponent $component, SimpleSelectorMap $extensions, ?array $mediaQueryContext, bool $inOriginal): ?array
+ {
+ // If there's more than one target and they all need to match, we track
+ // which targets are actually extended.
+ $targetsUsed = $this->mode === ExtendMode::normal || \count($extensions) < 2 ? null : new SimpleSelectorMap();
+
+ $simples = $component->getSelector()->getComponents();
+
+ // The complex selectors produced from each simple selector in the compound selector.
+ $options = null;
+
+ foreach ($simples as $i => $simple) {
+ $extended = $this->extendSimple($simple, $extensions, $mediaQueryContext, $targetsUsed);
+
+ \assert($extended === null || \count($extended) > 0, "extendSimple($simple) should return null rather than [] if extension fails.");
+
+ if ($extended === null) {
+ if ($options !== null) {
+ $options[] = [$this->extenderForSimple($simple)];
+ }
+ } else {
+ if ($options === null) {
+ $options = [];
+
+ if ($i !== 0) {
+ $options[] = [$this->extenderForCompound(array_slice($simples, 0, $i), $component->getSpan())];
+ }
+ }
+
+ array_push($options, ...$extended);
+ }
+ }
+
+ if ($options === null) {
+ return null;
+ }
+
+ /**
+ * If {@see mode} isn't {@see ExtendMode::normal} and we didn't use all the targets in
+ * $extensions, extension fails for $component.
+ */
+ if ($targetsUsed !== null && \count($targetsUsed) !== \count($extensions)) {
+ return null;
+ }
+
+ // Optimize for the simple case of a single simple selector that doesn't
+ // need any unification.
+ if (\count($options) === 1) {
+ $extenders = $options[0];
+ $result = null;
+ foreach ($extenders as $extender) {
+ $extender->assertCompatibleMediaContext($mediaQueryContext);
+
+ $complex = $extender->selector->withAdditionalCombinators($component->getCombinators());
+ if ($complex->isUseless()) {
+ continue;
+ }
+
+ $result ??= [];
+ $result[] = $complex;
+ }
+
+ return $result;
+ }
+
+ // Find all paths through $options. In this case, each path represents a
+ // different unification of the base selector. For example, if we have:
+ //
+ // .a.b {...}
+ // .w .x {@extend .a}
+ // .y .z {@extend .b}
+ //
+ // then $options is `[[.a, .w .x], [.b, .y .z]]` and `paths($options)` is
+ //
+ // [
+ // [.a, .b],
+ // [.a, .y .z],
+ // [.w .x, .b],
+ // [.w .x, .y .z]
+ // ]
+ //
+ // We then unify each path to get a list of complex selectors:
+ //
+ // [
+ // [.a.b],
+ // [.y .a.z],
+ // [.w .x.b],
+ // [.w .y .x.z, .y .w .x.z]
+ // ]
+ //
+ // And finally flatten them to get:
+ //
+ // [
+ // .a.b,
+ // .y .a.z,
+ // .w .x.b,
+ // .w .y .x.z,
+ // .y .w .x.z
+ // ]
+ $extenderPaths = ExtendUtil::paths($options);
+
+ $result = [];
+
+ if ($this->mode !== ExtendMode::replace) {
+ // The first path is always the original selector. We can't just return
+ // $component directly because selector pseudos may be modified, but we
+ // don't have to do any unification.
+ $result[] = new ComplexSelector([], [new ComplexSelectorComponent(
+ new CompoundSelector(iterator_to_array(self::expandIterable($extenderPaths[0], function (Extender $extender) {
+ \assert(\count($extender->selector->getComponents()) === 1);
+ return ListUtil::last($extender->selector->getComponents())->getSelector()->getComponents();
+ }), false), $component->getSelector()->getSpan()),
+ $component->getCombinators(),
+ $component->getSpan(),
+ )], $component->getSpan());
+ }
+
+ foreach (array_slice($extenderPaths, $this->mode === ExtendMode::replace ? 0 : 1) as $path) {
+ $extended = $this->unifyExtenders($path, $mediaQueryContext, $component->getSpan());
+
+ if ($extended === null) {
+ continue;
+ }
+
+ foreach ($extended as $complex) {
+ $withCombinators = $complex->withAdditionalCombinators($component->getCombinators());
+
+ if (!$withCombinators->isUseless()) {
+ $result[] = $withCombinators;
+ }
+ }
+ }
+
+ // If we're preserving the original selector, mark the first unification as
+ // such so {@see trim} doesn't get rid of it.
+ $isOriginal = fn (ComplexSelector $complex) => false;
+ if ($inOriginal && $this->mode !== ExtendMode::replace) {
+ $original = $result[0];
+ $isOriginal = fn (ComplexSelector $complex) => EquatableUtil::equals($complex, $original);
+ }
+
+ return $this->trim($result, $isOriginal);
+ }
+
+ /**
+ * Returns a list of {@see ComplexSelector}s that match the intersection of
+ * elements matched by all of $extenders' selectors.
+ *
+ * The $span will be used for the new selectors.
+ *
+ * @param list<Extender> $extenders
+ * @param list<CssMediaQuery>|null $mediaQueryContext
+ * @return list<ComplexSelector>|null
+ */
+ private function unifyExtenders(array $extenders, ?array $mediaQueryContext, FileSpan $span): ?array
+ {
+ $toUnify = [];
+ $originals = null;
+ $originalsLineBreak = false;
+
+ foreach ($extenders as $extender) {
+ if ($extender->isOriginal) {
+ $originals ??= [];
+ $finalExtenderComponent = ListUtil::last($extender->selector->getComponents());
+ \assert(\count($finalExtenderComponent->getCombinators()) === 0);
+
+ foreach ($finalExtenderComponent->getSelector()->getComponents() as $component) {
+ $originals[] = $component;
+ }
+ $originalsLineBreak = $originalsLineBreak || $extender->selector->getLineBreak();
+ } elseif ($extender->selector->isUseless()) {
+ return null;
+ } else {
+ $toUnify[] = $extender->selector;
+ }
+ }
+
+ if ($originals !== null) {
+ array_unshift($toUnify, new ComplexSelector([], [
+ new ComplexSelectorComponent(new CompoundSelector($originals, $span), [], $span),
+ ], $span, $originalsLineBreak));
+ }
+
+ $complexes = ExtendUtil::unifyComplex($toUnify, $span);
+ if ($complexes === null) {
+ return null;
+ }
+
+ foreach ($extenders as $extender) {
+ $extender->assertCompatibleMediaContext($mediaQueryContext);
+ }
+
+ return $complexes;
+ }
+
+ /**
+ * @param SimpleSelectorMap<ComplexSelectorMap<Extension>> $extensions
+ * @param list<CssMediaQuery>|null $mediaQueryContext
+ * @param SimpleSelectorMap<mixed>|null $targetsUsed
+ * @return list<list<Extender>>|null
+ */
+ private function extendSimple(SimpleSelector $simple, SimpleSelectorMap $extensions, ?array $mediaQueryContext, ?SimpleSelectorMap $targetsUsed): ?array
+ {
+ // Extends $simple without extending the contents of any selector pseudos
+ // it contains.
+ $withoutPseudo = function (SimpleSelector $simple) use ($extensions, $targetsUsed) {
+ $extensionsForSimple = $extensions[$simple] ?? null;
+
+ if ($extensionsForSimple === null) {
+ return null;
+ }
+ $targetsUsed?->attach($simple);
+
+ $result = [];
+
+ if ($this->mode !== ExtendMode::replace) {
+ $result[] = $this->extenderForSimple($simple);
+ }
+
+ /** @var Extension $extension */
+ foreach ($extensionsForSimple->getValues() as $extension) {
+ $result[] = $extension->extender;
+ }
+
+ return $result;
+ };
+
+ if ($simple instanceof PseudoSelector && $simple->getSelector() !== null) {
+ $extended = $this->extendPseudo($simple, $extensions, $mediaQueryContext);
+
+ if ($extended !== null) {
+ return array_map(fn ($pseudo) => $withoutPseudo($pseudo) ?? [$this->extenderForSimple($pseudo)], $extended);
+ }
+ }
+
+ $result = $withoutPseudo($simple);
+
+ if ($result === null) {
+ return null;
+ }
+
+ return [$result];
+ }
+
+ /**
+ * Returns an {@see Extender} composed solely of a compound selector containing
+ * $simples.
+ *
+ * @param list<SimpleSelector> $simples
+ */
+ private function extenderForCompound(array $simples, FileSpan $span): Extender
+ {
+ $compound = new CompoundSelector($simples, $span);
+
+ return Extender::create(
+ new ComplexSelector([], [new ComplexSelectorComponent($compound, [], $span)], $span),
+ $this->sourceSpecificityFor($compound),
+ true,
+ );
+ }
+
+ /**
+ * Returns an {@see Extender} composed solely of $simple.
+ */
+ private function extenderForSimple(SimpleSelector $simple): Extender
+ {
+ return Extender::create(
+ new ComplexSelector([], [new ComplexSelectorComponent(
+ new CompoundSelector([$simple], $simple->getSpan()),
+ [],
+ $simple->getSpan(),
+ )], $simple->getSpan()),
+ $this->sourceSpecificity[$simple] ?? 0,
+ true,
+ );
+ }
+
+ /**
+ * Extends $pseudo using $extensions, and returns a list of resulting
+ * pseudo selectors.
+ *
+ * This requires that $pseudo have a selector argument.
+ *
+ * @param SimpleSelectorMap<ComplexSelectorMap<Extension>> $extensions
+ * @param list<CssMediaQuery>|null $mediaQueryContext
+ * @return list<PseudoSelector>|null
+ */
+ private function extendPseudo(PseudoSelector $pseudo, SimpleSelectorMap $extensions, ?array $mediaQueryContext): ?array
+ {
+ $selector = $pseudo->getSelector();
+
+ if ($selector === null) {
+ throw new \InvalidArgumentException("Selector $pseudo must have a selector argument.");
+ }
+
+ $extended = $this->extendList($selector, $extensions, $mediaQueryContext);
+
+ if ($extended === $selector) {
+ return null;
+ }
+
+ // For `:not()`, we usually want to get rid of any complex selectors because
+ // that will cause the selector to fail to parse on all browsers at time of
+ // writing. We can keep them if either the original selector had a complex
+ // selector, or the result of extending has only complex selectors, because
+ // either way we aren't breaking anything that isn't already broken.
+ $complexes = $extended->getComponents();
+ if (
+ $pseudo->getNormalizedName() === 'not'
+ && !IterableUtil::any($selector->getComponents(), fn ($complex) => \count($complex->getComponents()) > 1)
+ && IterableUtil::any($extended->getComponents(), fn ($complex) => \count($complex->getComponents()) === 1)
+ ) {
+ $complexes = array_filter($extended->getComponents(), fn ($complex) => \count($complex->getComponents()) <= 1);
+ }
+
+ $complexes = iterator_to_array(self::expandIterable($complexes, function (ComplexSelector $complex) use ($pseudo) {
+ $innerPseudo = $complex->getSingleCompound()?->getSingleSimple();
+ if (!$innerPseudo instanceof PseudoSelector) {
+ return [$complex];
+ }
+
+ $innerSelector = $innerPseudo->getSelector();
+ if ($innerSelector === null) {
+ return [$complex];
+ }
+
+ switch ($pseudo->getNormalizedName()) {
+ case 'not':
+ // In theory, if there's a `:not` nested within another `:not`, the
+ // inner `:not`'s contents should be unified with the return value.
+ // For example, if `:not(.foo)` extends `.bar`, `:not(.bar)` should
+ // become `.foo:not(.bar)`. However, this is a narrow edge case and
+ // supporting it properly would make this code and the code calling it
+ // a lot more complicated, so it's not supported for now.
+ if (!\in_array($innerPseudo->getNormalizedName(), ['is', 'matches', 'where'], true)) {
+ return [];
+ }
+
+ return $innerSelector->getComponents();
+
+ case 'is':
+ case 'matches':
+ case 'where':
+ case 'any':
+ case 'current':
+ case 'nth-child':
+ case 'nth-last-child':
+ // As above, we could theoretically support :not within :matches, but
+ // doing so would require this method and its callers to handle much
+ // more complex cases that likely aren't worth the pain.
+ if ($innerPseudo->getName() !== $pseudo->getName()) {
+ return [];
+ }
+ if ($innerPseudo->getArgument() !== $pseudo->getArgument()) {
+ return [];
+ }
+
+ return $innerSelector->getComponents();
+
+ case 'has':
+ case 'host':
+ case 'host-context':
+ case 'slotted':
+ // We can't expand nested selectors here, because each layer adds an
+ // additional layer of semantics. For example, `:has(:has(img))`
+ // doesn't match `<div><img></div>` but `:has(img)` does.
+ return [$complex];
+
+ default:
+ return [];
+ }
+ }), false);
+
+ // Older browsers support `:not`, but only with a single complex selector.
+ // In order to support those browsers, we break up the contents of a `:not`
+ // unless it originally contained a selector list.
+ if ($pseudo->getNormalizedName() === 'not' && \count($selector->getComponents()) === 1) {
+ $result = array_map(fn (ComplexSelector $complex) => $pseudo->withSelector(new SelectorList([$complex], $selector->getSpan())), $complexes);
+
+ return \count($result) === 0 ? null : $result;
+ }
+
+ return [$pseudo->withSelector(new SelectorList($complexes, $selector->getSpan()))];
+ }
+
+ /**
+ * @template E
+ * @template T
+ * @param iterable<E> $elements
+ * @param callable(E): iterable<T> $callback
+ * @return \Traversable<T>
+ *
+ * @param-immediately-invoked-callable $callback
+ */
+ private static function expandIterable(iterable $elements, callable $callback): \Traversable
+ {
+ foreach ($elements as $element) {
+ yield from $callback($element);
+ }
+ }
+
+ /**
+ * Removes elements from $selectors if they're subselectors of other
+ * elements.
+ *
+ * The $isOriginal callback indicates which selectors are original to the
+ * document, and thus should never be trimmed.
+ *
+ * @param list<ComplexSelector> $selectors
+ * @param callable(ComplexSelector): bool $isOriginal
+ * @return list<ComplexSelector>
+ *
+ * @param-immediately-invoked-callable $isOriginal
+ */
+ private function trim(array $selectors, callable $isOriginal): array
+ {
+ // This is n² on the sequences, but only comparing between separate
+ // sequences should limit the quadratic behavior. We iterate from last to
+ // first and reverse the result so that, if two selectors are identical, we
+ // keep the first one.
+ /** @var list<ComplexSelector> $result */
+ $result = [];
+ $numOriginals = 0;
+
+ for ($i = \count($selectors) - 1; $i >= 0; $i--) {
+ $complex1 = $selectors[$i];
+
+ if ($isOriginal($complex1)) {
+ // Make sure we don't include duplicate originals, which could happen if
+ // a style rule extends a component of its own selector.
+ for ($j = 0; $j < $numOriginals; $j++) {
+ if (EquatableUtil::equals($result[$j], $complex1)) {
+ // Rotates the slice one index higher
+ $element = $result[$j];
+ for ($k = 0; $k <= $j; $k++) {
+ $next = $result[$k];
+ $result[$k] = $element;
+ $element = $next;
+ }
+ // Rotating the slice preserves the list status of the array, but phpstan does not recognize it.
+ \assert(array_is_list($result));
+
+ continue 2;
+ }
+ }
+
+ $numOriginals++;
+ array_unshift($result, $complex1);
+ continue;
+ }
+
+ // The maximum specificity of the sources that caused $complex1 to be
+ // generated. In order for $complex1 to be removed, there must be another
+ // selector that's a superselector of it *and* that has specificity
+ // greater or equal to this.
+ $maxSpecificity = 0;
+ foreach ($complex1->getComponents() as $component) {
+ $maxSpecificity = max($maxSpecificity, $this->sourceSpecificityFor($component->getSelector()));
+ }
+
+ // Look in $result rather than $selectors for selectors after $i. This
+ // ensures that we aren't comparing against a selector that's already been
+ // trimmed, and thus that if there are two identical selectors only one is
+ // trimmed.
+ if (IterableUtil::any($result, fn (ComplexSelector $complex2) => $complex2->getSpecificity() >= $maxSpecificity && $complex2->isSuperselector($complex1))) {
+ continue;
+ }
+
+ if (IterableUtil::any(array_slice($selectors, 0, $i), fn (ComplexSelector $complex2) => $complex2->getSpecificity() >= $maxSpecificity && $complex2->isSuperselector($complex1))) {
+ continue;
+ }
+
+ array_unshift($result, $complex1);
+ }
+
+ return $result;
+ }
+
+ /**
+ * Returns the maximum specificity for sources that went into producing
+ * $compound.
+ */
+ private function sourceSpecificityFor(CompoundSelector $compound): int
+ {
+ $specificity = 0;
+
+ foreach ($compound->getComponents() as $simple) {
+ $specificity = max($specificity, $this->sourceSpecificity[$simple] ?? 0);
+ }
+
+ return $specificity;
+ }
+
+ public function clone(): array
+ {
+ /** @var SimpleSelectorMap<ObjectSet<ModifiableBox<SelectorList>>> $newSelectors */
+ $newSelectors = new SimpleSelectorMap();
+ /** @var \SplObjectStorage<ModifiableBox<SelectorList>, list<CssMediaQuery>> $newMediaContexts */
+ $newMediaContexts = new \SplObjectStorage();
+ /** @var \SplObjectStorage<SelectorList, Box<SelectorList>> $oldToNewSelectors */
+ $oldToNewSelectors = new \SplObjectStorage();
+
+ foreach ($this->selectors as $simple) {
+ $selectors = $this->selectors->getInfo();
+
+ /** @var ObjectSet<ModifiableBox<SelectorList>> $newSelectorSet */
+ $newSelectorSet = new ObjectSet();
+ $newSelectors[$simple] = $newSelectorSet;
+
+ foreach ($selectors as $selector) {
+ $newSelector = new ModifiableBox($selector->getValue());
+ $newSelectorSet->add($newSelector);
+ $oldToNewSelectors[$selector->getValue()] = $newSelector->seal();
+
+ if (isset($this->mediaContexts[$selector])) {
+ $newMediaContexts[$newSelector] = $this->mediaContexts[$selector];
+ }
+ }
+ }
+
+ /** @var SimpleSelectorMap<ComplexSelectorMap<Extension>> $newExtensions */
+ $newExtensions = new SimpleSelectorMap();
+ foreach ($this->extensions as $simple) {
+ $newExtensions[$simple] = clone $this->extensions->getInfo();
+ }
+
+ return [new ConcreteExtensionStore(
+ $newSelectors,
+ $newExtensions,
+ clone $this->extensionsByExtender,
+ $newMediaContexts,
+ clone $this->sourceSpecificity,
+ clone $this->originals,
+ ExtendMode::normal,
+ ), $oldToNewSelectors];
+ }
+}
diff --git a/vendor/scssphp/scssphp/src/Extend/EmptyExtensionStore.php b/vendor/scssphp/scssphp/src/Extend/EmptyExtensionStore.php
new file mode 100644
index 000000000..431cd1f08
--- /dev/null
+++ b/vendor/scssphp/scssphp/src/Extend/EmptyExtensionStore.php
@@ -0,0 +1,66 @@
+<?php
+
+/**
+ * SCSSPHP
+ *
+ * @copyright 2012-2020 Leaf Corcoran
+ *
+ * @license http://opensource.org/licenses/MIT MIT
+ *
+ * @link http://scssphp.github.io/scssphp
+ */
+
+namespace ScssPhp\ScssPhp\Extend;
+
+use ScssPhp\ScssPhp\Ast\Sass\Statement\ExtendRule;
+use ScssPhp\ScssPhp\Ast\Selector\SelectorList;
+use ScssPhp\ScssPhp\Ast\Selector\SimpleSelector;
+use ScssPhp\ScssPhp\Extend\ExtensionStore;
+use ScssPhp\ScssPhp\Util\Box;
+
+/**
+ * An {@see ExtensionStore} that contains no extensions and can have no extensions
+ * added.
+ *
+ * @internal
+ */
+final class EmptyExtensionStore implements ExtensionStore
+{
+ public function isEmpty(): bool
+ {
+ return true;
+ }
+
+ public function getSimpleSelectors(): array
+ {
+ return [];
+ }
+
+ public function extensionsWhereTarget(callable $callback): iterable
+ {
+ return [];
+ }
+
+ public function addSelector(SelectorList $selector, ?array $mediaContext): Box
+ {
+ throw new \BadMethodCallException("addSelector() can't be called for a const ExtensionStore.");
+ }
+
+ public function addExtension(SelectorList $extender, SimpleSelector $target, ExtendRule $extend, ?array $mediaContext): void
+ {
+ throw new \BadMethodCallException("addExtension() can't be called for a const ExtensionStore.");
+ }
+
+ public function addExtensions(iterable $extensionStores): void
+ {
+ throw new \BadMethodCallException("addExtensions() can't be called for a const ExtensionStore.");
+ }
+
+ public function clone(): array
+ {
+ /** @var \SplObjectStorage<SelectorList, Box<SelectorList>> $map */
+ $map = new \SplObjectStorage();
+
+ return [new EmptyExtensionStore(), $map];
+ }
+}
diff --git a/vendor/scssphp/scssphp/src/Extend/ExtendMode.php b/vendor/scssphp/scssphp/src/Extend/ExtendMode.php
new file mode 100644
index 000000000..fb39b78d8
--- /dev/null
+++ b/vendor/scssphp/scssphp/src/Extend/ExtendMode.php
@@ -0,0 +1,44 @@
+<?php
+
+/**
+ * SCSSPHP
+ *
+ * @copyright 2012-2020 Leaf Corcoran
+ *
+ * @license http://opensource.org/licenses/MIT MIT
+ *
+ * @link http://scssphp.github.io/scssphp
+ */
+
+namespace ScssPhp\ScssPhp\Extend;
+
+/**
+ * Different modes in which extension can run.
+ *
+ * @internal
+ */
+enum ExtendMode
+{
+ /**
+ * Normal mode, used with the `@extend` rule.
+ *
+ * This preserves existing selectors and extends each target individually.
+ */
+ case normal;
+
+ /**
+ * Replace mode, used by the `selector-replace()` function.
+ *
+ * This replaces existing selectors and requires every target to match to
+ * extend a given compound selector.
+ */
+ case replace;
+
+ /**
+ * All-targets mode, used by the `selector-extend()` function.
+ *
+ * This preserves existing selectors but requires every target to match to
+ * extend a given compound selector.
+ */
+ case allTargets;
+}
diff --git a/vendor/scssphp/scssphp/src/Extend/ExtendUtil.php b/vendor/scssphp/scssphp/src/Extend/ExtendUtil.php
new file mode 100644
index 000000000..0a33f5af0
--- /dev/null
+++ b/vendor/scssphp/scssphp/src/Extend/ExtendUtil.php
@@ -0,0 +1,1292 @@
+<?php
+
+/**
+ * SCSSPHP
+ *
+ * @copyright 2012-2020 Leaf Corcoran
+ *
+ * @license http://opensource.org/licenses/MIT MIT
+ *
+ * @link http://scssphp.github.io/scssphp
+ */
+
+namespace ScssPhp\ScssPhp\Extend;
+
+use ScssPhp\ScssPhp\Ast\Css\CssValue;
+use ScssPhp\ScssPhp\Ast\Selector\Combinator;
+use ScssPhp\ScssPhp\Ast\Selector\ComplexSelector;
+use ScssPhp\ScssPhp\Ast\Selector\ComplexSelectorComponent;
+use ScssPhp\ScssPhp\Ast\Selector\CompoundSelector;
+use ScssPhp\ScssPhp\Ast\Selector\IDSelector;
+use ScssPhp\ScssPhp\Ast\Selector\PlaceholderSelector;
+use ScssPhp\ScssPhp\Ast\Selector\PseudoSelector;
+use ScssPhp\ScssPhp\Ast\Selector\QualifiedName;
+use ScssPhp\ScssPhp\Ast\Selector\SelectorList;
+use ScssPhp\ScssPhp\Ast\Selector\SimpleSelector;
+use ScssPhp\ScssPhp\Ast\Selector\TypeSelector;
+use ScssPhp\ScssPhp\Ast\Selector\UniversalSelector;
+use ScssPhp\ScssPhp\Util\EquatableUtil;
+use ScssPhp\ScssPhp\Util\IterableUtil;
+use ScssPhp\ScssPhp\Util\ListUtil;
+use ScssPhp\ScssPhp\Util\SpanUtil;
+use SourceSpan\FileSpan;
+
+/**
+ * @internal
+ */
+final class ExtendUtil
+{
+ /**
+ * Pseudo-selectors that can only meaningfully appear in the first component of
+ * a complex selector.
+ */
+ private const ROOTISH_PSEUDO_CLASSES = ['root', 'scope', 'host', 'host-context'];
+
+ /**
+ * Returns the contents of a {@see SelectorList} that matches only elements that are
+ * matched by every complex selector in $complexes.
+ *
+ * If no such list can be produced, returns `null`.
+ *
+ * @param list<ComplexSelector> $complexes
+ *
+ * @return list<ComplexSelector>|null
+ */
+ public static function unifyComplex(array $complexes, FileSpan $span): ?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 && !EquatableUtil::equals($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->getSpan(), $complex->getLineBreak());
+ }
+
+ if ($complex->getLineBreak()) {
+ $hasLineBreak = true;
+ }
+ }
+
+ \assert($unifiedBase !== null);
+
+ $base = new ComplexSelector(
+ $leadingCombinator === null ? [] : [$leadingCombinator],
+ [new ComplexSelectorComponent(new CompoundSelector($unifiedBase, $span), $trailingCombinator === null ? [] : [$trailingCombinator], $span)],
+ $span,
+ $hasLineBreak
+ );
+
+ return self::weave($withoutBases === [] ? [$base] : array_merge(ListUtil::exceptLast($withoutBases), [ListUtil::last($withoutBases)->concatenate($base, $span)]), $span);
+ }
+
+ /**
+ * 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`.
+ */
+ public static function unifyCompound(CompoundSelector $compound1, CompoundSelector $compound2): ?CompoundSelector
+ {
+ $result = $compound2->getComponents();
+
+ foreach ($compound1->getComponents() as $simple) {
+ $unified = $simple->unify($result);
+
+ if ($unified === null) {
+ return null;
+ }
+
+ $result = $unified;
+ }
+
+ return new CompoundSelector($result, $compound1->getSpan());
+ }
+
+ /**
+ * 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
+ {
+ [$namespace1, $name1] = self::namespaceAndName($selector1, 'selector1');
+ [$namespace2, $name2] = self::namespaceAndName($selector2, 'selector2');
+
+ 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($selector1->getSpan(), $namespace);
+ }
+
+ return new TypeSelector(new QualifiedName($name, $namespace), $selector1->getSpan());
+ }
+
+ /**
+ * Returns the namespace and name for $selector, which must be a
+ * {@see UniversalSelector} or a {@see TypeSelector}.
+ *
+ * The $name parameter is used for error reporting.
+ *
+ * @return array{string|null, string|null} The namespace and the name
+ */
+ private static function namespaceAndName(SimpleSelector $selector, string $name): array
+ {
+ if ($selector instanceof UniversalSelector) {
+ return [$selector->getNamespace(), null];
+ }
+
+ if ($selector instanceof TypeSelector) {
+ return [$selector->getName()->getNamespace(), $selector->getName()->getName()];
+ }
+
+ throw new \InvalidArgumentException("Argument $name must be a UniversalSelector or a TypeSelector.");
+ }
+
+ /**
+ * 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]`.
+ *
+ * The $span will be used for any new combined selectors.
+ *
+ * If $forceLineBreak is `true`, this will mark all returned complex selectors
+ * as having line breaks.
+ *
+ * @param list<ComplexSelector> $complexes
+ *
+ * @return list<ComplexSelector>
+ */
+ public static function weave(array $complexes, FileSpan $span, bool $forceLineBreak = false): array
+ {
+ if (\count($complexes) === 1) {
+ $complex = $complexes[0];
+
+ if (!$forceLineBreak || $complex->getLineBreak()) {
+ return $complexes;
+ }
+
+ return [
+ new ComplexSelector($complex->getLeadingCombinators(), $complex->getComponents(), $complex->getSpan(), true),
+ ];
+ }
+
+ $prefixes = [$complexes[0]];
+
+ foreach (array_slice($complexes, 1) as $complex) {
+ if (\count($complex->getComponents()) === 1) {
+ foreach ($prefixes as $i => $prefix) {
+ $prefixes[$i] = $prefix->concatenate($complex, $span, $forceLineBreak);
+ }
+
+ continue;
+ }
+
+ $newPrefixes = [];
+
+ foreach ($prefixes as $prefix) {
+ foreach (self::weaveParents($prefix, $complex, $span) ?? [] as $parentPrefix) {
+ $newPrefixes[] = $parentPrefix->withAdditionalComponent(ListUtil::last($complex->getComponents()), $span, $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.
+ *
+ * The $span will be used for any new combined selectors.
+ *
+ * Returns `null` if this intersection is empty.
+ *
+ * @return list<ComplexSelector>|null
+ */
+ private static function weaveParents(ComplexSelector $prefix, ComplexSelector $base, FileSpan $span): ?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, $span);
+ 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(), $rootish2->getSelector());
+
+ if ($rootish === null) {
+ return null;
+ }
+
+ array_unshift($queue1, new ComplexSelectorComponent($rootish, $rootish1->getCombinators(), $rootish1->getSpan()));
+ array_unshift($queue2, new ComplexSelectorComponent($rootish, $rootish2->getCombinators(), $rootish2->getSpan()));
+ } 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;
+ \assert($rootish !== null);
+ array_unshift($queue1, $rootish);
+ array_unshift($queue2, $rootish);
+ }
+
+ $groups1 = self::groupSelectors($queue1);
+ $groups2 = self::groupSelectors($queue2);
+
+ /** @var list<list<ComplexSelectorComponent>> $lcs */
+ $lcs = ListUtil::longestCommonSubsequence($groups2, $groups1, function ($group1, $group2) use ($span) {
+ 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, $span), new ComplexSelector([], $group2, $span)], $span);
+
+ if ($unified === null) {
+ return null;
+ }
+ if (\count($unified) > 1) {
+ return null;
+ }
+
+ return $unified[0]->getComponents();
+ });
+
+ $choices = [];
+
+ foreach ($lcs as $group) {
+ $newChoice = [];
+ /** @var list<list<list<ComplexSelectorComponent>>> $chunks */
+ $chunks = self::chunks($groups1, $groups2, fn($sequence) => self::complexIsParentSuperselector($sequence[0], $group));
+ foreach ($chunks as $chunk) {
+ $flattened = [];
+ foreach ($chunk as $chunkGroup) {
+ $flattened = array_merge($flattened, $chunkGroup);
+ }
+ $newChoice[] = $flattened;
+ }
+
+ /** @var list<list<ComplexSelectorComponent>> $groups1 */
+ /** @var list<list<ComplexSelectorComponent>> $groups2 */
+ $choices[] = $newChoice;
+ $choices[] = [$group];
+ array_shift($groups1);
+ array_shift($groups2);
+ }
+
+ $newChoice = [];
+ /** @var list<list<list<ComplexSelectorComponent>>> $chunks */
+ $chunks = self::chunks($groups1, $groups2, fn($sequence) => 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, fn($choice) => $choice !== []);
+
+ $paths = self::paths($choices);
+
+ return array_map(function (array $path) use ($leadingCombinators, $prefix, $base, $span) {
+ $result = [];
+
+ foreach ($path as $group) {
+ $result = array_merge($result, $group);
+ }
+
+ return new ComplexSelector($leadingCombinators, $result, $span, $prefix->getLineBreak() || $base->getLineBreak());
+ }, $paths);
+ }
+
+ /**
+ * If the first element of $queue has a `:root` selector, removes and returns
+ * that element.
+ *
+ * @param list<ComplexSelectorComponent> $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<CssValue<Combinator>>|null $combinators1
+ * @param list<CssValue<Combinator>>|null $combinators2
+ *
+ * @return list<CssValue<Combinator>>|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`.
+ *
+ * The $span will be used for any new combined selectors.
+ *
+ * @param list<ComplexSelectorComponent> $components1
+ * @param list<ComplexSelectorComponent> $components2
+ * @param list<list<list<ComplexSelectorComponent>>> $result
+ *
+ * @return list<list<list<ComplexSelectorComponent>>>|null
+ */
+ private static function mergeTrailingCombinators(array &$components1, array &$components2, FileSpan $span, 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->getValue() === Combinator::FOLLOWING_SIBLING && $combinator2->getValue() === 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(), $component2->getSelector());
+
+ if ($unified !== null) {
+ $choices[] = [new ComplexSelectorComponent($unified, [$combinator1], $span)];
+ }
+
+ array_unshift($result, $choices);
+ }
+ } elseif (($combinator1->getValue() === Combinator::FOLLOWING_SIBLING && $combinator2->getValue() === Combinator::NEXT_SIBLING) || ($combinator1->getValue() === Combinator::NEXT_SIBLING && $combinator2->getValue() === Combinator::FOLLOWING_SIBLING)) {
+ $followingSiblingComponent = $combinator1->getValue() === Combinator::FOLLOWING_SIBLING ? $component1 : $component2;
+ $nextSiblingComponent = $combinator1->getValue() === Combinator::FOLLOWING_SIBLING ? $component2 : $component1;
+
+ if ($followingSiblingComponent->getSelector()->isSuperselector($nextSiblingComponent->getSelector())) {
+ array_unshift($result, [[$nextSiblingComponent]]);
+ } else {
+ $unified = self::unifyCompound($followingSiblingComponent->getSelector(), $nextSiblingComponent->getSelector());
+
+ $choices = [
+ [$followingSiblingComponent, $nextSiblingComponent],
+ ];
+
+ if ($unified !== null) {
+ $choices[] = [new ComplexSelectorComponent($unified, $nextSiblingComponent->getCombinators(), $span)];
+ }
+
+ array_unshift($result, $choices);
+ }
+ } elseif ($combinator1->getValue() === Combinator::CHILD && ($combinator2->getValue() === Combinator::NEXT_SIBLING || $combinator2->getValue() === Combinator::FOLLOWING_SIBLING)) {
+ array_unshift($result, [[$component2]]);
+ $components1[] = $component1;
+ } elseif ($combinator2->getValue() === Combinator::CHILD && ($combinator1->getValue() === Combinator::NEXT_SIBLING || $combinator1->getValue() === Combinator::FOLLOWING_SIBLING)) {
+ array_unshift($result, [[$component1]]);
+ $components2[] = $component2;
+ } elseif (EquatableUtil::equals($combinator1, $combinator2)) {
+ $unified = self::unifyCompound($component1->getSelector(), $component2->getSelector());
+
+ if ($unified === null) {
+ return null;
+ }
+
+ array_unshift($result, [[new ComplexSelectorComponent($unified, [$combinator1], $span)]]);
+ } else {
+ return null;
+ }
+
+ return self::mergeTrailingCombinators($components1, $components2, $span, $result);
+ }
+
+ if ($combinator1 !== null) {
+ $component1 = array_pop($components1);
+ \assert($component1 instanceof ComplexSelectorComponent);
+
+ if ($combinator1->getValue() === Combinator::CHILD && \count($components2) > 0 && ListUtil::last($components2)->getSelector()->isSuperselector($component1->getSelector())) {
+ array_pop($components2);
+ }
+
+ array_unshift($result, [[$component1]]);
+
+ return self::mergeTrailingCombinators($components1, $components2, $span, $result);
+ }
+
+ $component2 = array_pop($components2);
+ \assert($component2 instanceof ComplexSelectorComponent);
+ assert($combinator2 !== null);
+
+ if ($combinator2->getValue() === Combinator::CHILD && \count($components1) > 0 && ListUtil::last($components1)->getSelector()->isSuperselector($component2->getSelector())) {
+ array_pop($components1);
+ }
+
+ array_unshift($result, [[$component2]]);
+
+ return self::mergeTrailingCombinators($components1, $components2, $span, $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<ComplexSelectorComponent> $complex1
+ * @param list<ComplexSelectorComponent> $complex2
+ */
+ 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::iterableContains($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<T> $queue1
+ * @param list<T> $queue2
+ * @param callable(list<T>): bool $done
+ *
+ * @return list<list<T>>
+ *
+ * @param-immediately-invoked-callable $done
+ */
+ 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<list<T>> $choices
+ *
+ * @return list<list<T>>
+ */
+ 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<ComplexSelectorComponent> $complex
+ *
+ * @return list<list<ComplexSelectorComponent>>
+ */
+ 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<ComplexSelector> $list1
+ * @param list<ComplexSelector> $list2
+ */
+ 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<ComplexSelectorComponent> $complex1
+ * @param list<ComplexSelectorComponent> $complex2
+ */
+ private static function complexIsParentSuperselector(array $complex1, array $complex2): bool
+ {
+ if (\count($complex1) > \count($complex2)) {
+ return false;
+ }
+
+ $bogusSpan = SpanUtil::bogusSpan();
+
+ $base = new ComplexSelectorComponent(new CompoundSelector([new PlaceholderSelector('<temp>', $bogusSpan)], $bogusSpan), [], $bogusSpan);
+ $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<ComplexSelectorComponent> $complex1
+ * @param list<ComplexSelectorComponent> $complex2
+ */
+ 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;
+ $previousCombinator = null;
+
+ 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) {
+ if (IterableUtil::any($complex2, fn (ComplexSelectorComponent $parent) => \count($parent->getCombinators()) > 1)) {
+ return false;
+ }
+
+ return self::compoundIsSuperselector(
+ $component1->getSelector(),
+ ListUtil::last($complex2)->getSelector(),
+ $component1->getSelector()->hasComplicatedSuperselectorSemantics() ? array_slice($complex2, $i2, -1) : null
+ );
+ }
+
+ // Find the first index $endOfSubselector in $complex2 such that
+ // `complex2.sublist(i2, endOfSubselector + 1)` is a subselector of
+ // `$component1->getSelector()`.
+ $endOfSubselector = $i2;
+ while (true) {
+ $component2 = $complex2[$endOfSubselector];
+ if (\count($component2->getCombinators()) > 1) {
+ return false;
+ }
+ if (self::compoundIsSuperselector($component1->getSelector(), $component2->getSelector(), $component1->getSelector()->hasComplicatedSuperselectorSemantics() ? array_slice($complex2, $i2, $endOfSubselector - $i2) : null)) {
+ 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;
+ }
+ }
+
+ if (!self::compatibleWithPreviousCombinator($previousCombinator, array_slice($complex2, $i2, $endOfSubselector - $i2))) {
+ return false;
+ }
+
+ $component2 = $complex2[$endOfSubselector];
+ $combinator1 = $component1->getCombinators()[0] ?? null;
+ $combinator2 = $component2->getCombinators()[0] ?? null;
+
+ if (!self::isSupercombinator($combinator1, $combinator2)) {
+ return false;
+ }
+
+ $i1++;
+ $i2 = $endOfSubselector + 1;
+ $previousCombinator = $combinator1;
+
+ if (\count($complex1) - $i1 === 1) {
+ if ($combinator1 !== null && $combinator1->getValue() === 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;
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * @param CssValue<Combinator>|null $previous
+ * @param list<ComplexSelectorComponent> $parents
+ */
+ private static function compatibleWithPreviousCombinator(?CssValue $previous, array $parents): bool
+ {
+ if ($parents === []) {
+ return true;
+ }
+
+ if ($previous === null) {
+ return true;
+ }
+
+ // The child and next sibling combinators require that the *immediate*
+ // following component be a superselector.
+ if ($previous->getValue() !== Combinator::FOLLOWING_SIBLING) {
+ return false;
+ }
+
+ // The following sibling combinator does allow intermediate components, but
+ // only if they're all siblings.
+ foreach ($parents as $component) {
+ $firstCombinator = $component->getCombinators()[0] ?? null;
+ $firstCombinatorValue = $firstCombinator?->getValue();
+
+ if ($firstCombinatorValue !== Combinator::FOLLOWING_SIBLING && $firstCombinatorValue !== Combinator::NEXT_SIBLING) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * Returns whether $combinator1 is a supercombinator of $combinator2.
+ *
+ * That is, whether `X $combinator1 Y` is a superselector of `X $combinator2 Y`.
+ *
+ * @param CssValue<Combinator>|null $combinator1
+ * @param CssValue<Combinator>|null $combinator2
+ */
+ private static function isSupercombinator(?CssValue $combinator1, ?CssValue $combinator2): bool
+ {
+ return EquatableUtil::equals($combinator1, $combinator2) || ($combinator1 === null && $combinator2 !== null && $combinator2->getValue() === Combinator::CHILD) || ($combinator1 !== null && $combinator1->getValue() === Combinator::FOLLOWING_SIBLING && $combinator2 !== null && $combinator2->getValue() === 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 list<ComplexSelectorComponent>|null $parents
+ */
+ public static function compoundIsSuperselector(CompoundSelector $compound1, CompoundSelector $compound2, ?array $parents = null): bool
+ {
+ if (!$compound1->hasComplicatedSuperselectorSemantics() && !$compound2->hasComplicatedSuperselectorSemantics()) {
+ if (\count($compound1->getComponents()) > \count($compound2->getComponents())) {
+ return false;
+ }
+
+ return IterableUtil::every(
+ $compound1->getComponents(),
+ fn (SimpleSelector $simple1) => IterableUtil::any($compound2->getComponents(), $simple1->isSuperselector(...))
+ );
+ }
+
+ // 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<SimpleSelector> $compound1
+ * @param list<SimpleSelector> $compound2
+ * @param list<ComplexSelectorComponent>|null $parents
+ */
+ private static function compoundComponentsIsSuperselector(array $compound1, array $compound2, ?array $parents = null): bool
+ {
+ if (\count($compound1) === 0) {
+ return true;
+ }
+
+ $bogusSpan = SpanUtil::bogusSpan();
+
+ if (\count($compound2) === 0) {
+ $compound2 = [new UniversalSelector($bogusSpan, '*')];
+ }
+
+ return self::compoundIsSuperselector(new CompoundSelector($compound1, $bogusSpan), new CompoundSelector($compound2, $bogusSpan), $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<ComplexSelectorComponent>|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, [], $compound2->getSpan());
+
+ 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/vendor/scssphp/scssphp/src/Extend/Extender.php b/vendor/scssphp/scssphp/src/Extend/Extender.php
new file mode 100644
index 000000000..ceaf84d39
--- /dev/null
+++ b/vendor/scssphp/scssphp/src/Extend/Extender.php
@@ -0,0 +1,86 @@
+<?php
+
+/**
+ * SCSSPHP
+ *
+ * @copyright 2012-2020 Leaf Corcoran
+ *
+ * @license http://opensource.org/licenses/MIT MIT
+ *
+ * @link http://scssphp.github.io/scssphp
+ */
+
+namespace ScssPhp\ScssPhp\Extend;
+
+use ScssPhp\ScssPhp\Ast\Css\CssMediaQuery;
+use ScssPhp\ScssPhp\Ast\Selector\ComplexSelector;
+use ScssPhp\ScssPhp\Exception\SimpleSassException;
+use ScssPhp\ScssPhp\Util\EquatableUtil;
+
+/**
+ * A selector that's extending another selector, such as `A` in `A {@extend B}`.
+ * @internal
+ */
+final class Extender
+{
+ public readonly ComplexSelector $selector;
+
+ /**
+ * The minimum specificity required for any selector generated from this
+ * extender.
+ */
+ public readonly int $specificity;
+
+ /**
+ * Whether this extender represents a selector that was originally in the
+ * document, rather than one defined with `@extend`.
+ */
+ public readonly bool $isOriginal;
+
+ /**
+ * The extension that created this Extender.
+ *
+ * Not all {@see Extender}s are created by extensions. Some simply represent the
+ * original selectors that exist in the document.
+ */
+ private readonly ?Extension $extension;
+
+ private function __construct(ComplexSelector $selector, ?int $specificity = null, bool $original = false, ?Extension $extension = null)
+ {
+ $this->selector = $selector;
+ $this->specificity = $specificity ?? $selector->getSpecificity();
+ $this->isOriginal = $original;
+ $this->extension = $extension;
+ }
+
+ public static function create(ComplexSelector $selector, ?int $specificity = null, bool $original = false): self
+ {
+ return new Extender($selector, $specificity, $original);
+ }
+
+ public static function forExtension(ComplexSelector $selector, Extension $extension): self
+ {
+ return new Extender($selector, extension: $extension);
+ }
+
+ /**
+ * @param list<CssMediaQuery>|null $mediaContext
+ */
+ public function assertCompatibleMediaContext(?array $mediaContext): void
+ {
+ if ($this->extension === null) {
+ return;
+ }
+
+ $expectedMediaContext = $this->extension->mediaContext;
+ if ($expectedMediaContext === null) {
+ return;
+ }
+
+ if ($mediaContext !== null && EquatableUtil::listEquals($expectedMediaContext, $mediaContext)) {
+ return;
+ }
+
+ throw new SimpleSassException('You may not @extend selectors across media queries.', $this->extension->span);
+ }
+}
diff --git a/vendor/scssphp/scssphp/src/Extend/Extension.php b/vendor/scssphp/scssphp/src/Extend/Extension.php
new file mode 100644
index 000000000..6de0c281b
--- /dev/null
+++ b/vendor/scssphp/scssphp/src/Extend/Extension.php
@@ -0,0 +1,68 @@
+<?php
+
+/**
+ * SCSSPHP
+ *
+ * @copyright 2012-2020 Leaf Corcoran
+ *
+ * @license http://opensource.org/licenses/MIT MIT
+ *
+ * @link http://scssphp.github.io/scssphp
+ */
+
+namespace ScssPhp\ScssPhp\Extend;
+
+use ScssPhp\ScssPhp\Ast\Css\CssMediaQuery;
+use ScssPhp\ScssPhp\Ast\Selector\ComplexSelector;
+use ScssPhp\ScssPhp\Ast\Selector\SimpleSelector;
+use SourceSpan\FileSpan;
+
+/**
+ * The state of an extension for a given extender.
+ *
+ * The target of the extension is represented externally, in the map that
+ * contains this extender.
+ *
+ * @internal
+ */
+class Extension
+{
+ /**
+ * The extender (such as `A` in `A {@extend B}`).
+ */
+ public readonly Extender $extender;
+
+ /**
+ * The selector that's being extended.
+ */
+ public readonly SimpleSelector $target;
+
+ /**
+ * The media query context to which this extension is restricted, or `null`
+ * if it can apply within any context.
+ *
+ * @var list<CssMediaQuery>|null
+ */
+ public readonly ?array $mediaContext;
+
+ public readonly bool $isOptional;
+
+ public readonly FileSpan $span;
+
+ /**
+ * @param list<CssMediaQuery>|null $mediaContext
+ */
+ public function __construct(ComplexSelector $extender, SimpleSelector $target, FileSpan $span, ?array $mediaContext = null, bool $optional = false)
+ {
+ $this->extender = Extender::forExtension($extender, $this);
+ $this->target = $target;
+ $this->mediaContext = $mediaContext;
+ $this->isOptional = $optional;
+ $this->span = $span;
+ }
+
+ public function withExtender(ComplexSelector $newExtender): Extension
+ {
+ return new Extension($newExtender, $this->target, $this->span, $this->mediaContext, $this->isOptional);
+ }
+}
diff --git a/vendor/scssphp/scssphp/src/Extend/ExtensionStore.php b/vendor/scssphp/scssphp/src/Extend/ExtensionStore.php
new file mode 100644
index 000000000..849fbeb0f
--- /dev/null
+++ b/vendor/scssphp/scssphp/src/Extend/ExtensionStore.php
@@ -0,0 +1,63 @@
+<?php
+
+/**
+ * SCSSPHP
+ *
+ * @copyright 2012-2020 Leaf Corcoran
+ *
+ * @license http://opensource.org/licenses/MIT MIT
+ *
+ * @link http://scssphp.github.io/scssphp
+ */
+
+namespace ScssPhp\ScssPhp\Extend;
+
+use ScssPhp\ScssPhp\Ast\Css\CssMediaQuery;
+use ScssPhp\ScssPhp\Ast\Sass\Statement\ExtendRule;
+use ScssPhp\ScssPhp\Ast\Selector\SelectorList;
+use ScssPhp\ScssPhp\Ast\Selector\SimpleSelector;
+use ScssPhp\ScssPhp\Util\Box;
+
+/**
+ * Tracks selectors and extensions, and applies the latter to the former.
+ *
+ * @internal
+ */
+interface ExtensionStore
+{
+ public function isEmpty(): bool;
+
+ /**
+ * @return SimpleSelector[]
+ */
+ public function getSimpleSelectors(): array; // TODO check the right representation for this
+
+ /**
+ * @param callable(SimpleSelector): bool $callback
+ * @return iterable<Extension>
+ *
+ * @param-immediately-invoked-callable $callback
+ */
+ public function extensionsWhereTarget(callable $callback): iterable;
+
+ /**
+ * @param list<CssMediaQuery>|null $mediaContext
+ * @return Box<SelectorList>
+ */
+ public function addSelector(SelectorList $selector, ?array $mediaContext): Box;
+
+ /**
+ * @param list<CssMediaQuery>|null $mediaContext
+ */
+ public function addExtension(SelectorList $extender, SimpleSelector $target, ExtendRule $extend, ?array $mediaContext): void;
+
+ /**
+ * @param iterable<ExtensionStore> $extensionStores
+ */
+ public function addExtensions(iterable $extensionStores): void;
+
+ /**
+ * @return array{ExtensionStore, \SplObjectStorage<SelectorList, Box<SelectorList>>}
+ */
+ public function clone(): array;
+}
diff --git a/vendor/scssphp/scssphp/src/Extend/MergedExtension.php b/vendor/scssphp/scssphp/src/Extend/MergedExtension.php
new file mode 100644
index 000000000..6ae3e47d6
--- /dev/null
+++ b/vendor/scssphp/scssphp/src/Extend/MergedExtension.php
@@ -0,0 +1,83 @@
+<?php
+
+/**
+ * SCSSPHP
+ *
+ * @copyright 2012-2020 Leaf Corcoran
+ *
+ * @license http://opensource.org/licenses/MIT MIT
+ *
+ * @link http://scssphp.github.io/scssphp
+ */
+
+namespace ScssPhp\ScssPhp\Extend;
+
+use ScssPhp\ScssPhp\Exception\SimpleSassException;
+use ScssPhp\ScssPhp\Util\EquatableUtil;
+
+/**
+ * An {@see Extension} created by merging two {@see Extension}s with the same extender
+ * and target.
+ *
+ * This is used when multiple mandatory extensions exist to ensure that both of
+ * them are marked as resolved.
+ *
+ * @internal
+ */
+final class MergedExtension extends Extension
+{
+ public readonly Extension $left;
+ public readonly Extension $right;
+
+ private function __construct(Extension $left, Extension $right)
+ {
+ $this->left = $left;
+ $this->right = $right;
+
+ parent::__construct($left->extender->selector, $left->target, $left->span, $left->mediaContext ?? $right->mediaContext, true);
+ }
+
+ public static function merge(Extension $left, Extension $right): Extension
+ {
+ if (!EquatableUtil::equals($left->extender->selector, $right->extender->selector) || !EquatableUtil::equals($left->target, $right->target)) {
+ throw new \InvalidArgumentException('$left and $right aren\'t the same extension.');
+ }
+
+ if ($left->mediaContext !== null && $right->mediaContext !== null && !EquatableUtil::listEquals($left->mediaContext, $right->mediaContext)) {
+ $location = $left->span->message('');
+
+ throw new SimpleSassException("From $location\nYou may not @extend the same selector from within different media queries.", $right->span);
+ }
+
+ // If one extension is optional and doesn't add a special media context, it
+ // doesn't need to be merged.
+ if ($right->isOptional && $right->mediaContext === null) {
+ return $left;
+ }
+ if ($left->isOptional && $left->mediaContext === null) {
+ return $right;
+ }
+
+ return new MergedExtension($left, $right);
+ }
+
+ /**
+ * Returns all leaf-node [Extension]s in the tree of [MergedExtension]s.
+ *
+ * @return \Traversable<Extension>
+ */
+ public function unmerge(): \Traversable
+ {
+ if ($this->left instanceof MergedExtension) {
+ yield from $this->left->unmerge();
+ } else {
+ yield $this->left;
+ }
+
+ if ($this->right instanceof MergedExtension) {
+ yield from $this->right->unmerge();
+ } else {
+ yield $this->right;
+ }
+ }
+}
diff --git a/vendor/scssphp/scssphp/src/Extend/ObjectSet.php b/vendor/scssphp/scssphp/src/Extend/ObjectSet.php
new file mode 100644
index 000000000..2a122b607
--- /dev/null
+++ b/vendor/scssphp/scssphp/src/Extend/ObjectSet.php
@@ -0,0 +1,59 @@
+<?php
+
+/**
+ * SCSSPHP
+ *
+ * @copyright 2012-2020 Leaf Corcoran
+ *
+ * @license http://opensource.org/licenses/MIT MIT
+ *
+ * @link http://scssphp.github.io/scssphp
+ */
+
+namespace ScssPhp\ScssPhp\Extend;
+
+/**
+ * @template T of object
+ * @template-implements \IteratorAggregate<int, T>
+ */
+class ObjectSet implements \IteratorAggregate
+{
+ /**
+ * @var \SplObjectStorage<T, mixed>
+ */
+ private readonly \SplObjectStorage $storage;
+
+ public function __construct()
+ {
+ $this->storage = new \SplObjectStorage();
+ }
+
+ /**
+ * @param T $value
+ */
+ public function contains(object $value): bool
+ {
+ return $this->storage->contains($value);
+ }
+
+ /**
+ * @param T $value
+ */
+ public function add(object $value): void
+ {
+ $this->storage->attach($value);
+ }
+
+ /**
+ * @param ObjectSet<T> $set
+ */
+ public function addAll(self $set): void
+ {
+ $this->storage->addAll($set->storage);
+ }
+
+ public function getIterator(): \Traversable
+ {
+ return $this->storage;
+ }
+}
diff --git a/vendor/scssphp/scssphp/src/Extend/SimpleSelectorMap.php b/vendor/scssphp/scssphp/src/Extend/SimpleSelectorMap.php
new file mode 100644
index 000000000..abea6e51d
--- /dev/null
+++ b/vendor/scssphp/scssphp/src/Extend/SimpleSelectorMap.php
@@ -0,0 +1,31 @@
+<?php
+
+/**
+ * SCSSPHP
+ *
+ * @copyright 2012-2020 Leaf Corcoran
+ *
+ * @license http://opensource.org/licenses/MIT MIT
+ *
+ * @link http://scssphp.github.io/scssphp
+ */
+
+namespace ScssPhp\ScssPhp\Extend;
+
+use ScssPhp\ScssPhp\Ast\Selector\SimpleSelector;
+
+/**
+ * @template T
+ * @template-extends \SplObjectStorage<SimpleSelector, T>
+ *
+ * @internal
+ */
+final class SimpleSelectorMap extends \SplObjectStorage
+{
+ public function getHash(object $object): string
+ {
+ \assert($object instanceof SimpleSelector);
+ // For SimpleSelector, selectors that are equal by value semantic are exactly the ones that have the same string representation.
+ return (string) $object;
+ }
+}
diff --git a/vendor/scssphp/scssphp/src/Formatter.php b/vendor/scssphp/scssphp/src/Formatter.php
deleted file mode 100644
index 6137dc650..000000000
--- a/vendor/scssphp/scssphp/src/Formatter.php
+++ /dev/null
@@ -1,377 +0,0 @@
-<?php
-
-/**
- * SCSSPHP
- *
- * @copyright 2012-2020 Leaf Corcoran
- *
- * @license http://opensource.org/licenses/MIT MIT
- *
- * @link http://scssphp.github.io/scssphp
- */
-
-namespace ScssPhp\ScssPhp;
-
-use ScssPhp\ScssPhp\Formatter\OutputBlock;
-use ScssPhp\ScssPhp\SourceMap\SourceMapGenerator;
-
-/**
- * Base formatter
- *
- * @author Leaf Corcoran <leafot@gmail.com>
- *
- * @internal
- */
-abstract class Formatter
-{
- /**
- * @var int
- */
- public $indentLevel;
-
- /**
- * @var string
- */
- public $indentChar;
-
- /**
- * @var string
- */
- public $break;
-
- /**
- * @var string
- */
- public $open;
-
- /**
- * @var string
- */
- public $close;
-
- /**
- * @var string
- */
- public $tagSeparator;
-
- /**
- * @var string
- */
- public $assignSeparator;
-
- /**
- * @var bool
- */
- public $keepSemicolons;
-
- /**
- * @var \ScssPhp\ScssPhp\Formatter\OutputBlock
- */
- protected $currentBlock;
-
- /**
- * @var int
- */
- protected $currentLine;
-
- /**
- * @var int
- */
- protected $currentColumn;
-
- /**
- * @var \ScssPhp\ScssPhp\SourceMap\SourceMapGenerator|null
- */
- protected $sourceMapGenerator;
-
- /**
- * @var string
- */
- protected $strippedSemicolon;
-
- /**
- * Initialize formatter
- *
- * @api
- */
- abstract public function __construct();
-
- /**
- * Return indentation (whitespace)
- *
- * @return string
- */
- protected function indentStr()
- {
- return '';
- }
-
- /**
- * Return property assignment
- *
- * @api
- *
- * @param string $name
- * @param mixed $value
- *
- * @return string
- */
- public function property($name, $value)
- {
- return rtrim($name) . $this->assignSeparator . $value . ';';
- }
-
- /**
- * Return custom property assignment
- * differs in that you have to keep spaces in the value as is
- *
- * @api
- *
- * @param string $name
- * @param mixed $value
- *
- * @return string
- */
- public function customProperty($name, $value)
- {
- return rtrim($name) . trim($this->assignSeparator) . $value . ';';
- }
-
- /**
- * Output lines inside a block
- *
- * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $block
- *
- * @return void
- */
- protected function blockLines(OutputBlock $block)
- {
- $inner = $this->indentStr();
- $glue = $this->break . $inner;
-
- $this->write($inner . implode($glue, $block->lines));
-
- if (! empty($block->children)) {
- $this->write($this->break);
- }
- }
-
- /**
- * Output block selectors
- *
- * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $block
- *
- * @return void
- */
- protected function blockSelectors(OutputBlock $block)
- {
- assert(! empty($block->selectors));
-
- $inner = $this->indentStr();
-
- $this->write($inner
- . implode($this->tagSeparator, $block->selectors)
- . $this->open . $this->break);
- }
-
- /**
- * Output block children
- *
- * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $block
- *
- * @return void
- */
- protected function blockChildren(OutputBlock $block)
- {
- foreach ($block->children as $child) {
- $this->block($child);
- }
- }
-
- /**
- * Output non-empty block
- *
- * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $block
- *
- * @return void
- */
- protected function block(OutputBlock $block)
- {
- if (empty($block->lines) && empty($block->children)) {
- return;
- }
-
- $this->currentBlock = $block;
-
- $pre = $this->indentStr();
-
- if (! empty($block->selectors)) {
- $this->blockSelectors($block);
-
- $this->indentLevel++;
- }
-
- if (! empty($block->lines)) {
- $this->blockLines($block);
- }
-
- if (! empty($block->children)) {
- $this->blockChildren($block);
- }
-
- if (! empty($block->selectors)) {
- $this->indentLevel--;
-
- if (! $this->keepSemicolons) {
- $this->strippedSemicolon = '';
- }
-
- if (empty($block->children)) {
- $this->write($this->break);
- }
-
- $this->write($pre . $this->close . $this->break);
- }
- }
-
- /**
- * Test and clean safely empty children
- *
- * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $block
- *
- * @return bool
- */
- protected function testEmptyChildren($block)
- {
- $isEmpty = empty($block->lines);
-
- if ($block->children) {
- foreach ($block->children as $k => &$child) {
- if (! $this->testEmptyChildren($child)) {
- $isEmpty = false;
- continue;
- }
-
- if ($child->type === Type::T_MEDIA || $child->type === Type::T_DIRECTIVE) {
- $child->children = [];
- $child->selectors = null;
- }
- }
- }
-
- return $isEmpty;
- }
-
- /**
- * Entry point to formatting a block
- *
- * @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)
- {
- $this->sourceMapGenerator = null;
-
- if ($sourceMapGenerator) {
- $this->currentLine = 1;
- $this->currentColumn = 0;
- $this->sourceMapGenerator = $sourceMapGenerator;
- }
-
- $this->testEmptyChildren($block);
-
- ob_start();
-
- try {
- $this->block($block);
- } catch (\Exception $e) {
- ob_end_clean();
- throw $e;
- } catch (\Throwable $e) {
- ob_end_clean();
- throw $e;
- }
-
- $out = ob_get_clean();
- assert($out !== false);
-
- return $out;
- }
-
- /**
- * Output content
- *
- * @param string $str
- *
- * @return void
- */
- protected function write($str)
- {
- if (! empty($this->strippedSemicolon)) {
- echo $this->strippedSemicolon;
-
- $this->strippedSemicolon = '';
- }
-
- /*
- * Maybe Strip semi-colon appended by property(); it's a separator, not a terminator
- * will be striped for real before a closing, otherwise displayed unchanged starting the next write
- */
- if (
- ! $this->keepSemicolons &&
- $str &&
- (strpos($str, ';') !== false) &&
- (substr($str, -1) === ';')
- ) {
- $str = substr($str, 0, -1);
-
- $this->strippedSemicolon = ';';
- }
-
- if ($this->sourceMapGenerator) {
- $lines = explode("\n", $str);
- $lastLine = array_pop($lines);
-
- foreach ($lines as $line) {
- // If the written line starts is empty, adding a mapping would add it for
- // a non-existent column as we are at the end of the line
- if ($line !== '') {
- assert($this->currentBlock->sourceLine !== null);
- assert($this->currentBlock->sourceName !== null);
- $this->sourceMapGenerator->addMapping(
- $this->currentLine,
- $this->currentColumn,
- $this->currentBlock->sourceLine,
- //columns from parser are off by one
- $this->currentBlock->sourceColumn > 0 ? $this->currentBlock->sourceColumn - 1 : 0,
- $this->currentBlock->sourceName
- );
- }
-
- $this->currentLine++;
- $this->currentColumn = 0;
- }
-
- if ($lastLine !== '') {
- assert($this->currentBlock->sourceLine !== null);
- assert($this->currentBlock->sourceName !== null);
- $this->sourceMapGenerator->addMapping(
- $this->currentLine,
- $this->currentColumn,
- $this->currentBlock->sourceLine,
- //columns from parser are off by one
- $this->currentBlock->sourceColumn > 0 ? $this->currentBlock->sourceColumn - 1 : 0,
- $this->currentBlock->sourceName
- );
- }
-
- $this->currentColumn += \strlen($lastLine);
- }
-
- echo $str;
- }
-}
diff --git a/vendor/scssphp/scssphp/src/Formatter/Compact.php b/vendor/scssphp/scssphp/src/Formatter/Compact.php
deleted file mode 100644
index 22f226889..000000000
--- a/vendor/scssphp/scssphp/src/Formatter/Compact.php
+++ /dev/null
@@ -1,52 +0,0 @@
-<?php
-
-/**
- * SCSSPHP
- *
- * @copyright 2012-2020 Leaf Corcoran
- *
- * @license http://opensource.org/licenses/MIT MIT
- *
- * @link http://scssphp.github.io/scssphp
- */
-
-namespace ScssPhp\ScssPhp\Formatter;
-
-use ScssPhp\ScssPhp\Formatter;
-
-/**
- * Compact formatter
- *
- * @author Leaf Corcoran <leafot@gmail.com>
- *
- * @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/vendor/scssphp/scssphp/src/Formatter/Compressed.php b/vendor/scssphp/scssphp/src/Formatter/Compressed.php
deleted file mode 100644
index 58ebe3f11..000000000
--- a/vendor/scssphp/scssphp/src/Formatter/Compressed.php
+++ /dev/null
@@ -1,83 +0,0 @@
-<?php
-
-/**
- * SCSSPHP
- *
- * @copyright 2012-2020 Leaf Corcoran
- *
- * @license http://opensource.org/licenses/MIT MIT
- *
- * @link http://scssphp.github.io/scssphp
- */
-
-namespace ScssPhp\ScssPhp\Formatter;
-
-use ScssPhp\ScssPhp\Formatter;
-
-/**
- * Compressed formatter
- *
- * @author Leaf Corcoran <leafot@gmail.com>
- *
- * @internal
- */
-class Compressed extends Formatter
-{
- /**
- * {@inheritdoc}
- */
- public function __construct()
- {
- $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) === '/*' && substr($line, 2, 1) !== '!') {
- 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/vendor/scssphp/scssphp/src/Formatter/Crunched.php b/vendor/scssphp/scssphp/src/Formatter/Crunched.php
deleted file mode 100644
index 2bc1e9299..000000000
--- a/vendor/scssphp/scssphp/src/Formatter/Crunched.php
+++ /dev/null
@@ -1,87 +0,0 @@
-<?php
-
-/**
- * SCSSPHP
- *
- * @copyright 2012-2020 Leaf Corcoran
- *
- * @license http://opensource.org/licenses/MIT MIT
- *
- * @link http://scssphp.github.io/scssphp
- */
-
-namespace ScssPhp\ScssPhp\Formatter;
-
-use ScssPhp\ScssPhp\Formatter;
-
-/**
- * Crunched formatter
- *
- * @author Anthon Pang <anthon.pang@gmail.com>
- *
- * @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/vendor/scssphp/scssphp/src/Formatter/Debug.php b/vendor/scssphp/scssphp/src/Formatter/Debug.php
deleted file mode 100644
index b3f442253..000000000
--- a/vendor/scssphp/scssphp/src/Formatter/Debug.php
+++ /dev/null
@@ -1,127 +0,0 @@
-<?php
-
-/**
- * SCSSPHP
- *
- * @copyright 2012-2020 Leaf Corcoran
- *
- * @license http://opensource.org/licenses/MIT MIT
- *
- * @link http://scssphp.github.io/scssphp
- */
-
-namespace ScssPhp\ScssPhp\Formatter;
-
-use ScssPhp\ScssPhp\Formatter;
-
-/**
- * Debug formatter
- *
- * @author Anthon Pang <anthon.pang@gmail.com>
- *
- * @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/vendor/scssphp/scssphp/src/Formatter/Expanded.php b/vendor/scssphp/scssphp/src/Formatter/Expanded.php
deleted file mode 100644
index 6eb4a0cb7..000000000
--- a/vendor/scssphp/scssphp/src/Formatter/Expanded.php
+++ /dev/null
@@ -1,72 +0,0 @@
-<?php
-
-/**
- * SCSSPHP
- *
- * @copyright 2012-2020 Leaf Corcoran
- *
- * @license http://opensource.org/licenses/MIT MIT
- *
- * @link http://scssphp.github.io/scssphp
- */
-
-namespace ScssPhp\ScssPhp\Formatter;
-
-use ScssPhp\ScssPhp\Formatter;
-
-/**
- * Expanded formatter
- *
- * @author Leaf Corcoran <leafot@gmail.com>
- *
- * @internal
- */
-class Expanded extends Formatter
-{
- /**
- * {@inheritdoc}
- */
- public function __construct()
- {
- $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->indentChar, $this->indentLevel);
- }
-
- /**
- * {@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));
-
- if (empty($block->selectors) || ! empty($block->children)) {
- $this->write($this->break);
- }
- }
-}
diff --git a/vendor/scssphp/scssphp/src/Formatter/Nested.php b/vendor/scssphp/scssphp/src/Formatter/Nested.php
deleted file mode 100644
index d5ed85cc2..000000000
--- a/vendor/scssphp/scssphp/src/Formatter/Nested.php
+++ /dev/null
@@ -1,238 +0,0 @@
-<?php
-
-/**
- * SCSSPHP
- *
- * @copyright 2012-2020 Leaf Corcoran
- *
- * @license http://opensource.org/licenses/MIT MIT
- *
- * @link http://scssphp.github.io/scssphp
- */
-
-namespace ScssPhp\ScssPhp\Formatter;
-
-use ScssPhp\ScssPhp\Formatter;
-use ScssPhp\ScssPhp\Type;
-
-/**
- * Nested formatter
- *
- * @author Leaf Corcoran <leafot@gmail.com>
- *
- * @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/vendor/scssphp/scssphp/src/Formatter/OutputBlock.php b/vendor/scssphp/scssphp/src/Formatter/OutputBlock.php
deleted file mode 100644
index 2799656a4..000000000
--- a/vendor/scssphp/scssphp/src/Formatter/OutputBlock.php
+++ /dev/null
@@ -1,68 +0,0 @@
-<?php
-
-/**
- * SCSSPHP
- *
- * @copyright 2012-2020 Leaf Corcoran
- *
- * @license http://opensource.org/licenses/MIT MIT
- *
- * @link http://scssphp.github.io/scssphp
- */
-
-namespace ScssPhp\ScssPhp\Formatter;
-
-/**
- * Output block
- *
- * @author Anthon Pang <anthon.pang@gmail.com>
- *
- * @internal
- */
-class OutputBlock
-{
- /**
- * @var string|null
- */
- public $type;
-
- /**
- * @var int
- */
- public $depth;
-
- /**
- * @var array|null
- */
- public $selectors;
-
- /**
- * @var string[]
- */
- public $lines;
-
- /**
- * @var OutputBlock[]
- */
- public $children;
-
- /**
- * @var OutputBlock|null
- */
- public $parent;
-
- /**
- * @var string|null
- */
- public $sourceName;
-
- /**
- * @var int|null
- */
- public $sourceLine;
-
- /**
- * @var int|null
- */
- public $sourceColumn;
-}
diff --git a/vendor/scssphp/scssphp/src/Function/ColorFunctions.php b/vendor/scssphp/scssphp/src/Function/ColorFunctions.php
new file mode 100644
index 000000000..d879360e5
--- /dev/null
+++ b/vendor/scssphp/scssphp/src/Function/ColorFunctions.php
@@ -0,0 +1,886 @@
+<?php
+
+/**
+ * SCSSPHP
+ *
+ * @copyright 2012-2020 Leaf Corcoran
+ *
+ * @license http://opensource.org/licenses/MIT MIT
+ *
+ * @link http://scssphp.github.io/scssphp
+ */
+
+namespace ScssPhp\ScssPhp\Function;
+
+use ScssPhp\ScssPhp\Deprecation;
+use ScssPhp\ScssPhp\Exception\SassScriptException;
+use ScssPhp\ScssPhp\Util\IterableUtil;
+use ScssPhp\ScssPhp\Util\NumberUtil;
+use ScssPhp\ScssPhp\Util\StringUtil;
+use ScssPhp\ScssPhp\Value\ColorFormatEnum;
+use ScssPhp\ScssPhp\Value\ListSeparator;
+use ScssPhp\ScssPhp\Value\SassArgumentList;
+use ScssPhp\ScssPhp\Value\SassColor;
+use ScssPhp\ScssPhp\Value\SassNumber;
+use ScssPhp\ScssPhp\Value\SassString;
+use ScssPhp\ScssPhp\Value\Value;
+use ScssPhp\ScssPhp\Warn;
+
+/**
+ * @internal
+ */
+class ColorFunctions
+{
+ /**
+ * @param list<Value> $arguments
+ */
+ public static function rgb(array $arguments): Value
+ {
+ return self::rgbImpl('rgb', $arguments);
+ }
+
+ /**
+ * @param list<Value> $arguments
+ */
+ public static function rgbTwoArgs(array $arguments): Value
+ {
+ return self::rgbTwoArgsImpl('rgb', $arguments);
+ }
+
+ /**
+ * @param list<Value> $arguments
+ */
+ public static function rgbOneArgs(array $arguments): Value
+ {
+ $parsed = self::parseChannels('rgb', ['$red', '$green', '$blue'], $arguments[0]);
+
+ return $parsed instanceof SassString ? $parsed : self::rgbImpl('rgb', $parsed);
+ }
+
+ /**
+ * @param list<Value> $arguments
+ */
+ public static function rgba(array $arguments): Value
+ {
+ return self::rgbImpl('rgba', $arguments);
+ }
+
+ /**
+ * @param list<Value> $arguments
+ */
+ public static function rgbaTwoArgs(array $arguments): Value
+ {
+ return self::rgbTwoArgsImpl('rgba', $arguments);
+ }
+
+ /**
+ * @param list<Value> $arguments
+ */
+ public static function rgbaOneArgs(array $arguments): Value
+ {
+ $parsed = self::parseChannels('rgba', ['$red', '$green', '$blue'], $arguments[0]);
+
+ return $parsed instanceof SassString ? $parsed : self::rgbImpl('rgba', $parsed);
+ }
+
+ /**
+ * @param list<Value> $arguments
+ */
+ public static function invert(array $arguments): Value
+ {
+ $weight = $arguments[1]->assertNumber('weight');
+ if ($arguments[0] instanceof SassNumber || $arguments[0]->isSpecialNumber()) {
+ if ($weight->getValue() !== 100.0 || !$weight->hasUnit('%')) {
+ throw new SassScriptException('Only one argument may be passed to the plain-CSS invert() function.');
+ }
+
+ // Use the native CSS `invert` filter function.
+ return self::functionString('invert', [$arguments[0]]);
+ }
+
+ $color = $arguments[0]->assertColor('color');
+ $inverse = $color->changeRgb(255 - $color->getRed(), 255 - $color->getGreen(), 255 - $color->getBlue());
+
+ return self::mixColors($inverse, $color, $weight);
+ }
+
+ /**
+ * @param list<Value> $arguments
+ */
+ public static function hsl(array $arguments): Value
+ {
+ return self::hslImpl('hsl', $arguments);
+ }
+
+ /**
+ * @param list<Value> $arguments
+ */
+ public static function hslTwoArgs(array $arguments): Value
+ {
+ // hsl(123, var(--foo)) is valid CSS because --foo might be `10%, 20%` and
+ // functions are parsed after variable substitution.
+ if ($arguments[0]->isVar() || $arguments[1]->isVar()) {
+ return self::functionString('hsl', $arguments);
+ }
+
+ throw new SassScriptException('Missing argument $lightness.');
+ }
+
+ /**
+ * @param list<Value> $arguments
+ */
+ public static function hslOneArgs(array $arguments): Value
+ {
+ $parsed = self::parseChannels('hsl', ['$hue', '$saturation', '$lightness'], $arguments[0]);
+
+ return $parsed instanceof SassString ? $parsed : self::hslImpl('hsl', $parsed);
+ }
+
+ /**
+ * @param list<Value> $arguments
+ */
+ public static function hsla(array $arguments): Value
+ {
+ return self::hslImpl('hsla', $arguments);
+ }
+
+ /**
+ * @param list<Value> $arguments
+ */
+ public static function hslaTwoArgs(array $arguments): Value
+ {
+ // hsl(123, var(--foo)) is valid CSS because --foo might be `10%, 20%` and
+ // functions are parsed after variable substitution.
+ if ($arguments[0]->isVar() || $arguments[1]->isVar()) {
+ return self::functionString('hsla', $arguments);
+ }
+
+ throw new SassScriptException('Missing argument $lightness.');
+ }
+
+ /**
+ * @param list<Value> $arguments
+ */
+ public static function hslaOneArgs(array $arguments): Value
+ {
+ $parsed = self::parseChannels('hsla', ['$hue', '$saturation', '$lightness'], $arguments[0]);
+
+ return $parsed instanceof SassString ? $parsed : self::hslImpl('hsla', $parsed);
+ }
+
+ /**
+ * @param list<Value> $arguments
+ */
+ public static function grayscale(array $arguments): Value
+ {
+ if ($arguments[0] instanceof SassNumber || $arguments[0]->isSpecialNumber()) {
+ // Use the native CSS `grayscale` filter function.
+ return self::functionString('grayscale', $arguments);
+ }
+
+ $color = $arguments[0]->assertColor('color');
+
+ return $color->changeHsl(saturation: 0);
+ }
+
+ /**
+ * @param list<Value> $arguments
+ */
+ public static function adjustHue(array $arguments): Value
+ {
+ $color = $arguments[0]->assertColor('color');
+ $degrees = self::angleValue($arguments[1], 'degrees');
+
+ return $color->changeHsl(hue: $color->getHue() + $degrees);
+ }
+
+ /**
+ * @param list<Value> $arguments
+ */
+ public static function lighten(array $arguments): Value
+ {
+ $color = $arguments[0]->assertColor('color');
+ $amount = $arguments[1]->assertNumber('amount');
+
+ return $color->changeHsl(lightness: NumberUtil::clamp($color->getLightness() + $amount->valueInRange(0, 100, 'amount'), 0, 100));
+ }
+
+ /**
+ * @param list<Value> $arguments
+ */
+ public static function darken(array $arguments): Value
+ {
+ $color = $arguments[0]->assertColor('color');
+ $amount = $arguments[1]->assertNumber('amount');
+
+ return $color->changeHsl(lightness: NumberUtil::clamp($color->getLightness() - $amount->valueInRange(0, 100, 'amount'), 0, 100));
+ }
+
+ /**
+ * @param list<Value> $arguments
+ */
+ public static function saturateCss(array $arguments): Value
+ {
+ if ($arguments[0] instanceof SassNumber || $arguments[0]->isSpecialNumber()) {
+ // Use the native CSS `saturate` filter function.
+ return self::functionString('saturate', $arguments);
+ }
+
+ $number = $arguments[0]->assertNumber('amount');
+
+ return new SassString('saturate(' . $number->toCssString() . ')', false);
+ }
+
+ /**
+ * @param list<Value> $arguments
+ */
+ public static function saturate(array $arguments): Value
+ {
+ $color = $arguments[0]->assertColor('color');
+ $amount = $arguments[1]->assertNumber('amount');
+
+ return $color->changeHsl(saturation: NumberUtil::clamp($color->getSaturation() + $amount->valueInRange(0, 100, 'amount'), 0, 100));
+ }
+
+ /**
+ * @param list<Value> $arguments
+ */
+ public static function desaturate(array $arguments): Value
+ {
+ $color = $arguments[0]->assertColor('color');
+ $amount = $arguments[1]->assertNumber('amount');
+
+ return $color->changeHsl(saturation: NumberUtil::clamp($color->getSaturation() - $amount->valueInRange(0, 100, 'amount'), 0, 100));
+ }
+
+ /**
+ * @param list<Value> $arguments
+ */
+ public static function alpha(array $arguments): Value
+ {
+ $argument = $arguments[0];
+ if ($argument instanceof SassString && !$argument->hasQuotes() && preg_match('/^[a-zA-Z]+\s*=/', $argument->getText())) {
+ // Support the proprietary Microsoft alpha() function.
+ return self::functionString('alpha', $arguments);
+ }
+
+ $color = $arguments[0]->assertColor('color');
+
+ return SassNumber::create($color->getAlpha());
+ }
+
+ /**
+ * @param list<Value> $arguments
+ */
+ public static function alphaMicrosoft(array $arguments): Value
+ {
+ $argList = $arguments[0]->asList();
+ $argumentCount = \count($argList);
+
+ if ($argumentCount > 0 && IterableUtil::every($argList, fn($argument) => $argument instanceof SassString && !$argument->hasQuotes() && preg_match('/^[a-zA-Z]+\s*=/', $argument->getText()))) {
+ // Support the proprietary Microsoft alpha() function.
+ return self::functionString('alpha', $arguments);
+ }
+
+ \assert($argumentCount !== 1);
+
+ if ($argumentCount === 0) {
+ throw new SassScriptException('Missing argument $color.');
+ }
+
+ throw new SassScriptException("Only 1 argument allowed, but $argumentCount were passed.");
+ }
+
+ /**
+ * @param list<Value> $arguments
+ */
+ public static function opacity(array $arguments): Value
+ {
+ if ($arguments[0] instanceof SassNumber || $arguments[0]->isSpecialNumber()) {
+ // Use the native CSS `opacity` filter function.
+ return self::functionString('opacity', $arguments);
+ }
+
+ $color = $arguments[0]->assertColor('color');
+
+ return SassNumber::create($color->getAlpha());
+ }
+
+ /**
+ * @param list<Value> $arguments
+ */
+ public static function red(array $arguments): Value
+ {
+ return SassNumber::create($arguments[0]->assertColor('color')->getRed());
+ }
+
+ /**
+ * @param list<Value> $arguments
+ */
+ public static function green(array $arguments): Value
+ {
+ return SassNumber::create($arguments[0]->assertColor('color')->getGreen());
+ }
+
+ /**
+ * @param list<Value> $arguments
+ */
+ public static function blue(array $arguments): Value
+ {
+ return SassNumber::create($arguments[0]->assertColor('color')->getBlue());
+ }
+
+ /**
+ * @param list<Value> $arguments
+ */
+ public static function mix(array $arguments): Value
+ {
+ $color1 = $arguments[0]->assertColor('color1');
+ $color2 = $arguments[1]->assertColor('color2');
+ $weight = $arguments[2]->assertNumber('weight');
+
+ return self::mixColors($color1, $color2, $weight);
+ }
+
+ /**
+ * @param list<Value> $arguments
+ */
+ public static function hue(array $arguments): Value
+ {
+ return SassNumber::create($arguments[0]->assertColor('color')->getHue(), 'deg');
+ }
+
+ /**
+ * @param list<Value> $arguments
+ */
+ public static function saturation(array $arguments): Value
+ {
+ return SassNumber::create($arguments[0]->assertColor('color')->getSaturation(), '%');
+ }
+
+ /**
+ * @param list<Value> $arguments
+ */
+ public static function lightness(array $arguments): Value
+ {
+ return SassNumber::create($arguments[0]->assertColor('color')->getLightness(), '%');
+ }
+
+ /**
+ * @param list<Value> $arguments
+ */
+ public static function complement(array $arguments): Value
+ {
+ $color = $arguments[0]->assertColor('color');
+
+ return $color->changeHsl(hue: $color->getHue() + 180);
+ }
+
+ /**
+ * @param list<Value> $arguments
+ */
+ public static function adjust(array $arguments): Value
+ {
+ return self::updateComponents($arguments, adjust: true);
+ }
+
+ /**
+ * @param list<Value> $arguments
+ */
+ public static function scale(array $arguments): Value
+ {
+ return self::updateComponents($arguments, scale: true);
+ }
+
+ /**
+ * @param list<Value> $arguments
+ */
+ public static function change(array $arguments): Value
+ {
+ return self::updateComponents($arguments, change: true);
+ }
+
+ /**
+ * @param list<Value> $arguments
+ */
+ public static function ieHexStr(array $arguments): Value
+ {
+ $color = $arguments[0]->assertColor('color');
+ return new SassString('#' . self::hexString(NumberUtil::fuzzyRound($color->getAlpha() * 255)) . self::hexString($color->getRed()) . self::hexString($color->getGreen()) . self::hexString($color->getBlue()), false);
+ }
+
+ private static function hexString(int $component): string
+ {
+ return strtoupper(str_pad(dechex($component), 2, '0', STR_PAD_LEFT));
+ }
+
+ /**
+ * @param list<Value> $arguments
+ */
+ private static function updateComponents(array $arguments, bool $change = false, bool $adjust = false, bool $scale = false): SassColor
+ {
+ \assert(\count(array_filter([$change, $adjust, $scale])) === 1);
+
+ $color = $arguments[0]->assertColor('color');
+ $argumentList = $arguments[1];
+ \assert($argumentList instanceof SassArgumentList);
+
+ if (\count($argumentList->asList()) > 0) {
+ throw new SassScriptException('Only one positional argument is allowed. All other arguments must be passed by name.');
+ }
+
+ $keywords = $argumentList->getKeywords();
+
+ $getParam = function (string $name, float $max, bool $checkPercent = false, bool $assertPercent = false, bool $checkUnitless = false) use (&$keywords, $change, $scale): ?float {
+ $number = ($keywords[$name] ?? null)?->assertNumber($name);
+ unset($keywords[$name]);
+
+ if ($number === null) {
+ return null;
+ }
+
+ if (!$scale && $checkUnitless) {
+ if ($number->hasUnits()) {
+ Warn::forDeprecation(
+ <<<TXT
+\$$name: Passing a number with unit {$number->getUnitString()} is deprecated.
+
+To preserve current behavior: {$number->unitSuggestion($name)}
+
+More info: https://sass-lang.com/d/function-units
+TXT,
+ Deprecation::functionUnits
+ );
+ }
+ }
+ if (!$scale && $checkPercent) {
+ self::checkPercent($number, $name);
+ }
+ if ($scale || $assertPercent) {
+ $number->assertUnit('%', $name);
+ }
+ if ($scale) {
+ $max = 100;
+ }
+
+ return $scale || $assertPercent
+ ? $number->valueInRange($change ? 0 : -$max, $max, $name)
+ : $number->valueInRangeWithUnit($change ? 0 : -$max, $max, $name, $checkPercent ? '%' : '');
+ };
+
+ $alpha = $getParam('alpha', 1, checkUnitless: true);
+ $red = $getParam('red', 255);
+ $green = $getParam('green', 255);
+ $blue = $getParam('blue', 255);
+
+ if ($scale) {
+ $hue = null;
+ } else {
+ $hueValue = $keywords['hue'] ?? null;
+ unset($keywords['hue']);
+ $hue = $hueValue === null ? null : self::angleValue($hueValue, 'hue');
+ }
+
+ $saturation = $getParam('saturation', 100, checkPercent: true);
+ $lightness = $getParam('lightness', 100, checkPercent: true);
+ $whiteness = $getParam('whiteness', 100, assertPercent: true);
+ $blackness = $getParam('blackness', 100, assertPercent: true);
+
+ if (\count($keywords) > 0) {
+ throw new SassScriptException(sprintf(
+ 'No %s named %s.',
+ StringUtil::pluralize('argument', \count($keywords)),
+ StringUtil::toSentence(array_map(fn($name) => "\$$name", array_keys($keywords)), 'or')
+ ));
+ }
+
+ $hasRgb = $red !== null || $green !== null || $blue !== null;
+ $hasSL = $saturation !== null || $lightness !== null;
+ $hasWB = $whiteness !== null || $blackness !== null;
+
+ if ($hasRgb && ($hasSL || $hasWB || $hue !== null)) {
+ $format = $hasWB ? 'HWB' : 'HSL';
+ throw new SassScriptException("RGB parameters may not be passed along with $format parameters.");
+ }
+
+ if ($hasSL && $hasWB) {
+ throw new SassScriptException('HSL parameters may not be passed along with HWB parameters.');
+ }
+
+ $updateValue = function (float $current, ?float $param, float $max) use ($change, $adjust): float {
+ if ($param === null) {
+ return $current;
+ }
+
+ if ($change) {
+ return $param;
+ }
+
+ if ($adjust) {
+ return NumberUtil::clamp($current + $param, 0, $max);
+ }
+
+ return $current + ($param > 0 ? $max - $current : $current) * $param / 100;
+ };
+
+ $updateRgb = function (int $current, ?float $param) use ($updateValue): int {
+ return NumberUtil::fuzzyRound($updateValue($current, $param, 255));
+ };
+
+ if ($hasRgb) {
+ return $color->changeRgb(
+ $updateRgb($color->getRed(), $red),
+ $updateRgb($color->getGreen(), $green),
+ $updateRgb($color->getBlue(), $blue),
+ $updateValue($color->getAlpha(), $alpha, 1)
+ );
+ }
+
+ if ($hasWB) {
+ return $color->changeHwb(
+ $change ? $hue : $color->getHue() + ($hue ?? 0),
+ $updateValue($color->getWhiteness(), $whiteness, 100),
+ $updateValue($color->getBlackness(), $blackness, 100),
+ $updateValue($color->getAlpha(), $alpha, 1)
+ );
+ }
+
+ if ($hue !== null || $hasSL) {
+ return $color->changeHsl(
+ $change ? $hue : $color->getHue() + ($hue ?? 0),
+ $updateValue($color->getSaturation(), $saturation, 100),
+ $updateValue($color->getLightness(), $lightness, 100),
+ $updateValue($color->getAlpha(), $alpha, 1)
+ );
+ }
+
+ if ($alpha !== null) {
+ return $color->changeAlpha($updateValue($color->getAlpha(), $alpha, 1));
+ }
+
+ return $color;
+ }
+
+ /**
+ * Returns a string representation of $name called with $arguments, as though
+ * it were a plain CSS function.
+ *
+ * @param Value[] $arguments
+ */
+ private static function functionString(string $name, array $arguments): SassString
+ {
+ return new SassString($name . '(' . implode(', ', array_map(fn(Value $argument) => $argument->toCssString(), $arguments)) . ')', false);
+ }
+
+ /**
+ * @param list<Value> $arguments
+ */
+ private static function rgbImpl(string $name, array $arguments): Value
+ {
+ $alpha = $arguments[3] ?? null;
+
+ if ($arguments[0]->isSpecialNumber() || $arguments[1]->isSpecialNumber() || $arguments[2]->isSpecialNumber() || ($alpha?->isSpecialNumber() ?? false)) {
+ return self::functionString($name, $arguments);
+ }
+
+ $red = $arguments[0]->assertNumber('red');
+ $green = $arguments[1]->assertNumber('green');
+ $blue = $arguments[2]->assertNumber('blue');
+
+ return SassColor::rgbInternal(
+ NumberUtil::fuzzyRound(self::percentageOrUnitless($red, 255, 'red')),
+ NumberUtil::fuzzyRound(self::percentageOrUnitless($green, 255, 'green')),
+ NumberUtil::fuzzyRound(self::percentageOrUnitless($blue, 255, 'blue')),
+ $alpha !== null ? self::percentageOrUnitless($alpha->assertNumber('alpha'), 1, 'alpha') : 1,
+ ColorFormatEnum::rgbFunction
+ );
+ }
+
+ /**
+ * @param list<Value> $arguments
+ */
+ private static function rgbTwoArgsImpl(string $name, array $arguments): Value
+ {
+ // rgba(var(--foo), 0.5) is valid CSS because --foo might be `123, 456, 789`
+ // and functions are parsed after variable substitution.
+ if ($arguments[0]->isVar() || (!$arguments[0] instanceof SassColor && $arguments[1]->isVar())) {
+ return self::functionString($name, $arguments);
+ }
+
+ if ($arguments[1]->isSpecialNumber()) {
+ $color = $arguments[0]->assertColor('color');
+
+ return new SassString("$name({$color->getRed()}, {$color->getGreen()}, {$color->getBlue()}, {$arguments[1]->toCssString()})", false);
+ }
+
+ $color = $arguments[0]->assertColor('color');
+ $alpha = $arguments[1]->assertNumber('alpha');
+
+ return $color->changeAlpha(self::percentageOrUnitless($alpha, 1, 'alpha'));
+ }
+
+ /**
+ * @param list<Value> $arguments
+ */
+ private static function hslImpl(string $name, array $arguments): Value
+ {
+ $alpha = $arguments[3] ?? null;
+
+ if ($arguments[0]->isSpecialNumber() || $arguments[1]->isSpecialNumber() || $arguments[2]->isSpecialNumber() || ($alpha?->isSpecialNumber() ?? false)) {
+ return self::functionString($name, $arguments);
+ }
+
+ $hue = self::angleValue($arguments[0], 'hue');
+ $saturation = $arguments[1]->assertNumber('saturation');
+ $lightness = $arguments[2]->assertNumber('lightness');
+
+ self::checkPercent($saturation, 'saturation');
+ self::checkPercent($lightness, 'lightness');
+
+ return SassColor::hslInternal(
+ $hue,
+ NumberUtil::clamp($saturation->getValue(), 0, 100),
+ NumberUtil::clamp($lightness->getValue(), 0, 100),
+ $alpha !== null ? self::percentageOrUnitless($alpha->assertNumber('alpha'), 1, 'alpha') : 1,
+ ColorFormatEnum::hslFunction
+ );
+ }
+
+ /**
+ * Asserts that $angle is a number and returns its value in degrees.
+ *
+ * Prints a deprecation warning if $angle has a non-angle unit.
+ */
+ private static function angleValue(Value $angleValue, string $name): float
+ {
+ $angle = $angleValue->assertNumber($name);
+
+ if ($angle->compatibleWithUnit('deg')) {
+ return $angle->coerceValueToUnit('deg');
+ }
+
+ Warn::forDeprecation(
+ <<<TXT
+\$$name: Passing a unit other than deg ($angle) is deprecated.
+
+To preserve current behavior: {$angle->unitSuggestion($name)}
+
+See https://sass-lang.com/d/function-units
+TXT,
+ Deprecation::functionUnits
+ );
+
+ return $angle->getValue();
+ }
+
+ private static function checkPercent(SassNumber $number, string $name): void
+ {
+ if ($number->hasUnit('%')) {
+ return;
+ }
+
+ Warn::forDeprecation(
+ <<<TXT
+\$$name: Passing a number without unit % ($number) is deprecated.
+
+To preserve current behavior: {$number->unitSuggestion($name, '%')}
+
+More info: https://sass-lang.com/d/function-units
+TXT,
+ Deprecation::functionUnits
+ );
+ }
+
+ /**
+ * @param list<string> $argumentNames
+ *
+ * @return SassString|list<Value>
+ */
+ private static function parseChannels(string $name, array $argumentNames, Value $channels): SassString|array
+ {
+ if ($channels->isVar()) {
+ return self::functionString($name, [$channels]);
+ }
+
+ $originalChannels = $channels;
+ $alphaFromSlashList = null;
+
+ if ($channels->getSeparator() === ListSeparator::SLASH) {
+ $list = $channels->asList();
+
+ if (\count($list) !== 2) {
+ throw new SassScriptException(sprintf(
+ 'Only 2 slash-separated elements allowed, but %s %s passed.',
+ \count($list),
+ StringUtil::pluralize('was', \count($list), 'were')
+ ));
+ }
+
+ $channels = $list[0];
+ $alphaFromSlashList = $list[1];
+
+ if (!$alphaFromSlashList->isSpecialNumber()) {
+ $alphaFromSlashList->assertNumber('alpha');
+ }
+
+ if ($list[0]->isVar()) {
+ return self::functionString($name, [$originalChannels]);
+ }
+ }
+
+ $isCommaSeparated = $channels->getSeparator() === ListSeparator::COMMA;
+ $isBracketed = $channels->hasBrackets();
+
+ if ($isCommaSeparated || $isBracketed) {
+ $buffer = '$channels must be';
+ if ($isBracketed) {
+ $buffer .= ' an unbracketed';
+ }
+ if ($isCommaSeparated) {
+ $buffer .= $isBracketed ? ',' : ' a';
+ $buffer .= ' space-separated';
+ }
+
+ $buffer .= ' list.';
+
+ throw new SassScriptException($buffer);
+ }
+
+ $list = $channels->asList();
+
+ if (\count($list) >= 2 && $list[0] instanceof SassString && !$list[0]->hasQuotes() && StringUtil::equalsIgnoreCase($list[0]->getText(), 'from')) {
+ return self::functionString($name, [$originalChannels]);
+ }
+
+ if (\count($list) > 3) {
+ throw new SassScriptException(sprintf(
+ 'Only 3 elements allowed, but %s were passed',
+ \count($list)
+ ));
+ }
+
+ if (\count($list) < 3) {
+ if (IterableUtil::any($list, fn (Value $value) => $value->isVar()) || (\count($list) > 0 && self::isVarSlash($list[0]))) {
+ return self::functionString($name, [$originalChannels]);
+ }
+
+ $argument = $argumentNames[\count($list)];
+
+ throw new SassScriptException("Missing element $argument.");
+ }
+
+ if ($alphaFromSlashList !== null) {
+ return [...$list, $alphaFromSlashList];
+ }
+
+ if ($list[2] instanceof SassNumber && $list[2]->getAsSlash() !== null) {
+ [$channel3, $alpha] = $list[2]->getAsSlash();
+
+ return [$list[0], $list[1], $channel3, $alpha];
+ }
+
+ if ($list[2] instanceof SassString && !$list[2]->hasQuotes() && str_contains($list[2]->getText(), '/')) {
+ return self::functionString($name, [$channels]);
+ }
+
+ return $list;
+ }
+
+ /**
+ * Returns whether $value is an unquoted string that start with `var(` and
+ * contains `/`.
+ */
+ private static function isVarSlash(Value $value): bool
+ {
+ return $value instanceof SassString && $value->hasQuotes() && StringUtil::startsWithIgnoreCase($value->getText(), 'var(') && str_contains($value->getText(), '/');
+ }
+
+ /**
+ * Asserts that $number is a percentage or has no units, and normalizes the
+ * value.
+ *
+ * If $number has no units, its value is clamped to be greater than `0` or
+ * less than $max and returned. If $number is a percentage, it's scaled to be
+ * within `0` and $max. Otherwise, this throws a {@see SassScriptException}.
+ *
+ * $name is used to identify the argument in the error message.
+ */
+ private static function percentageOrUnitless(SassNumber $number, float $max, string $name): float
+ {
+ if (!$number->hasUnits()) {
+ $value = $number->getValue();
+ } elseif ($number->hasUnit('%')) {
+ $value = $max * $number->getValue() / 100;
+ } else {
+ throw new SassScriptException("\$$name: Expected $number to have unit \"%\" or no units.");
+ }
+
+ return NumberUtil::clamp($value, 0, $max);
+ }
+
+ private static function mixColors(SassColor $color1, SassColor $color2, SassNumber $weight): SassColor
+ {
+ self::checkPercent($weight, 'weight');
+
+ // This algorithm factors in both the user-provided weight (w) and the
+ // difference between the alpha values of the two colors (a) to decide how
+ // to perform the weighted average of the two RGB values.
+ //
+ // It works by first normalizing both parameters to be within [-1, 1], where
+ // 1 indicates "only use color1", -1 indicates "only use color2", and all
+ // values in between indicated a proportionately weighted average.
+ //
+ // Once we have the normalized variables w and a, we apply the formula
+ // (w + a)/(1 + w*a) to get the combined weight (in [-1, 1]) of color1. This
+ // formula has two especially nice properties:
+ //
+ // * When either w or a are -1 or 1, the combined weight is also that
+ // number (cases where w * a == -1 are undefined, and handled as a
+ // special case).
+ //
+ // * When a is 0, the combined weight is w, and vice versa.
+ //
+ // Finally, the weight of color1 is renormalized to be within [0, 1] and the
+ // weight of color2 is given by 1 minus the weight of color1.
+
+ $weightScale = $weight->valueInRange(0, 100, 'weight') / 100;
+ $normalizedWeight = $weightScale * 2 - 1;
+ $alphaDistance = $color1->getAlpha() - $color2->getAlpha();
+
+ $combinedWeight1 = $normalizedWeight * $alphaDistance == -1
+ ? $normalizedWeight
+ : ($normalizedWeight + $alphaDistance) / (1 + $normalizedWeight * $alphaDistance);
+
+ $weight1 = ($combinedWeight1 + 1) / 2;
+ $weight2 = 1 - $weight1;
+
+ return SassColor::rgb(
+ NumberUtil::fuzzyRound($color1->getRed() * $weight1 + $color2->getRed() * $weight2),
+ NumberUtil::fuzzyRound($color1->getGreen() * $weight1 + $color2->getGreen() * $weight2),
+ NumberUtil::fuzzyRound($color1->getBlue() * $weight1 + $color2->getBlue() * $weight2),
+ $color1->getAlpha() * $weightScale + $color2->getAlpha() * (1 - $weightScale)
+ );
+ }
+
+ /**
+ * @param list<Value> $arguments
+ */
+ public static function opacify(array $arguments): SassColor
+ {
+ $color = $arguments[0]->assertColor('color');
+ $amount = $arguments[1]->assertNumber('amount');
+
+ return $color->changeAlpha(NumberUtil::clamp($color->getAlpha() + $amount->valueInRangeWithUnit(0, 1, 'amount', ''), 0, 1));
+ }
+
+ /**
+ * @param list<Value> $arguments
+ */
+ public static function transparentize(array $arguments): SassColor
+ {
+ $color = $arguments[0]->assertColor('color');
+ $amount = $arguments[1]->assertNumber('amount');
+
+ return $color->changeAlpha(NumberUtil::clamp($color->getAlpha() - $amount->valueInRangeWithUnit(0, 1, 'amount', ''), 0, 1));
+ }
+}
diff --git a/vendor/scssphp/scssphp/src/Function/FunctionRegistry.php b/vendor/scssphp/scssphp/src/Function/FunctionRegistry.php
new file mode 100644
index 000000000..3ee125a5a
--- /dev/null
+++ b/vendor/scssphp/scssphp/src/Function/FunctionRegistry.php
@@ -0,0 +1,182 @@
+<?php
+
+/**
+ * SCSSPHP
+ *
+ * @copyright 2012-2020 Leaf Corcoran
+ *
+ * @license http://opensource.org/licenses/MIT MIT
+ *
+ * @link http://scssphp.github.io/scssphp
+ */
+
+namespace ScssPhp\ScssPhp\Function;
+
+use League\Uri\Uri;
+use ScssPhp\ScssPhp\SassCallable\BuiltInCallable;
+use ScssPhp\ScssPhp\Value\Value;
+
+/**
+ * @internal
+ */
+class FunctionRegistry
+{
+ /**
+ * @var array<string, array{overloads: array<string, callable(list<Value>): Value>, url: string}>
+ */
+ private const BUILTIN_FUNCTIONS = [
+ // sass:color
+ 'red' => ['overloads' => ['$color' => [ColorFunctions::class, 'red']], 'url' => 'sass:color'],
+ 'green' => ['overloads' => ['$color' => [ColorFunctions::class, 'green']], 'url' => 'sass:color'],
+ 'blue' => ['overloads' => ['$color' => [ColorFunctions::class, 'blue']], 'url' => 'sass:color'],
+ 'mix' => ['overloads' => ['$color1, $color2, $weight: 50%' => [ColorFunctions::class, 'mix']], 'url' => 'sass:color'],
+ 'rgb' => ['overloads' => [
+ '$red, $green, $blue, $alpha' => [ColorFunctions::class, 'rgb'],
+ '$red, $green, $blue' => [ColorFunctions::class, 'rgb'],
+ '$color, $alpha' => [ColorFunctions::class, 'rgbTwoArgs'],
+ '$channels' => [ColorFunctions::class, 'rgbOneArgs'],
+ ], 'url' => 'sass:color'],
+ 'rgba' => ['overloads' => [
+ '$red, $green, $blue, $alpha' => [ColorFunctions::class, 'rgba'],
+ '$red, $green, $blue' => [ColorFunctions::class, 'rgba'],
+ '$color, $alpha' => [ColorFunctions::class, 'rgbaTwoArgs'],
+ '$channels' => [ColorFunctions::class, 'rgbaOneArgs'],
+ ], 'url' => 'sass:color'],
+ 'invert' => ['overloads' => ['$color, $weight: 100%' => [ColorFunctions::class, 'invert']], 'url' => 'sass:color'],
+ 'hue' => ['overloads' => ['$color' => [ColorFunctions::class, 'hue']], 'url' => 'sass:color'],
+ 'saturation' => ['overloads' => ['$color' => [ColorFunctions::class, 'saturation']], 'url' => 'sass:color'],
+ 'lightness' => ['overloads' => ['$color' => [ColorFunctions::class, 'lightness']], 'url' => 'sass:color'],
+ 'complement' => ['overloads' => ['$color' => [ColorFunctions::class, 'complement']], 'url' => 'sass:color'],
+ 'hsl' => ['overloads' => [
+ '$hue, $saturation, $lightness, $alpha' => [ColorFunctions::class, 'hsl'],
+ '$hue, $saturation, $lightness' => [ColorFunctions::class, 'hsl'],
+ '$hue, $saturation' => [ColorFunctions::class, 'hslTwoArgs'],
+ '$channels' => [ColorFunctions::class, 'hslOneArgs'],
+ ], 'url' => 'sass:color'],
+ 'hsla' => ['overloads' => [
+ '$hue, $saturation, $lightness, $alpha' => [ColorFunctions::class, 'hsla'],
+ '$hue, $saturation, $lightness' => [ColorFunctions::class, 'hsla'],
+ '$hue, $saturation' => [ColorFunctions::class, 'hslaTwoArgs'],
+ '$channels' => [ColorFunctions::class, 'hslaOneArgs'],
+ ], 'url' => 'sass:color'],
+ 'grayscale' => ['overloads' => ['$color' => [ColorFunctions::class, 'grayscale']], 'url' => 'sass:color'],
+ 'adjust-hue' => ['overloads' => ['$color, $degrees' => [ColorFunctions::class, 'adjustHue']], 'url' => 'sass:color'],
+ 'lighten' => ['overloads' => ['$color, $amount' => [ColorFunctions::class, 'lighten']], 'url' => 'sass:color'],
+ 'darken' => ['overloads' => ['$color, $amount' => [ColorFunctions::class, 'darken']], 'url' => 'sass:color'],
+ 'saturate' => ['overloads' => [
+ '$amount' => [ColorFunctions::class, 'saturateCss'],
+ '$color, $amount' => [ColorFunctions::class, 'saturate'],
+ ], 'url' => 'sass:color'],
+ 'desaturate' => ['overloads' => ['$color, $amount' => [ColorFunctions::class, 'desaturate']], 'url' => 'sass:color'],
+ 'opacify' => ['overloads' => ['$color, $amount' => [ColorFunctions::class, 'opacify']], 'url' => 'sass:color'],
+ 'fade-in' => ['overloads' => ['$color, $amount' => [ColorFunctions::class, 'opacify']], 'url' => 'sass:color'],
+ 'transparentize' => ['overloads' => ['$color, $amount' => [ColorFunctions::class, 'transparentize']], 'url' => 'sass:color'],
+ 'fade-out' => ['overloads' => ['$color, $amount' => [ColorFunctions::class, 'transparentize']], 'url' => 'sass:color'],
+ 'alpha' => ['overloads' => [
+ '$color' => [ColorFunctions::class, 'alpha'],
+ '$args...' => [ColorFunctions::class, 'alphaMicrosoft'],
+ ], 'url' => 'sass:color'],
+ 'opacity' => ['overloads' => ['$color' => [ColorFunctions::class, 'opacity']], 'url' => 'sass:color'],
+ 'ie-hex-str' => ['overloads' => ['$color' => [ColorFunctions::class, 'ieHexStr']], 'url' => 'sass:color'],
+ 'adjust-color' => ['overloads' => ['$color, $kwargs...' => [ColorFunctions::class, 'adjust']], 'url' => 'sass:color'],
+ 'scale-color' => ['overloads' => ['$color, $kwargs...' => [ColorFunctions::class, 'scale']], 'url' => 'sass:color'],
+ 'change-color' => ['overloads' => ['$color, $kwargs...' => [ColorFunctions::class, 'change']], 'url' => 'sass:color'],
+ // sass:list
+ 'length' => ['overloads' => ['$list' => [ListFunctions::class, 'length']], 'url' => 'sass:list'],
+ 'nth' => ['overloads' => ['$list, $n' => [ListFunctions::class, 'nth']], 'url' => 'sass:list'],
+ 'set-nth' => ['overloads' => ['$list, $n, $value' => [ListFunctions::class, 'setNth']], 'url' => 'sass:list'],
+ 'join' => ['overloads' => ['$list1, $list2, $separator: auto, $bracketed: auto' => [ListFunctions::class, 'join']], 'url' => 'sass:list'],
+ 'append' => ['overloads' => ['$list, $val, $separator: auto' => [ListFunctions::class, 'append']], 'url' => 'sass:list'],
+ 'zip' => ['overloads' => ['$lists...' => [ListFunctions::class, 'zip']], 'url' => 'sass:list'],
+ 'index' => ['overloads' => ['$list, $value' => [ListFunctions::class, 'index']], 'url' => 'sass:list'],
+ 'is-bracketed' => ['overloads' => ['$list' => [ListFunctions::class, 'isBracketed']], 'url' => 'sass:list'],
+ 'list-separator' => ['overloads' => ['$list' => [ListFunctions::class, 'separator']], 'url' => 'sass:list'],
+ // sass:map
+ 'map-get' => ['overloads' => ['$map, $key, $keys...' => [MapFunctions::class, 'get']], 'url' => 'sass:map'],
+ 'map-merge' => ['overloads' => [
+ '$map1, $map2' => [MapFunctions::class, 'mergeTwoArgs'],
+ '$map1, $args...' => [MapFunctions::class, 'mergeVariadic'],
+ ], 'url' => 'sass:map'],
+ 'map-remove' => ['overloads' => [
+ // Because the signature below has an explicit `$key` argument, it doesn't
+ // allow zero keys to be passed. We want to allow that case, so we add an
+ // explicit overload for it.
+ '$map' => [MapFunctions::class, 'removeNoKeys'],
+ // The first argument has special handling so that the $key parameter can be
+ // passed by name.
+ '$map, $key, $keys...' => [MapFunctions::class, 'remove'],
+ ], 'url' => 'sass:map'],
+ 'map-keys' => ['overloads' => ['$map' => [MapFunctions::class, 'keys']], 'url' => 'sass:map'],
+ 'map-values' => ['overloads' => ['$map' => [MapFunctions::class, 'values']], 'url' => 'sass:map'],
+ 'map-has-key' => ['overloads' => ['$map, $key, $keys...' => [MapFunctions::class, 'hasKey']], 'url' => 'sass:map'],
+ // sass:math
+ 'abs' => ['overloads' => ['$number' => [MathFunctions::class, 'abs']], 'url' => 'sass:math'],
+ 'ceil' => ['overloads' => ['$number' => [MathFunctions::class, 'ceil']], 'url' => 'sass:math'],
+ 'floor' => ['overloads' => ['$number' => [MathFunctions::class, 'floor']], 'url' => 'sass:math'],
+ 'max' => ['overloads' => ['$numbers...' => [MathFunctions::class, 'max']], 'url' => 'sass:math'],
+ 'min' => ['overloads' => ['$numbers...' => [MathFunctions::class, 'min']], 'url' => 'sass:math'],
+ 'random' => ['overloads' => ['$limit: null' => [MathFunctions::class, 'random']], 'url' => 'sass:math'],
+ 'percentage' => ['overloads' => ['$number' => [MathFunctions::class, 'percentage']], 'url' => 'sass:math'],
+ 'round' => ['overloads' => ['$number' => [MathFunctions::class, 'round']], 'url' => 'sass:math'],
+ 'unit' => ['overloads' => ['$number' => [MathFunctions::class, 'unit']], 'url' => 'sass:math'],
+ 'comparable' => ['overloads' => ['$number1, $number2' => [MathFunctions::class, 'compatible']], 'url' => 'sass:math'],
+ 'unitless' => ['overloads' => ['$number' => [MathFunctions::class, 'isUnitless']], 'url' => 'sass:math'],
+ // sass:meta
+ 'feature-exists' => ['overloads' => ['$feature' => [MetaFunctions::class, 'featureExists']], 'url' => 'sass:meta'],
+ 'inspect' => ['overloads' => ['$value' => [MetaFunctions::class, 'inspect']], 'url' => 'sass:meta'],
+ 'type-of' => ['overloads' => ['$value' => [MetaFunctions::class, 'typeof']], 'url' => 'sass:meta'],
+ 'keywords' => ['overloads' => ['$args' => [MetaFunctions::class, 'keywords']], 'url' => 'sass:meta'],
+ // sass:selector
+ 'is-superselector' => ['overloads' => ['$super, $sub' => [SelectorFunctions::class, 'isSuperselector']], 'url' => 'sass:selector'],
+ 'simple-selectors' => ['overloads' => ['$selector' => [SelectorFunctions::class, 'simpleSelectors']], 'url' => 'sass:selector'],
+ 'selector-parse' => ['overloads' => ['$selector' => [SelectorFunctions::class, 'parse']], 'url' => 'sass:selector'],
+ 'selector-nest' => ['overloads' => ['$selectors...' => [SelectorFunctions::class, 'nest']], 'url' => 'sass:selector'],
+ 'selector-append' => ['overloads' => ['$selectors...' => [SelectorFunctions::class, 'append']], 'url' => 'sass:selector'],
+ 'selector-extend' => ['overloads' => ['$selector, $extendee, $extender' => [SelectorFunctions::class, 'extend']], 'url' => 'sass:selector'],
+ 'selector-replace' => ['overloads' => ['$selector, $original, $replacement' => [SelectorFunctions::class, 'replace']], 'url' => 'sass:selector'],
+ 'selector-unify' => ['overloads' => ['$selector1, $selector2' => [SelectorFunctions::class, 'unify']], 'url' => 'sass:selector'],
+ // sass:string
+ 'unquote' => ['overloads' => ['$string' => [StringFunctions::class, 'unquote']], 'url' => 'sass:string'],
+ 'quote' => ['overloads' => ['$string' => [StringFunctions::class, 'quote']], 'url' => 'sass:string'],
+ 'to-upper-case' => ['overloads' => ['$string' => [StringFunctions::class, 'toUpperCase']], 'url' => 'sass:string'],
+ 'to-lower-case' => ['overloads' => ['$string' => [StringFunctions::class, 'toLowerCase']], 'url' => 'sass:string'],
+ 'unique-id' => ['overloads' => ['' => [StringFunctions::class, 'uniqueId']], 'url' => 'sass:string'],
+ 'str-length' => ['overloads' => ['$string' => [StringFunctions::class, 'length']], 'url' => 'sass:string'],
+ 'str-insert' => ['overloads' => ['$string, $insert, $index' => [StringFunctions::class, 'insert']], 'url' => 'sass:string'],
+ 'str-index' => ['overloads' => ['$string, $substring' => [StringFunctions::class, 'index']], 'url' => 'sass:string'],
+ 'str-slice' => ['overloads' => ['$string, $start-at, $end-at: -1' => [StringFunctions::class, 'slice']], 'url' => 'sass:string'],
+ ];
+
+ /**
+ * Special meta functions defined directly in the {@see EvaluateVisitor} constructor
+ */
+ private const SPECIAL_META_GLOBAL_FUNCTIONS = [
+ 'global-variable-exists',
+ 'variable-exists',
+ 'function-exists',
+ 'mixin-exists',
+ 'content-exists',
+ 'get-function',
+ 'get-mixin',
+ 'call',
+ ];
+
+ public static function has(string $name): bool
+ {
+ return isset(self::BUILTIN_FUNCTIONS[$name]);
+ }
+
+ public static function get(string $name): BuiltInCallable
+ {
+ if (!isset(self::BUILTIN_FUNCTIONS[$name])) {
+ throw new \InvalidArgumentException("There is no builtin function named $name.");
+ }
+
+ return BuiltInCallable::overloadedFunction($name, self::BUILTIN_FUNCTIONS[$name]['overloads'], Uri::new(self::BUILTIN_FUNCTIONS[$name]['url']));
+ }
+
+ public static function isBuiltinFunction(string $name): bool
+ {
+ return isset(self::BUILTIN_FUNCTIONS[$name]) || \in_array($name, self::SPECIAL_META_GLOBAL_FUNCTIONS, true);
+ }
+}
diff --git a/vendor/scssphp/scssphp/src/Function/ListFunctions.php b/vendor/scssphp/scssphp/src/Function/ListFunctions.php
new file mode 100644
index 000000000..336203021
--- /dev/null
+++ b/vendor/scssphp/scssphp/src/Function/ListFunctions.php
@@ -0,0 +1,183 @@
+<?php
+
+/**
+ * SCSSPHP
+ *
+ * @copyright 2012-2020 Leaf Corcoran
+ *
+ * @license http://opensource.org/licenses/MIT MIT
+ *
+ * @link http://scssphp.github.io/scssphp
+ */
+
+namespace ScssPhp\ScssPhp\Function;
+
+use ScssPhp\ScssPhp\Exception\SassScriptException;
+use ScssPhp\ScssPhp\Util\IterableUtil;
+use ScssPhp\ScssPhp\Value\ListSeparator;
+use ScssPhp\ScssPhp\Value\SassBoolean;
+use ScssPhp\ScssPhp\Value\SassList;
+use ScssPhp\ScssPhp\Value\SassNull;
+use ScssPhp\ScssPhp\Value\SassNumber;
+use ScssPhp\ScssPhp\Value\SassString;
+use ScssPhp\ScssPhp\Value\Value;
+
+/**
+ * @internal
+ */
+class ListFunctions
+{
+ /**
+ * @param list<Value> $arguments
+ */
+ public static function length(array $arguments): Value
+ {
+ return SassNumber::create(\count($arguments[0]->asList()));
+ }
+
+ /**
+ * @param list<Value> $arguments
+ */
+ public static function nth(array $arguments): Value
+ {
+ $list = $arguments[0];
+ $index = $arguments[1];
+
+ return $list->asList()[$list->sassIndexToListIndex($index, 'n')];
+ }
+
+ /**
+ * @param list<Value> $arguments
+ */
+ public static function setNth(array $arguments): Value
+ {
+ $list = $arguments[0];
+ $index = $arguments[1];
+ $value = $arguments[2];
+
+ $newList = $list->asList();
+ $newList[$list->sassIndexToListIndex($index, 'n')] = $value;
+ \assert(array_is_list($newList), 'The mutation is guaranteed to affect an existing index');
+
+ return $list->withListContents($newList);
+ }
+
+ /**
+ * @param list<Value> $arguments
+ */
+ public static function join(array $arguments): Value
+ {
+ $list1 = $arguments[0];
+ $list2 = $arguments[1];
+ $separatorParam = $arguments[2]->assertString('separator');
+ $bracketedParam = $arguments[3];
+
+ $separator = match ($separatorParam->getText()) {
+ 'auto' => self::getAutoJoinSeparator($list1->getSeparator(), $list2->getSeparator()),
+ 'space' => ListSeparator::SPACE,
+ 'comma' => ListSeparator::COMMA,
+ 'slash' => ListSeparator::SLASH,
+ default => throw new SassScriptException('$separator: Must be "space", "comma", "slash", or "auto".')
+ };
+
+ $bracketed = $bracketedParam instanceof SassString && $bracketedParam->getText() === 'auto' ? $list1->hasBrackets() : $bracketedParam->isTruthy();
+
+ $newList = [...$list1->asList(), ...$list2->asList()];
+
+ return new SassList($newList, $separator, $bracketed);
+ }
+
+ /**
+ * @param list<Value> $arguments
+ */
+ public static function append(array $arguments): Value
+ {
+ $list = $arguments[0];
+ $value = $arguments[1];
+ $separatorParam = $arguments[2]->assertString('separator');
+
+ $separator = match ($separatorParam->getText()) {
+ 'auto' => $list->getSeparator() === ListSeparator::UNDECIDED ? ListSeparator::SPACE : $list->getSeparator(),
+ 'space' => ListSeparator::SPACE,
+ 'comma' => ListSeparator::COMMA,
+ 'slash' => ListSeparator::SLASH,
+ default => throw new SassScriptException('$separator: Must be "space", "comma", "slash", or "auto".')
+ };
+
+ $newList = [...$list->asList(), $value];
+
+ return $list->withListContents($newList, $separator);
+ }
+
+ /**
+ * @param list<Value> $arguments
+ */
+ public static function zip(array $arguments): Value
+ {
+ $lists = array_map(fn (Value $list) => $list->asList(), $arguments[0]->asList());
+
+ if (\count($lists) === 0) {
+ return SassList::createEmpty(ListSeparator::COMMA);
+ }
+
+ $i = 0;
+ $results = [];
+
+ while (IterableUtil::every($lists, fn ($list) => $i !== \count($list))) {
+ $results[] = new SassList(array_map(fn ($list) => $list[$i], $lists), ListSeparator::SPACE);
+ $i++;
+ }
+
+ return new SassList($results, ListSeparator::COMMA);
+ }
+
+ /**
+ * @param list<Value> $arguments
+ */
+ public static function index(array $arguments): Value
+ {
+ $list = $arguments[0]->asList();
+ $value = $arguments[1];
+
+ foreach ($list as $index => $item) {
+ if ($item->equals($value)) {
+ return SassNumber::create($index + 1);
+ }
+ }
+
+ return SassNull::create();
+ }
+
+ /**
+ * @param list<Value> $arguments
+ */
+ public static function separator(array $arguments): Value
+ {
+ return match ($arguments[0]->getSeparator()) {
+ ListSeparator::COMMA => new SassString('comma', false),
+ ListSeparator::SLASH => new SassString('slash', false),
+ default => new SassString('space', false),
+ };
+ }
+
+ /**
+ * @param list<Value> $arguments
+ */
+ public static function isBracketed(array $arguments): Value
+ {
+ return SassBoolean::create($arguments[0]->hasBrackets());
+ }
+
+ private static function getAutoJoinSeparator(ListSeparator $separator1, ListSeparator $separator2): ListSeparator
+ {
+ if ($separator1 === ListSeparator::UNDECIDED && $separator2 === ListSeparator::UNDECIDED) {
+ return ListSeparator::SPACE;
+ }
+
+ if ($separator1 === ListSeparator::UNDECIDED) {
+ return $separator2;
+ }
+
+ return $separator1;
+ }
+}
diff --git a/vendor/scssphp/scssphp/src/Function/MapFunctions.php b/vendor/scssphp/scssphp/src/Function/MapFunctions.php
new file mode 100644
index 000000000..135d7db50
--- /dev/null
+++ b/vendor/scssphp/scssphp/src/Function/MapFunctions.php
@@ -0,0 +1,213 @@
+<?php
+
+/**
+ * SCSSPHP
+ *
+ * @copyright 2012-2020 Leaf Corcoran
+ *
+ * @license http://opensource.org/licenses/MIT MIT
+ *
+ * @link http://scssphp.github.io/scssphp
+ */
+
+namespace ScssPhp\ScssPhp\Function;
+
+use ScssPhp\ScssPhp\Collection\Map;
+use ScssPhp\ScssPhp\Exception\SassScriptException;
+use ScssPhp\ScssPhp\Util\ListUtil;
+use ScssPhp\ScssPhp\Value\ListSeparator;
+use ScssPhp\ScssPhp\Value\SassBoolean;
+use ScssPhp\ScssPhp\Value\SassList;
+use ScssPhp\ScssPhp\Value\SassMap;
+use ScssPhp\ScssPhp\Value\SassNull;
+use ScssPhp\ScssPhp\Value\Value;
+
+/**
+ * @internal
+ */
+class MapFunctions
+{
+ /**
+ * @param list<Value> $arguments
+ */
+ public static function get(array $arguments): Value
+ {
+ $map = $arguments[0]->assertMap('map');
+ $keys = [$arguments[1], ...$arguments[2]->asList()];
+
+ foreach (ListUtil::exceptLast($keys) as $key) {
+ $value = $map->getContents()->get($key);
+
+ if (!$value instanceof SassMap) {
+ return SassNull::create();
+ }
+
+ $map = $value;
+ }
+
+ return $map->getContents()->get(ListUtil::last($keys)) ?? SassNull::create();
+ }
+
+ /**
+ * @param list<Value> $arguments
+ */
+ public static function mergeTwoArgs(array $arguments): Value
+ {
+ $map1 = $arguments[0]->assertMap('map1');
+ $map2 = $arguments[1]->assertMap('map2');
+
+ $result = Map::of($map1->getContents());
+
+ foreach ($map2->getContents() as $key => $value) {
+ $result->put($key, $value);
+ }
+
+ return SassMap::create($result);
+ }
+
+ /**
+ * @param list<Value> $arguments
+ */
+ public static function mergeVariadic(array $arguments): Value
+ {
+ $map1 = $arguments[0]->assertMap('map1');
+ $args = $arguments[1]->asList();
+
+ if ($args === []) {
+ throw new SassScriptException('Expected $args to contain a key.');
+ }
+
+ if (\count($args) === 1) {
+ throw new SassScriptException('Expected $args to contain a map.');
+ }
+
+ $keys = ListUtil::exceptLast($args);
+ $map2 = ListUtil::last($args)->assertMap('map2');
+
+ return self::modify($map1, $keys, function (Value $oldValue) use ($map2) {
+ $nestedMap = $oldValue->tryMap();
+ if ($nestedMap === null) {
+ return $map2;
+ }
+
+ $result = Map::of($nestedMap->getContents());
+
+ foreach ($map2->getContents() as $key => $value) {
+ $result->put($key, $value);
+ }
+
+ return SassMap::create($result);
+ });
+ }
+
+ /**
+ * @param list<Value> $arguments
+ */
+ public static function removeNoKeys(array $arguments): Value
+ {
+ return $arguments[0]->assertMap('map');
+ }
+
+ /**
+ * @param list<Value> $arguments
+ */
+ public static function remove(array $arguments): Value
+ {
+ $map = $arguments[0]->assertMap('map');
+ $keys = [$arguments[1], ...$arguments[2]->asList()];
+
+ $mutableMap = Map::of($map->getContents());
+
+ foreach ($keys as $key) {
+ $mutableMap->remove($key);
+ }
+
+ return SassMap::create($mutableMap);
+ }
+
+ /**
+ * @param list<Value> $arguments
+ */
+ public static function keys(array $arguments): Value
+ {
+ return new SassList($arguments[0]->assertMap('map')->getContents()->keys(), ListSeparator::COMMA);
+ }
+
+ /**
+ * @param list<Value> $arguments
+ */
+ public static function values(array $arguments): Value
+ {
+ return new SassList($arguments[0]->assertMap('map')->getContents()->values(), ListSeparator::COMMA);
+ }
+
+ /**
+ * @param list<Value> $arguments
+ */
+ public static function hasKey(array $arguments): Value
+ {
+ $map = $arguments[0]->assertMap('map');
+ $keys = [$arguments[1], ...$arguments[2]->asList()];
+
+ foreach (ListUtil::exceptLast($keys) as $key) {
+ $value = $map->getContents()->get($key);
+
+ if (!$value instanceof SassMap) {
+ return SassBoolean::create(false);
+ }
+
+ $map = $value;
+ }
+
+ return SassBoolean::create($map->getContents()->containsKey(ListUtil::last($keys)));
+ }
+
+ /**
+ * Updates the specified value in $map by applying the $modify callback to
+ * it, then returns the resulting map.
+ *
+ * If more than one key is provided, this means the map targeted for update is
+ * nested within $map. The multiple $keys form a path of nested maps that
+ * leads to the targeted value, which is passed to $modify.
+ *
+ * If any value along the path (other than the last one) is not a map and
+ * $addNesting is `true`, this creates nested maps to match $keys and passes
+ * {@see SassNull} to $modify. Otherwise, this fails and returns $map with no
+ * changes.
+ *
+ * If no keys are provided, this passes $map directly to modify and returns
+ * the result.
+ *
+ * @param Value[] $keys
+ * @param callable(Value $old): Value $modify
+ *
+ * @param-immediately-invoked-callable $modify
+ */
+ private static function modify(SassMap $map, array $keys, callable $modify, bool $addNesting = true): Value
+ {
+ $iterator = new \ArrayIterator($keys);
+
+ $modifyNestedMap = function (SassMap $map) use ($iterator, $modify, $addNesting, &$modifyNestedMap): SassMap {
+ $mutableMap = Map::of($map->getContents());
+ $key = $iterator->current();
+
+ $iterator->next();
+ if (!$iterator->valid()) {
+ $mutableMap->put($key, $modify($mutableMap->get($key) ?? SassNull::create()));
+ return SassMap::create($mutableMap);
+ }
+
+ $nestedMap = $mutableMap->get($key)?->tryMap();
+ if ($nestedMap === null && !$addNesting) {
+ return SassMap::create($mutableMap);
+ }
+
+ $mutableMap->put($key, $modifyNestedMap($nestedMap ?? SassMap::createEmpty()));
+ return SassMap::create($mutableMap);
+ };
+
+ $iterator->rewind();
+
+ return $iterator->valid() ? $modifyNestedMap($map) : $modify($map);
+ }
+}
diff --git a/vendor/scssphp/scssphp/src/Function/MathFunctions.php b/vendor/scssphp/scssphp/src/Function/MathFunctions.php
new file mode 100644
index 000000000..324b13022
--- /dev/null
+++ b/vendor/scssphp/scssphp/src/Function/MathFunctions.php
@@ -0,0 +1,205 @@
+<?php
+
+/**
+ * SCSSPHP
+ *
+ * @copyright 2012-2020 Leaf Corcoran
+ *
+ * @license http://opensource.org/licenses/MIT MIT
+ *
+ * @link http://scssphp.github.io/scssphp
+ */
+
+namespace ScssPhp\ScssPhp\Function;
+
+use ScssPhp\ScssPhp\Deprecation;
+use ScssPhp\ScssPhp\Exception\SassScriptException;
+use ScssPhp\ScssPhp\Value\SassBoolean;
+use ScssPhp\ScssPhp\Value\SassNull;
+use ScssPhp\ScssPhp\Value\SassNumber;
+use ScssPhp\ScssPhp\Value\SassString;
+use ScssPhp\ScssPhp\Value\Value;
+use ScssPhp\ScssPhp\Warn;
+
+/**
+ * @internal
+ */
+final class MathFunctions
+{
+ /**
+ * @param list<Value> $arguments
+ */
+ public static function abs(array $arguments): Value
+ {
+ $number = $arguments[0]->assertNumber('number');
+ // TODO implement the deprecation for the % unit once modules are implemented to provided the replacement
+
+ return SassNumber::withUnits(abs($number->getValue()), $number->getNumeratorUnits(), $number->getDenominatorUnits());
+ }
+
+ /**
+ * @param list<Value> $arguments
+ */
+ public static function ceil(array $arguments): Value
+ {
+ return self::numberFunction($arguments, ceil(...));
+ }
+
+ /**
+ * @param list<Value> $arguments
+ */
+ public static function floor(array $arguments): Value
+ {
+ return self::numberFunction($arguments, floor(...));
+ }
+
+ /**
+ * @param list<Value> $arguments
+ */
+ public static function max(array $arguments): Value
+ {
+ $max = null;
+
+ foreach ($arguments[0]->asList() as $value) {
+ $number = $value->assertNumber();
+
+ if ($max === null || $max->lessThan($number)->isTruthy()) {
+ $max = $number;
+ }
+ }
+
+ if ($max !== null) {
+ return $max;
+ }
+
+ throw new SassScriptException('At least one argument must be passed.');
+ }
+
+ /**
+ * @param list<Value> $arguments
+ */
+ public static function min(array $arguments): Value
+ {
+ $min = null;
+
+ foreach ($arguments[0]->asList() as $value) {
+ $number = $value->assertNumber();
+
+ if ($min === null || $min->greaterThan($number)->isTruthy()) {
+ $min = $number;
+ }
+ }
+
+ if ($min !== null) {
+ return $min;
+ }
+
+ throw new SassScriptException('At least one argument must be passed.');
+ }
+
+ /**
+ * @param list<Value> $arguments
+ */
+ public static function round(array $arguments): Value
+ {
+ return self::numberFunction($arguments, round(...));
+ }
+
+ /**
+ * @param list<Value> $arguments
+ */
+ public static function compatible(array $arguments): Value
+ {
+ $number1 = $arguments[0]->assertNumber('number1');
+ $number2 = $arguments[1]->assertNumber('number2');
+
+ return SassBoolean::create($number1->isComparableTo($number2));
+ }
+
+ /**
+ * @param list<Value> $arguments
+ */
+ public static function isUnitless(array $arguments): Value
+ {
+ $number = $arguments[0]->assertNumber('number');
+
+ return SassBoolean::create(!$number->hasUnits());
+ }
+
+ /**
+ * @param list<Value> $arguments
+ */
+ public static function unit(array $arguments): Value
+ {
+ $number = $arguments[0]->assertNumber('number');
+
+ return new SassString($number->getUnitString(), true);
+ }
+
+ /**
+ * @param list<Value> $arguments
+ */
+ public static function percentage(array $arguments): Value
+ {
+ $number = $arguments[0]->assertNumber('number');
+ $number->assertNoUnits('number');
+
+ return SassNumber::create($number->getValue() * 100, '%');
+ }
+
+ /**
+ * @param list<Value> $arguments
+ */
+ public static function random(array $arguments): Value
+ {
+ if ($arguments[0] instanceof SassNull) {
+ // TODO use a better algorithm to generate a random float.
+ $max = mt_getrandmax();
+
+ return SassNumber::create(mt_rand(0, $max - 1) / $max);
+ }
+
+ $limit = $arguments[0]->assertNumber('limit');
+
+ if ($limit->hasUnits()) {
+ $unitString = $limit->getUnitString();
+
+ // TODO update the message when implementing modules and deprecating division.
+ Warn::forDeprecation(
+ <<<TXT
+ random() will no longer ignore \$limit units ($limit) in a future release.
+
+ Recommendation: random(\$limit / 1$unitString) * 1$unitString
+
+ To preserve current behavior: random(\$limit / 1$unitString)
+
+ More info: https://sass-lang.com/d/function-units
+ TXT,
+ Deprecation::functionUnits
+ );
+ }
+
+ $limitScalar = $limit->assertInt('limit');
+ if ($limitScalar < 1) {
+ throw new SassScriptException("\$limit: Must be greater than 0, was $limit.");
+ }
+
+ return SassNumber::create(mt_rand(1, $limitScalar));
+ }
+
+ /**
+ * Implements a callable that transforms a number's value
+ * using $transform and preserves its units.
+ *
+ * @param list<Value> $arguments
+ * @param callable(float): float $transform
+ *
+ * @param-immediately-invoked-callable $transform
+ */
+ private static function numberFunction(array $arguments, callable $transform): Value
+ {
+ $number = $arguments[0]->assertNumber('number');
+
+ return SassNumber::withUnits($transform($number->getValue()), $number->getNumeratorUnits(), $number->getDenominatorUnits());
+ }
+}
diff --git a/vendor/scssphp/scssphp/src/Function/MetaFunctions.php b/vendor/scssphp/scssphp/src/Function/MetaFunctions.php
new file mode 100644
index 000000000..379cc220b
--- /dev/null
+++ b/vendor/scssphp/scssphp/src/Function/MetaFunctions.php
@@ -0,0 +1,96 @@
+<?php
+
+/**
+ * SCSSPHP
+ *
+ * @copyright 2012-2020 Leaf Corcoran
+ *
+ * @license http://opensource.org/licenses/MIT MIT
+ *
+ * @link http://scssphp.github.io/scssphp
+ */
+
+namespace ScssPhp\ScssPhp\Function;
+
+use ScssPhp\ScssPhp\Collection\Map;
+use ScssPhp\ScssPhp\Deprecation;
+use ScssPhp\ScssPhp\Exception\SassScriptException;
+use ScssPhp\ScssPhp\Value\SassArgumentList;
+use ScssPhp\ScssPhp\Value\SassBoolean;
+use ScssPhp\ScssPhp\Value\SassCalculation;
+use ScssPhp\ScssPhp\Value\SassColor;
+use ScssPhp\ScssPhp\Value\SassFunction;
+use ScssPhp\ScssPhp\Value\SassList;
+use ScssPhp\ScssPhp\Value\SassMap;
+use ScssPhp\ScssPhp\Value\SassMixin;
+use ScssPhp\ScssPhp\Value\SassNull;
+use ScssPhp\ScssPhp\Value\SassNumber;
+use ScssPhp\ScssPhp\Value\SassString;
+use ScssPhp\ScssPhp\Value\Value;
+use ScssPhp\ScssPhp\Warn;
+
+/**
+ * @internal
+ */
+final class MetaFunctions
+{
+ /**
+ * @param list<Value> $arguments
+ */
+ public static function featureExists(array $arguments): Value
+ {
+ Warn::forDeprecation("The feature-exists() function is deprecated.\n\nMore info: https://sass-lang.com/d/feature-exists", Deprecation::featureExists);
+
+ $feature = $arguments[0]->assertString('feature');
+
+ return SassBoolean::create(\in_array($feature->getText(), ['global-variable-shadowing', 'extend-selector-pseudoclass', 'units-level-3', 'at-error', 'custom-property'], true));
+ }
+
+ /**
+ * @param list<Value> $arguments
+ */
+ public static function inspect(array $arguments): Value
+ {
+ return new SassString((string) $arguments[0], false);
+ }
+
+ /**
+ * @param list<Value> $arguments
+ */
+ public static function typeof(array $arguments): Value
+ {
+ $value = $arguments[0];
+
+ return new SassString(match (true) {
+ $value instanceof SassArgumentList => 'arglist',
+ $value instanceof SassBoolean => 'bool',
+ $value instanceof SassColor => 'color',
+ $value instanceof SassList => 'list',
+ $value instanceof SassMap => 'map',
+ $value instanceof SassNull => 'null',
+ $value instanceof SassNumber => 'number',
+ $value instanceof SassFunction => 'function',
+ $value instanceof SassMixin => 'mixin',
+ $value instanceof SassCalculation => 'calculation',
+ $value instanceof SassString => 'string',
+ default => throw new SassScriptException("[BUG] Unknown value type $value"),
+ }, false);
+ }
+
+ /**
+ * @param list<Value> $arguments
+ */
+ public static function keywords(array $arguments): Value
+ {
+ if ($arguments[0] instanceof SassArgumentList) {
+ $map = new Map();
+ foreach ($arguments[0]->getKeywords() as $key => $value) {
+ $map->put(new SassString($key, false), $value);
+ }
+
+ return SassMap::create($map);
+ }
+
+ throw SassScriptException::forArgument("$arguments[0] is not an argument list.", 'args');
+ }
+}
diff --git a/vendor/scssphp/scssphp/src/Function/SelectorFunctions.php b/vendor/scssphp/scssphp/src/Function/SelectorFunctions.php
new file mode 100644
index 000000000..bcfa7338c
--- /dev/null
+++ b/vendor/scssphp/scssphp/src/Function/SelectorFunctions.php
@@ -0,0 +1,204 @@
+<?php
+
+/**
+ * SCSSPHP
+ *
+ * @copyright 2012-2020 Leaf Corcoran
+ *
+ * @license http://opensource.org/licenses/MIT MIT
+ *
+ * @link http://scssphp.github.io/scssphp
+ */
+
+namespace ScssPhp\ScssPhp\Function;
+
+use ScssPhp\ScssPhp\Ast\Selector\ComplexSelector;
+use ScssPhp\ScssPhp\Ast\Selector\ComplexSelectorComponent;
+use ScssPhp\ScssPhp\Ast\Selector\CompoundSelector;
+use ScssPhp\ScssPhp\Ast\Selector\ParentSelector;
+use ScssPhp\ScssPhp\Ast\Selector\SelectorList;
+use ScssPhp\ScssPhp\Ast\Selector\SimpleSelector;
+use ScssPhp\ScssPhp\Ast\Selector\TypeSelector;
+use ScssPhp\ScssPhp\Ast\Selector\UniversalSelector;
+use ScssPhp\ScssPhp\Evaluation\EvaluationContext;
+use ScssPhp\ScssPhp\Exception\SassScriptException;
+use ScssPhp\ScssPhp\Extend\ConcreteExtensionStore;
+use ScssPhp\ScssPhp\Util\ArrayUtil;
+use ScssPhp\ScssPhp\Value\ListSeparator;
+use ScssPhp\ScssPhp\Value\SassBoolean;
+use ScssPhp\ScssPhp\Value\SassList;
+use ScssPhp\ScssPhp\Value\SassNull;
+use ScssPhp\ScssPhp\Value\SassString;
+use ScssPhp\ScssPhp\Value\Value;
+
+/**
+ * @internal
+ */
+final class SelectorFunctions
+{
+ /**
+ * @param list<Value> $arguments
+ */
+ public static function nest(array $arguments): Value
+ {
+ $selectors = $arguments[0]->asList();
+
+ if (\count($selectors) === 0) {
+ throw new SassScriptException('$selectors: At least one selector must be passed.');
+ }
+
+ $first = true;
+
+ return ArrayUtil::reduce(array_map(function (Value $selector) use (&$first) {
+ $result = $selector->assertSelector(allowParent: !$first);
+ $first = false;
+
+ return $result;
+ }, $selectors), fn (SelectorList $parent, SelectorList $child) => $child->nestWithin($parent))->asSassList();
+ }
+
+ /**
+ * @param list<Value> $arguments
+ */
+ public static function append(array $arguments): Value
+ {
+ $selectors = $arguments[0]->asList();
+
+ if (\count($selectors) === 0) {
+ throw new SassScriptException('$selectors: At least one selector must be passed.');
+ }
+
+ $span = EvaluationContext::getCurrent()->getCurrentCallableSpan();
+
+ return ArrayUtil::reduce(array_map(fn(Value $selector) => $selector->assertSelector(), $selectors), function (SelectorList $parent, SelectorList $child) use ($span) {
+ return (new SelectorList(array_map(function (ComplexSelector $complex) use ($span, $parent) {
+ if (\count($complex->getLeadingCombinators()) > 0) {
+ throw new SassScriptException("Can't append $complex to $parent.");
+ }
+
+ $component = $complex->getComponents()[0];
+ $rest = array_slice($complex->getComponents(), 1);
+ $newCompound = self::prependParent($component->getSelector());
+
+ if ($newCompound === null) {
+ throw new SassScriptException("Can't append $complex to $parent.");
+ }
+
+ return new ComplexSelector([], [
+ new ComplexSelectorComponent($newCompound, $component->getCombinators(), $span),
+ ...$rest,
+ ], $span);
+ }, $child->getComponents()), $span))->nestWithin($parent);
+ })->asSassList();
+ }
+
+ /**
+ * @param list<Value> $arguments
+ */
+ public static function extend(array $arguments): Value
+ {
+ $selector = $arguments[0]->assertSelector('selector');
+ $selector->assertNotBogus('selector');
+ $target = $arguments[1]->assertSelector('extendee');
+ $target->assertNotBogus('extendee');
+ $source = $arguments[2]->assertSelector('extender');
+ $source->assertNotBogus('extender');
+
+ return ConcreteExtensionStore::extend($selector, $source, $target, EvaluationContext::getCurrent()->getCurrentCallableSpan())->asSassList();
+ }
+
+ /**
+ * @param list<Value> $arguments
+ */
+ public static function replace(array $arguments): Value
+ {
+ $selector = $arguments[0]->assertSelector('selector');
+ $selector->assertNotBogus('selector');
+ $target = $arguments[1]->assertSelector('original');
+ $target->assertNotBogus('original');
+ $source = $arguments[2]->assertSelector('replacement');
+ $source->assertNotBogus('replacement');
+
+ return ConcreteExtensionStore::replace($selector, $source, $target, EvaluationContext::getCurrent()->getCurrentCallableSpan())->asSassList();
+ }
+
+ /**
+ * @param list<Value> $arguments
+ */
+ public static function unify(array $arguments): Value
+ {
+ $selector1 = $arguments[0]->assertSelector('selector1');
+ $selector1->assertNotBogus('selector1');
+
+ $selector2 = $arguments[1]->assertSelector('selector2');
+ $selector2->assertNotBogus('selector2');
+
+ return $selector1->unify($selector2)?->asSassList() ?? SassNull::create();
+ }
+
+ /**
+ * @param list<Value> $arguments
+ */
+ public static function isSuperselector(array $arguments): Value
+ {
+ $selector1 = $arguments[0]->assertSelector('super');
+ $selector1->assertNotBogus('super');
+
+ $selector2 = $arguments[1]->assertSelector('sub');
+ $selector2->assertNotBogus('sub');
+
+ return SassBoolean::create($selector1->isSuperselector($selector2));
+ }
+
+ /**
+ * @param list<Value> $arguments
+ */
+ public static function simpleSelectors(array $arguments): Value
+ {
+ $selector = $arguments[0]->assertCompoundSelector('selector');
+
+ return new SassList(
+ array_map(fn (SimpleSelector $simple) => new SassString((string) $simple, false), $selector->getComponents()),
+ ListSeparator::COMMA
+ );
+ }
+
+ /**
+ * @param list<Value> $arguments
+ */
+ public static function parse(array $arguments): Value
+ {
+ return $arguments[0]->assertSelector('selector')->asSassList();
+ }
+
+ /**
+ * Adds a {@see ParentSelector} to the beginning of $compound, or returns `null` if
+ * that wouldn't produce a valid selector.
+ */
+ private static function prependParent(CompoundSelector $compound): ?CompoundSelector
+ {
+ $span = EvaluationContext::getCurrent()->getCurrentCallableSpan();
+
+ $firstComponent = $compound->getComponents()[0];
+
+ if ($firstComponent instanceof UniversalSelector) {
+ return null;
+ }
+
+ if ($firstComponent instanceof TypeSelector && $firstComponent->getName()->getNamespace() !== null) {
+ return null;
+ }
+
+ if ($firstComponent instanceof TypeSelector) {
+ return new CompoundSelector([
+ new ParentSelector($span, $firstComponent->getName()->getName()),
+ ...array_slice($compound->getComponents(), 1),
+ ], $span);
+ }
+
+ return new CompoundSelector([
+ new ParentSelector($span),
+ ...$compound->getComponents(),
+ ], $span);
+ }
+}
diff --git a/vendor/scssphp/scssphp/src/Function/StringFunctions.php b/vendor/scssphp/scssphp/src/Function/StringFunctions.php
new file mode 100644
index 000000000..20fbf187e
--- /dev/null
+++ b/vendor/scssphp/scssphp/src/Function/StringFunctions.php
@@ -0,0 +1,219 @@
+<?php
+
+/**
+ * SCSSPHP
+ *
+ * @copyright 2012-2020 Leaf Corcoran
+ *
+ * @license http://opensource.org/licenses/MIT MIT
+ *
+ * @link http://scssphp.github.io/scssphp
+ */
+
+namespace ScssPhp\ScssPhp\Function;
+
+use ScssPhp\ScssPhp\Util\StringUtil;
+use ScssPhp\ScssPhp\Value\SassNull;
+use ScssPhp\ScssPhp\Value\SassNumber;
+use ScssPhp\ScssPhp\Value\SassString;
+use ScssPhp\ScssPhp\Value\Value;
+
+/**
+ * @internal
+ */
+final class StringFunctions
+{
+ private static ?int $previousId = null;
+
+ /**
+ * @param list<Value> $arguments
+ */
+ public static function unquote(array $arguments): Value
+ {
+ $string = $arguments[0]->assertString('string');
+
+ if (!$string->hasQuotes()) {
+ return $string;
+ }
+
+ return new SassString($string->getText(), false);
+ }
+
+ /**
+ * @param list<Value> $arguments
+ */
+ public static function quote(array $arguments): Value
+ {
+ $string = $arguments[0]->assertString('string');
+
+ if ($string->hasQuotes()) {
+ return $string;
+ }
+
+ return new SassString($string->getText(), true);
+ }
+
+ /**
+ * @param list<Value> $arguments
+ */
+ public static function length(array $arguments): Value
+ {
+ $string = $arguments[0]->assertString('string');
+
+ return SassNumber::create($string->getSassLength());
+ }
+
+ /**
+ * @param list<Value> $arguments
+ */
+ public static function insert(array $arguments): Value
+ {
+ $string = $arguments[0]->assertString('string');
+ $insert = $arguments[1]->assertString('insert');
+ $index = $arguments[2]->assertNumber('index');
+ $index->assertNoUnits('index');
+
+ $indexInt = $index->assertInt('index');
+
+ // str-insert has unusual behavior for negative inputs. It guarantees that
+ // the `$insert` string is at `$index` in the result, which means that we
+ // want to insert before `$index` if it's positive and after if it's
+ // negative.
+ if ($indexInt < 0) {
+ // +1 because negative indexes start counting from -1 rather than 0, and
+ // another +1 because we want to insert *after* that index.
+ $indexInt = max($string->getSassLength() + $indexInt + 2, 0);
+ }
+
+ $codepointIndex = self::codepointForIndex($indexInt, $string->getSassLength());
+
+ return new SassString(
+ mb_substr($string->getText(), 0, $codepointIndex) . $insert->getText() . mb_substr($string->getText(), $codepointIndex),
+ $string->hasQuotes()
+ );
+ }
+
+ /**
+ * @param list<Value> $arguments
+ */
+ public static function index(array $arguments): Value
+ {
+ $string = $arguments[0]->assertString('string');
+ $substring = $arguments[1]->assertString('substring');
+
+ $codepointIndex = mb_strpos($string->getText(), $substring->getText());
+
+ if ($codepointIndex === false) {
+ return SassNull::create();
+ }
+
+ return SassNumber::create($codepointIndex + 1);
+ }
+
+ /**
+ * @param list<Value> $arguments
+ */
+ public static function slice(array $arguments): Value
+ {
+ $string = $arguments[0]->assertString('string');
+ $start = $arguments[1]->assertNumber('start-at');
+ $end = $arguments[2]->assertNumber('end-at');
+ $start->assertNoUnits('start-at');
+ $end->assertNoUnits('end-at');
+
+ $lengthInCodepoints = $string->getSassLength();
+
+ // No matter what the start index is, an end index of 0 will produce an
+ // empty string.
+ $endInt = $end->assertInt('end-at');
+ if ($endInt === 0) {
+ return new SassString('', $string->hasQuotes());
+ }
+
+ $startCodepoint = self::codepointForIndex($start->assertInt('start-at'), $lengthInCodepoints);
+ $endCodepoint = self::codepointForIndex($endInt, $lengthInCodepoints, true);
+
+ if ($endCodepoint === $lengthInCodepoints) {
+ $endCodepoint--;
+ }
+
+ if ($endCodepoint < $startCodepoint) {
+ return new SassString('', $string->hasQuotes());
+ }
+
+ return new SassString(
+ mb_substr($string->getText(), $startCodepoint, $endCodepoint + 1 - $startCodepoint),
+ $string->hasQuotes()
+ );
+ }
+
+ /**
+ * @param list<Value> $arguments
+ */
+ public static function toUpperCase(array $arguments): Value
+ {
+ $string = $arguments[0]->assertString('string');
+
+ return new SassString(StringUtil::toAsciiUpperCase($string->getText()), $string->hasQuotes());
+ }
+
+ /**
+ * @param list<Value> $arguments
+ */
+ public static function toLowerCase(array $arguments): Value
+ {
+ $string = $arguments[0]->assertString('string');
+
+ return new SassString(StringUtil::toAsciiLowerCase($string->getText()), $string->hasQuotes());
+ }
+
+ /**
+ * @param list<Value> $arguments
+ */
+ public static function uniqueId(array $arguments): Value
+ {
+ if (self::$previousId === null) {
+ self::$previousId = random_int(0, 36 ** 6);
+ }
+
+ // Make it difficult to guess the next ID by randomizing the increase.
+ self::$previousId += random_int(0, 36) + 1;
+
+ if (self::$previousId > 36 ** 6) {
+ self::$previousId %= 36 ** 6;
+ }
+
+ // The leading "u" ensures that the result is a valid identifier.
+ return new SassString('u' . str_pad(base_convert((string) self::$previousId, 10, 36), 6, '0', STR_PAD_LEFT), false);
+ }
+
+ /**
+ * Converts a Sass string index into a codepoint index into a string which
+ * has length $lengthInCodepoints measured in codepoints (with `mb_strlen`).
+ *
+ * A Sass string index is one-based, and uses negative numbers to count
+ * backwards from the end of the string.
+ *
+ * If $index is negative and it points before the beginning of
+ * $lengthInCodepoints, this will return `0` if $allowNegative is `false` and
+ * the index if it's `true`.
+ */
+ private static function codepointForIndex(int $index, int $lengthInCodepoints, bool $allowNegative = false): int
+ {
+ if ($index === 0) {
+ return 0;
+ }
+
+ if ($index > 0) {
+ return min($index - 1, $lengthInCodepoints);
+ }
+
+ $result = $lengthInCodepoints + $index;
+
+ if ($result < 0 && !$allowNegative) {
+ return 0;
+ }
+
+ return $result;
+ }
+}
diff --git a/vendor/scssphp/scssphp/src/Importer/CanonicalizeContext.php b/vendor/scssphp/scssphp/src/Importer/CanonicalizeContext.php
new file mode 100644
index 000000000..f14a9970d
--- /dev/null
+++ b/vendor/scssphp/scssphp/src/Importer/CanonicalizeContext.php
@@ -0,0 +1,76 @@
+<?php
+
+/**
+ * SCSSPHP
+ *
+ * @copyright 2012-2020 Leaf Corcoran
+ *
+ * @license http://opensource.org/licenses/MIT MIT
+ *
+ * @link http://scssphp.github.io/scssphp
+ */
+
+namespace ScssPhp\ScssPhp\Importer;
+
+use League\Uri\Contracts\UriInterface;
+
+/**
+ * @internal
+ */
+final class CanonicalizeContext
+{
+ private readonly ?UriInterface $containingUrl;
+ private bool $fromImport;
+ private bool $containingUrlAccessed = false;
+
+ public function __construct(?UriInterface $containingUrl, bool $fromImport)
+ {
+ $this->containingUrl = $containingUrl;
+ $this->fromImport = $fromImport;
+ }
+
+ /**
+ * Whether the Sass compiler is currently evaluating an `@import` rule.
+ */
+ public function isFromImport(): bool
+ {
+ return $this->fromImport;
+ }
+
+ /**
+ * @template T
+ *
+ * @param callable(): T $callback
+ * @return T
+ *
+ * @param-immediately-invoked-callable $callback
+ */
+ public function withFromImport(bool $fromImport, callable $callback)
+ {
+ $oldFromImport = $this->fromImport;
+ $this->fromImport = $fromImport;
+
+ try {
+ return $callback();
+ } finally {
+ $this->fromImport = $oldFromImport;
+ }
+ }
+
+ public function getContainingUrl(): ?UriInterface
+ {
+ $this->containingUrlAccessed = true;
+
+ return $this->containingUrl;
+ }
+
+ /**
+ * Whether {@see getContainingUrl} has been accessed.
+ *
+ * This is used to determine whether canonicalize result is cacheable.
+ */
+ public function wasContainingUrlAccessed(): bool
+ {
+ return $this->containingUrlAccessed;
+ }
+}
diff --git a/vendor/scssphp/scssphp/src/Importer/CanonicalizeResult.php b/vendor/scssphp/scssphp/src/Importer/CanonicalizeResult.php
new file mode 100644
index 000000000..66c2000aa
--- /dev/null
+++ b/vendor/scssphp/scssphp/src/Importer/CanonicalizeResult.php
@@ -0,0 +1,28 @@
+<?php
+
+/**
+ * SCSSPHP
+ *
+ * @copyright 2012-2020 Leaf Corcoran
+ *
+ * @license http://opensource.org/licenses/MIT MIT
+ *
+ * @link http://scssphp.github.io/scssphp
+ */
+
+namespace ScssPhp\ScssPhp\Importer;
+
+use League\Uri\Contracts\UriInterface;
+
+/**
+ * @internal
+ */
+final class CanonicalizeResult
+{
+ public function __construct(
+ public readonly Importer $importer,
+ public readonly UriInterface $canonicalUrl,
+ public readonly UriInterface $originalUrl,
+ ) {
+ }
+}
diff --git a/vendor/scssphp/scssphp/src/Importer/FilesystemImporter.php b/vendor/scssphp/scssphp/src/Importer/FilesystemImporter.php
new file mode 100644
index 000000000..8515eb312
--- /dev/null
+++ b/vendor/scssphp/scssphp/src/Importer/FilesystemImporter.php
@@ -0,0 +1,92 @@
+<?php
+
+/**
+ * SCSSPHP
+ *
+ * @copyright 2012-2020 Leaf Corcoran
+ *
+ * @license http://opensource.org/licenses/MIT MIT
+ *
+ * @link http://scssphp.github.io/scssphp
+ */
+
+namespace ScssPhp\ScssPhp\Importer;
+
+use League\Uri\Contracts\UriInterface;
+use ScssPhp\ScssPhp\Syntax;
+use ScssPhp\ScssPhp\Util\Path;
+
+/**
+ * An importer that loads files from a load path on the filesystem.
+ */
+final class FilesystemImporter extends Importer
+{
+ /**
+ * The path relative to which this importer looks for files.
+ *
+ * If this is `null`, this importer will _only_ load absolute `file:` URLs
+ * and URLs relative to the current file.
+ */
+ private readonly ?string $loadPath;
+
+ public function __construct(?string $loadPath)
+ {
+ $this->loadPath = $loadPath !== null ? Path::absolute($loadPath) : null;
+ }
+
+ public function canonicalize(UriInterface $url): ?UriInterface
+ {
+ if ($url->getScheme() === 'file') {
+ $resolved = ImportUtil::resolveImportPath(Path::fromUri($url));
+ } elseif ($url->getScheme() !== null) {
+ return null;
+ } elseif ($this->loadPath !== null) {
+ $resolved = ImportUtil::resolveImportPath(Path::join($this->loadPath, Path::fromUri($url)));
+ } else {
+ return null;
+ }
+
+ if ($resolved === null) {
+ return null;
+ }
+
+ return Path::toUri(Path::canonicalize($resolved));
+ }
+
+ public function load(UriInterface $url): ?ImporterResult
+ {
+ $path = Path::fromUri($url);
+ $content = file_get_contents($path);
+
+ if ($content === false) {
+ throw new \Exception("Could not read file $path");
+ }
+
+ return new ImporterResult($content, Syntax::forPath($path), $url);
+ }
+
+ public function couldCanonicalize(UriInterface $url, UriInterface $canonicalUrl): bool
+ {
+ if ($url->getScheme() !== 'file' && $url->getScheme() !== null) {
+ return false;
+ }
+
+ if ($canonicalUrl->getScheme() !== 'file') {
+ return false;
+ }
+
+ $basename = basename((string) $url);
+ $canonicalBasename = basename((string) $canonicalUrl);
+
+ if (!str_starts_with($basename, '_') && str_starts_with($canonicalBasename, '_')) {
+ $canonicalBasename = substr($canonicalBasename, 1);
+ }
+
+ return $basename === $canonicalBasename || $basename === Path::withoutExtension($canonicalBasename);
+ }
+
+ public function __toString(): string
+ {
+ return $this->loadPath ?? '<absolute file importer>';
+ }
+}
diff --git a/vendor/scssphp/scssphp/src/Importer/ImportCache.php b/vendor/scssphp/scssphp/src/Importer/ImportCache.php
new file mode 100644
index 000000000..d8174a484
--- /dev/null
+++ b/vendor/scssphp/scssphp/src/Importer/ImportCache.php
@@ -0,0 +1,297 @@
+<?php
+
+/**
+ * SCSSPHP
+ *
+ * @copyright 2012-2020 Leaf Corcoran
+ *
+ * @license http://opensource.org/licenses/MIT MIT
+ *
+ * @link http://scssphp.github.io/scssphp
+ */
+
+namespace ScssPhp\ScssPhp\Importer;
+
+use League\Uri\Contracts\UriInterface;
+use ScssPhp\ScssPhp\Ast\Sass\Statement\Stylesheet;
+use ScssPhp\ScssPhp\Logger\LoggerInterface;
+use ScssPhp\ScssPhp\Logger\QuietLogger;
+use ScssPhp\ScssPhp\Util\UriUtil;
+
+/**
+ * An in-memory cache of parsed stylesheets that have been imported by Sass.
+ *
+ * @internal
+ */
+final class ImportCache
+{
+ /**
+ * @var list<Importer>
+ */
+ private readonly array $importers;
+
+ private readonly LoggerInterface $logger;
+
+ /**
+ * The canonicalized URLs for each non-canonical URL.
+ *
+ * The `forImport` in each key is true when this canonicalization is for an
+ * `@import` rule. Otherwise, it's for a `@use` or `@forward` rule.
+ *
+ * This cache covers loads that go through the entire chain of {@see $importers},
+ * but it doesn't cover individual loads or loads in which any importer
+ * accesses `containingUrl`. See also {@see $perImporterCanonicalizeCache}.
+ *
+ * @var array<string, array<0|1, CanonicalizeResult|SpecialCacheValue>>
+ */
+ private array $canonicalizeCache = [];
+
+ /**
+ * Like {@see $canonicalizeCache} but also includes the specific importer in the
+ * key.
+ *
+ * This is used to cache both relative imports from the base importer and
+ * individual importer results in the case where some other component of the
+ * importer chain isn't cacheable.
+ *
+ * @var \SplObjectStorage<Importer, array<string, array<0|1, CanonicalizeResult|SpecialCacheValue>>>
+ */
+ private \SplObjectStorage $perImporterCanonicalizeCache;
+
+ /**
+ * The parsed stylesheets for each canonicalized import URL.
+ *
+ * @var array<string, Stylesheet|SpecialCacheValue>
+ */
+ private array $importCache = [];
+
+ /**
+ * The import results for each canonicalized import URL.
+ *
+ * @var array<string, ImporterResult>
+ */
+ private array $resultsCache = [];
+
+ /**
+ * @param list<Importer> $importers
+ */
+ public function __construct(array $importers, LoggerInterface $logger)
+ {
+ $this->importers = $importers;
+ $this->logger = $logger;
+ $this->perImporterCanonicalizeCache = new \SplObjectStorage();
+ }
+
+ public function canonicalize(UriInterface $url, ?Importer $baseImporter = null, ?UriInterface $baseUrl = null, bool $forImport = false): ?CanonicalizeResult
+ {
+ $urlCacheKey = (string) $url;
+ $forImportCacheKey = (int) $forImport;
+
+ if ($baseImporter !== null && $url->getScheme() === null) {
+ $resolvedUrl = self::resolveUri($baseUrl, $url);
+ $resolvedUrlCacheKey = (string) $resolvedUrl;
+
+ if (!isset($this->perImporterCanonicalizeCache[$baseImporter][$resolvedUrlCacheKey][$forImportCacheKey])) {
+ [$result, $cacheable] = $this->doCanonicalize($baseImporter, $resolvedUrl, $baseUrl, $forImport);
+ \assert($cacheable, 'Relative loads should always be cacheable because they never provide access to the containing URL.');
+
+ $importerCache = $this->perImporterCanonicalizeCache[$baseImporter] ?? [];
+ $importerCache[$resolvedUrlCacheKey][$forImportCacheKey] = $result ?? SpecialCacheValue::null;
+ $this->perImporterCanonicalizeCache[$baseImporter] = $importerCache;
+ }
+
+ $relativeResult = $this->perImporterCanonicalizeCache[$baseImporter][$resolvedUrlCacheKey][$forImportCacheKey];
+
+ if ($relativeResult !== SpecialCacheValue::null) {
+ return $relativeResult;
+ }
+ }
+
+ if (isset($this->canonicalizeCache[$urlCacheKey][$forImportCacheKey])) {
+ $cacheResult = $this->canonicalizeCache[$urlCacheKey][$forImportCacheKey];
+
+ if ($cacheResult !== SpecialCacheValue::null) {
+ return $cacheResult;
+ }
+
+ return null;
+ }
+
+ // Each individual call to a `canonicalize()` override may not be cacheable
+ // (specifically, if it has access to `containingUrl` it's too
+ // context-sensitive to usefully cache). We want to cache a given URL across
+ // the _entire_ importer chain, so we use $cacheable to track whether _all_
+ // `canonicalize()` calls we've attempted are cacheable. Only if they are, do
+ // we store the result in the cache.
+ $cacheable = true;
+ foreach ($this->importers as $i => $importer) {
+ if (isset($this->perImporterCanonicalizeCache[$importer][$urlCacheKey][$forImportCacheKey])) {
+ $result = $this->perImporterCanonicalizeCache[$importer][$urlCacheKey][$forImportCacheKey];
+
+ if ($result !== SpecialCacheValue::null) {
+ return $result;
+ }
+
+ continue;
+ }
+
+ [$result, $importerCacheable] = $this->doCanonicalize($importer, $url, $baseUrl, $forImport);
+
+ if ($result !== null && $importerCacheable && $cacheable) {
+ $this->canonicalizeCache[$urlCacheKey][$forImportCacheKey] = $result;
+
+ return $result;
+ }
+ if ($importerCacheable && !$cacheable) {
+ $importerCache = $this->perImporterCanonicalizeCache[$importer] ?? [];
+ $importerCache[$urlCacheKey][$forImportCacheKey] = $result ?? SpecialCacheValue::null;
+ $this->perImporterCanonicalizeCache[$importer] = $importerCache;
+
+ if ($result !== null) {
+ return $result;
+ }
+ }
+ if (!$importerCacheable) {
+ if ($cacheable) {
+ // If this is the first uncacheable result, add all previous results
+ // to the per-importer cache so we don't have to re-run them for
+ // future uses of this importer.
+ for ($j = 0; $j < $i; ++$j) {
+ $importerCache = $this->perImporterCanonicalizeCache[$this->importers[$j]] ?? [];
+ $importerCache[$urlCacheKey][$forImportCacheKey] = SpecialCacheValue::null;
+ $this->perImporterCanonicalizeCache[$this->importers[$j]] = $importerCache;
+ }
+ $cacheable = false;
+ }
+
+ if ($result !== null) {
+ return $result;
+ }
+ }
+ }
+
+ if ($cacheable) {
+ $this->canonicalizeCache[$urlCacheKey][$forImportCacheKey] = SpecialCacheValue::null;
+ }
+
+ return null;
+ }
+
+ private static function resolveUri(?UriInterface $baseUrl, UriInterface $url): UriInterface
+ {
+ if ($baseUrl === null) {
+ return $url;
+ }
+
+ return UriUtil::resolveUri($baseUrl, $url);
+ }
+
+ /**
+ * Calls {@see Importer::canonicalize} and prints a deprecation warning if it
+ * returns a relative URL.
+ *
+ * This returns both the result of the call to `canonicalize()` and whether
+ * that result is cacheable at all.
+ *
+ * @return array{CanonicalizeResult|null, bool}
+ */
+ private function doCanonicalize(Importer $importer, UriInterface $url, ?UriInterface $baseUrl, bool $forImport): array
+ {
+ $passContainingUrl = $baseUrl !== null && ($url->getScheme() === null || $importer->isNonCanonicalScheme($url->getScheme()));
+ $canonicalizeContext = new CanonicalizeContext($passContainingUrl ? $baseUrl : null, $forImport);
+
+ $result = ImportContext::withCanonicalizeContext($canonicalizeContext, fn () => $importer->canonicalize($url));
+
+ $cacheable = !$passContainingUrl || !$canonicalizeContext->wasContainingUrlAccessed();
+
+ if ($result === null) {
+ return [null, $cacheable];
+ }
+
+ if ($result->getScheme() === null) {
+ // dart-sass triggers a deprecation here. As we never supported the old behavior, we forbid it directly.
+ throw new \UnexpectedValueException("Importer $importer canonicalized $url to $result but canonical URLs must be absolute.");
+ }
+
+ if ($importer->isNonCanonicalScheme($result->getScheme())) {
+ throw new \UnexpectedValueException("Importer $importer canonicalized $url to $result, which uses a scheme declared as non-canonical.");
+ }
+
+ return [new CanonicalizeResult($importer, $result, $url), $cacheable];
+ }
+
+ /**
+ * Tries to load the canonicalized $canonicalUrl using $importer.
+ *
+ * If $importer can import $canonicalUrl, returns the imported {@see Stylesheet}.
+ * Otherwise returns `null`.
+ *
+ * If passed, the $originalUrl represents the URL that was canonicalized
+ * into $canonicalUrl. It's used to resolve a relative canonical URL, which
+ * importers may return for legacy reasons.
+ *
+ * If $quiet is `true`, this will disable logging warnings when parsing the
+ * newly imported stylesheet.
+ *
+ * Caches the result of the import and uses cached results if possible.
+ */
+ public function importCanonical(Importer $importer, UriInterface $canonicalUrl, ?UriInterface $originalUrl = null, bool $quiet = false): ?Stylesheet
+ {
+ $result = $this->importCache[(string) $canonicalUrl] ??= $this->doImportCanonical($importer, $canonicalUrl, $originalUrl, $quiet) ?? SpecialCacheValue::null;
+
+ if ($result !== SpecialCacheValue::null) {
+ return $result;
+ }
+
+ return null;
+ }
+
+ private function doImportCanonical(Importer $importer, UriInterface $canonicalUrl, ?UriInterface $originalUrl = null, bool $quiet = false): ?Stylesheet
+ {
+ $result = $importer->load($canonicalUrl);
+
+ if ($result === null) {
+ return null;
+ }
+
+ $this->resultsCache[(string) $canonicalUrl] = $result;
+
+ return Stylesheet::parse($result->getContents(), $result->getSyntax(), $quiet ? new QuietLogger() : $this->logger, self::resolveUri($originalUrl, $canonicalUrl));
+ }
+
+ public function humanize(UriInterface $canonicalUrl): UriInterface
+ {
+ $shortestUrl = null;
+ $shortestLength = \PHP_INT_MAX;
+
+ foreach ($this->canonicalizeCache as $cacheValues) {
+ foreach ($cacheValues as $cacheValue) {
+ if ($cacheValue === SpecialCacheValue::null) {
+ continue;
+ }
+
+ if ($cacheValue->canonicalUrl->toString() !== $canonicalUrl->toString()) {
+ continue;
+ }
+
+ $originalUrlLength = \strlen($cacheValue->originalUrl->getPath());
+
+ if ($shortestUrl === null || $originalUrlLength < $shortestLength) {
+ $shortestUrl = $cacheValue->originalUrl;
+ $shortestLength = $originalUrlLength;
+ }
+ }
+ }
+
+ if ($shortestUrl !== null) {
+ return UriUtil::resolve($shortestUrl, basename($canonicalUrl->getPath()));
+ }
+
+ return $canonicalUrl;
+ }
+
+ public function sourceMapUrl(UriInterface $canonicalUrl): UriInterface
+ {
+ return ($this->resultsCache[(string) $canonicalUrl] ?? null)?->getSourceMapUrl() ?? $canonicalUrl;
+ }
+}
diff --git a/vendor/scssphp/scssphp/src/Importer/ImportContext.php b/vendor/scssphp/scssphp/src/Importer/ImportContext.php
new file mode 100644
index 000000000..1df42a1f3
--- /dev/null
+++ b/vendor/scssphp/scssphp/src/Importer/ImportContext.php
@@ -0,0 +1,84 @@
+<?php
+
+/**
+ * SCSSPHP
+ *
+ * @copyright 2012-2020 Leaf Corcoran
+ *
+ * @license http://opensource.org/licenses/MIT MIT
+ *
+ * @link http://scssphp.github.io/scssphp
+ */
+
+namespace ScssPhp\ScssPhp\Importer;
+
+/**
+ * @internal
+ */
+final class ImportContext
+{
+ private static ?CanonicalizeContext $context = null;
+
+ /**
+ * Whether the Sass compiler is currently evaluating an `@import` rule.
+ *
+ * When evaluating `@import` rules, URLs should canonicalize to an import-only
+ * file if one exists for the URL being canonicalized. Otherwise,
+ * canonicalization should be identical for `@import` and `@use` rules. It's
+ * admittedly hacky to set this globally, but `@import` will eventually be
+ * removed, at which point we can delete this and have one consistent behavior.
+ */
+ public static function isFromImport(): bool
+ {
+ return self::$context?->isFromImport() ?? false;
+ }
+
+ /**
+ * @template T
+ *
+ * @param callable(): T $callback
+ * @return T
+ *
+ * @param-immediately-invoked-callable $callback
+ */
+ public static function inImportRule(callable $callback)
+ {
+ if (self::$context !== null) {
+ return self::$context->withFromImport(true, $callback);
+ }
+
+ return self::withCanonicalizeContext(new CanonicalizeContext(null, true), $callback);
+ }
+
+ public static function getCanonicalizeContext(): CanonicalizeContext
+ {
+ if (self::$context === null) {
+ throw new \LogicException('canonicalizeContext may only be accessed within a call to canonicalize().');
+ }
+
+ return self::$context;
+ }
+
+ /**
+ * Runs $callback in the given context.
+ *
+ * @template T
+ *
+ * @param callable(): T $callback
+ * @return T
+ *
+ * @param-immediately-invoked-callable $callback
+ */
+ public static function withCanonicalizeContext(?CanonicalizeContext $canonicalizeContext, callable $callback)
+ {
+ $oldCanonicalizeContext = self::$context;
+
+ self::$context = $canonicalizeContext;
+
+ try {
+ return $callback();
+ } finally {
+ self::$context = $oldCanonicalizeContext;
+ }
+ }
+}
diff --git a/vendor/scssphp/scssphp/src/Importer/ImportUtil.php b/vendor/scssphp/scssphp/src/Importer/ImportUtil.php
new file mode 100644
index 000000000..233256bad
--- /dev/null
+++ b/vendor/scssphp/scssphp/src/Importer/ImportUtil.php
@@ -0,0 +1,141 @@
+<?php
+
+/**
+ * SCSSPHP
+ *
+ * @copyright 2012-2020 Leaf Corcoran
+ *
+ * @license http://opensource.org/licenses/MIT MIT
+ *
+ * @link http://scssphp.github.io/scssphp
+ */
+
+namespace ScssPhp\ScssPhp\Importer;
+
+use ScssPhp\ScssPhp\Util\Path;
+
+/**
+ * @internal
+ */
+final class ImportUtil
+{
+ /**
+ * Resolves an imported path using the same logic as the filesystem importer.
+ *
+ * This tries to fill in extensions and partial prefixes and check for a
+ * directory default. If no file can be found, it returns `null`.
+ */
+ public static function resolveImportPath(string $path): ?string
+ {
+ $extension = Path::extension($path);
+
+ if ($extension === '.sass' || $extension === '.scss' || $extension === '.css') {
+ return self::ifInImport(fn () => self::exactlyOne(self::tryPath(Path::withoutExtension($path) . '.import' . $extension)))
+ ?? self::exactlyOne(self::tryPath($path));
+ }
+
+ return self::ifInImport(fn () => self::exactlyOne(self::tryPathWithExtensions($path . '.import')))
+ ?? self::exactlyOne(self::tryPathWithExtensions($path))
+ ?? self::tryPathAsDirectory($path);
+ }
+
+ /**
+ * Like {@see tryPath}, but checks `.sass`, `.scss`, and `.css` extensions.
+ *
+ * @return list<string>
+ */
+ private static function tryPathWithExtensions(string $path): array
+ {
+ $result = array_merge(
+ self::tryPath($path . '.sass'),
+ self::tryPath($path . '.scss'),
+ );
+
+ if ($result !== []) {
+ return $result;
+ }
+
+ return self::tryPath($path . '.css');
+ }
+
+ /**
+ * Returns the $path and/or the partial with the same name, if either or both
+ * exists.
+ *
+ * If neither exists, returns an empty list.
+ *
+ * @return list<string>
+ */
+ private static function tryPath(string $path): array
+ {
+ $partial = Path::join(dirname($path), '_' . basename($path));
+ $candidates = [];
+
+ if (is_file($partial)) {
+ $candidates[] = $partial;
+ }
+
+ if (is_file($path)) {
+ $candidates[] = $path;
+ }
+
+ return $candidates;
+ }
+
+ /**
+ * Returns the resolved index file for $path if $path is a directory and the
+ * index file exists.
+ *
+ * Otherwise, returns `null`.
+ */
+ private static function tryPathAsDirectory(string $path): ?string
+ {
+ if (!is_dir($path)) {
+ return null;
+ }
+
+ return self::ifInImport(fn () => self::exactlyOne(self::tryPathWithExtensions(Path::join($path, 'index.import'))))
+ ?? self::exactlyOne(self::tryPathWithExtensions(Path::join($path, 'index')));
+ }
+
+ /**
+ * @param list<string> $paths
+ */
+ private static function exactlyOne(array $paths): ?string
+ {
+ if (\count($paths) === 0) {
+ return null;
+ }
+
+ if (\count($paths) === 1) {
+ return $paths[0];
+ }
+
+ $formattedPrettyPaths = [];
+
+ foreach ($paths as $path) {
+ $formattedPrettyPaths[] = ' ' . Path::prettyUri($path);
+ }
+
+ throw new \Exception("It's not clear which file to import. Found:\n" . implode("\n", $formattedPrettyPaths));
+ }
+
+ /**
+ * If {@see ImportContext::isFromImport} is `true`, invokes callback and returns the result.
+ *
+ * Otherwise, returns `null`.
+ *
+ * @template T
+ *
+ * @param callable(): T $callback
+ * @return T|null
+ */
+ private static function ifInImport(callable $callback)
+ {
+ if (ImportContext::isFromImport()) {
+ return $callback();
+ }
+
+ return null;
+ }
+}
diff --git a/vendor/scssphp/scssphp/src/Importer/Importer.php b/vendor/scssphp/scssphp/src/Importer/Importer.php
new file mode 100644
index 000000000..e6c46ce76
--- /dev/null
+++ b/vendor/scssphp/scssphp/src/Importer/Importer.php
@@ -0,0 +1,170 @@
+<?php
+
+/**
+ * SCSSPHP
+ *
+ * @copyright 2012-2020 Leaf Corcoran
+ *
+ * @license http://opensource.org/licenses/MIT MIT
+ *
+ * @link http://scssphp.github.io/scssphp
+ */
+
+namespace ScssPhp\ScssPhp\Importer;
+
+use League\Uri\Contracts\UriInterface;
+
+/**
+ * A base class for importers that resolves URLs in `@import`s to the contents
+ * of Sass files.
+ *
+ * Importers should implement {@see __toString} to provide a human-readable description
+ * of the importer. For example, the default filesystem importer returns its
+ * load path.
+ */
+abstract class Importer implements \Stringable
+{
+ /**
+ * If $url is recognized by this importer, returns its canonical format.
+ *
+ * Note that canonical URLs *must* be absolute, including a scheme. Returning
+ * `file:` URLs is encouraged if the imported stylesheet comes from a file on
+ * disk.
+ *
+ * If Sass has already loaded a stylesheet with the returned canonical URL,
+ * it re-uses the existing parse tree. This means that importers **must
+ * ensure** that the same canonical URL always refers to the same stylesheet,
+ * *even across different importers*.
+ *
+ * This may return `null` if $url isn't recognized by this importer.
+ *
+ * If this importer's URL format supports file extensions, it should
+ * canonicalize them the same way as the default filesystem importer:
+ *
+ * * The importer should look for stylesheets by adding the prefix `_` to the
+ * URL's basename, and by adding the extensions `.sass` and `.scss` if the
+ * URL doesn't already have one of those extensions. For example, if the
+ * URL was `foo/bar/baz`, the importer would look for:
+ * * `foo/bar/baz.sass`
+ * * `foo/bar/baz.scss`
+ * * `foo/bar/_baz.sass`
+ * * `foo/bar/_baz.scss`
+ *
+ * If the URL was `foo/bar/baz.scss`, the importer would just look for:
+ * * `foo/bar/baz.scss`
+ * * `foo/bar/_baz.scss`
+ *
+ * If the importer finds a stylesheet at more than one of these URLs, it
+ * should throw an exception indicating that the import is ambiguous. Note
+ * that if the extension is explicitly specified, a stylesheet with the
+ * opposite extension may exist.
+ *
+ * * If none of the possible paths is valid, the importer should perform the
+ * same resolution on the URL followed by `/index`. In the example above,
+ * it would look for:
+ * * `foo/bar/baz/_index.sass`
+ * * `foo/bar/baz/index.sass`
+ * * `foo/bar/baz/_index.scss`
+ * * `foo/bar/baz/index.scss`
+ *
+ * As above, if the importer finds a stylesheet at more than one of these
+ * URLs, it should throw an exception indicating that the import is
+ * ambiguous.
+ *
+ * If no stylesheets are found, the importer should return `null`.
+ *
+ * Calling {@see canonicalize} multiple times with the same URL must return the
+ * same result. Calling {@see canonicalize} with a URL returned by {@see canonicalize}
+ * must return that URL. Calling {@see canonicalize} with a URL relative to one
+ * returned by {@see canonicalize} must return a meaningful result.
+ */
+ abstract public function canonicalize(UriInterface $url): ?UriInterface;
+
+ /**
+ * Loads the Sass text for the given $url, or returns `null` if
+ * this importer can't find the stylesheet it refers to.
+ *
+ * The $url comes from a call to {@see canonicalize} for this importer.
+ *
+ * When Sass encounters an `@import` rule in a stylesheet, it first calls
+ * {@see canonicalize} and {@see load} on the importer that first loaded that
+ * stylesheet with the imported URL resolved relative to the stylesheet's
+ * original URL. If either of those returns `null`, it then calls
+ * {@see canonicalize} and {@see load} on each importer in order with the URL as it
+ * appears in the `@import` rule.
+ *
+ * If the importer finds a stylesheet at $url but it fails to load for some
+ * reason, or if $url is uniquely associated with this importer but doesn't
+ * refer to a real stylesheet, the importer may throw an exception that will
+ * be wrapped by Sass.
+ */
+ abstract public function load(UriInterface $url): ?ImporterResult;
+
+ /**
+ * Without accessing the filesystem, returns whether passing $url to
+ * {@see canonicalize} could possibly return $canonicalUrl.
+ *
+ * This is expected to be very efficient, and subclasses are allowed to
+ * return false positives if it would be inefficient to determine whether
+ * $url would actually resolve to $canonicalUrl. Subclasses are not allowed
+ * to return false negatives.
+ */
+ public function couldCanonicalize(UriInterface $url, UriInterface $canonicalUrl): bool
+ {
+ return true;
+ }
+
+ /**
+ * Returns whether the given URL scheme (without `:`) should be considered
+ * "non-canonical" for this importer.
+ *
+ * An importer may not return a URL with a non-canonical scheme from
+ * {@see canonicalize}. In exchange, {@see getContainingUrl} is available within
+ * {@see canonicalize} for absolute URLs with non-canonical schemes so that the
+ * importer can resolve those URLs differently based on where they're loaded.
+ *
+ * This must always return the same value for the same $scheme. It is
+ * expected to be very efficient.
+ */
+ public function isNonCanonicalScheme(string $scheme): bool
+ {
+ return false;
+ }
+
+ /**
+ * Whether the current {@see canonicalize} invocation comes from an `@import`
+ * rule.
+ *
+ * When evaluating `@import` rules, URLs should canonicalize to an
+ * [import-only file] if one exists for the URL being canonicalized.
+ * Otherwise, canonicalization should be identical for `@import` and `@use`
+ * rules.
+ *
+ * [import-only file]: https://sass-lang.com/documentation/at-rules/import#import-only-files
+ *
+ * Subclasses should only access this from within calls to {@see canonicalize}.
+ * Outside of that context, its value is undefined and subject to change.
+ */
+ final protected function isFromImport(): bool
+ {
+ return ImportContext::isFromImport();
+ }
+
+ /**
+ * The canonical URL of the stylesheet that caused the current {@see canonicalize}
+ * invocation.
+ *
+ * This is only set when the containing stylesheet has a canonical URL, and
+ * when the URL being canonicalized is either relative or has a scheme for
+ * which {@see isNonCanonicalScheme} returns `true`. This restriction ensures that
+ * canonical URLs are always interpreted the same way regardless of their
+ * context.
+ *
+ * Subclasses should only access this from within calls to {@see canonicalize}.
+ * Outside of that context, its value is undefined and subject to change.
+ */
+ final protected function getContainingUrl(): ?UriInterface
+ {
+ return ImportContext::getCanonicalizeContext()->getContainingUrl();
+ }
+}
diff --git a/vendor/scssphp/scssphp/src/Importer/ImporterResult.php b/vendor/scssphp/scssphp/src/Importer/ImporterResult.php
new file mode 100644
index 000000000..63ba7f90b
--- /dev/null
+++ b/vendor/scssphp/scssphp/src/Importer/ImporterResult.php
@@ -0,0 +1,59 @@
+<?php
+
+/**
+ * SCSSPHP
+ *
+ * @copyright 2012-2020 Leaf Corcoran
+ *
+ * @license http://opensource.org/licenses/MIT MIT
+ *
+ * @link http://scssphp.github.io/scssphp
+ */
+
+namespace ScssPhp\ScssPhp\Importer;
+
+use League\Uri\Contracts\UriInterface;
+use League\Uri\Uri;
+use ScssPhp\ScssPhp\Syntax;
+
+final class ImporterResult
+{
+ private readonly string $contents;
+
+ private readonly ?UriInterface $sourceMapUrl;
+
+ private readonly Syntax $syntax;
+
+ public function __construct(string $contents, Syntax $syntax, ?UriInterface $sourceMapUrl = null)
+ {
+ $this->contents = $contents;
+ $this->syntax = $syntax;
+ $this->sourceMapUrl = $sourceMapUrl;
+ }
+
+ public function getContents(): string
+ {
+ return $this->contents;
+ }
+
+ /**
+ * An absolute, browser-accessible URL indicating the resolved location of
+ * the imported stylesheet.
+ *
+ * This should be a `file:` URL if one is available, but an `http:` URL is
+ * acceptable as well. If no URL is supplied, a `data:` URL is generated
+ * automatically from {@see contents}.
+ */
+ public function getSourceMapUrl(): UriInterface
+ {
+ return $this->sourceMapUrl ?? Uri::fromData($this->contents, '', 'charset=utf-8');
+ }
+
+ /**
+ * The syntax to use to parse the stylesheet.
+ */
+ public function getSyntax(): Syntax
+ {
+ return $this->syntax;
+ }
+}
diff --git a/vendor/scssphp/scssphp/src/Importer/LegacyCallbackImporter.php b/vendor/scssphp/scssphp/src/Importer/LegacyCallbackImporter.php
new file mode 100644
index 000000000..a7a37c0f8
--- /dev/null
+++ b/vendor/scssphp/scssphp/src/Importer/LegacyCallbackImporter.php
@@ -0,0 +1,66 @@
+<?php
+
+/**
+ * SCSSPHP
+ *
+ * @copyright 2012-2020 Leaf Corcoran
+ *
+ * @license http://opensource.org/licenses/MIT MIT
+ *
+ * @link http://scssphp.github.io/scssphp
+ */
+
+namespace ScssPhp\ScssPhp\Importer;
+
+use League\Uri\Contracts\UriInterface;
+use ScssPhp\ScssPhp\Util\Path;
+
+/**
+ * @internal
+ */
+final class LegacyCallbackImporter extends Importer
+{
+ private readonly \Closure $callback;
+ private readonly Importer $filesystemImporter;
+
+ /**
+ * @param \Closure(string): (string|null) $callback
+ */
+ public function __construct(\Closure $callback)
+ {
+ $this->callback = $callback;
+ $this->filesystemImporter = new FilesystemImporter(null);
+ }
+
+ public function canonicalize(UriInterface $url): ?UriInterface
+ {
+ if ($url->getScheme() === 'file') {
+ return $this->filesystemImporter->canonicalize($url);
+ }
+
+ $result = ($this->callback)((string) $url);
+
+ if ($result === null) {
+ return null;
+ }
+
+ $resultUrl = Path::toUri($result);
+
+ return $this->filesystemImporter->canonicalize($resultUrl);
+ }
+
+ public function load(UriInterface $url): ?ImporterResult
+ {
+ return $this->filesystemImporter->load($url);
+ }
+
+ public function couldCanonicalize(UriInterface $url, UriInterface $canonicalUrl): bool
+ {
+ return $this->filesystemImporter->couldCanonicalize($url, $canonicalUrl);
+ }
+
+ public function __toString(): string
+ {
+ return 'LegacyCallbackImporter';
+ }
+}
diff --git a/vendor/scssphp/scssphp/src/Importer/NoOpImporter.php b/vendor/scssphp/scssphp/src/Importer/NoOpImporter.php
new file mode 100644
index 000000000..826611389
--- /dev/null
+++ b/vendor/scssphp/scssphp/src/Importer/NoOpImporter.php
@@ -0,0 +1,44 @@
+<?php
+
+/**
+ * SCSSPHP
+ *
+ * @copyright 2012-2020 Leaf Corcoran
+ *
+ * @license http://opensource.org/licenses/MIT MIT
+ *
+ * @link http://scssphp.github.io/scssphp
+ */
+
+namespace ScssPhp\ScssPhp\Importer;
+
+use League\Uri\Contracts\UriInterface;
+
+/**
+ * An importer that never imports any stylesheets.
+ *
+ * This is used for stylesheets which don't support relative imports, such as
+ * those created from PHP code with plain strings.
+ */
+final class NoOpImporter extends Importer
+{
+ public function canonicalize(UriInterface $url): ?UriInterface
+ {
+ return null;
+ }
+
+ public function load(UriInterface $url): ?ImporterResult
+ {
+ return null;
+ }
+
+ public function couldCanonicalize(UriInterface $url, UriInterface $canonicalUrl): bool
+ {
+ return false;
+ }
+
+ public function __toString(): string
+ {
+ return '(unknown)';
+ }
+}
diff --git a/vendor/scssphp/scssphp/src/Importer/SpecialCacheValue.php b/vendor/scssphp/scssphp/src/Importer/SpecialCacheValue.php
new file mode 100644
index 000000000..283593c05
--- /dev/null
+++ b/vendor/scssphp/scssphp/src/Importer/SpecialCacheValue.php
@@ -0,0 +1,21 @@
+<?php
+
+/**
+ * SCSSPHP
+ *
+ * @copyright 2012-2020 Leaf Corcoran
+ *
+ * @license http://opensource.org/licenses/MIT MIT
+ *
+ * @link http://scssphp.github.io/scssphp
+ */
+
+namespace ScssPhp\ScssPhp\Importer;
+
+/**
+ * @internal
+ */
+enum SpecialCacheValue
+{
+ case null;
+}
diff --git a/vendor/scssphp/scssphp/src/Logger/DeprecationProcessingLogger.php b/vendor/scssphp/scssphp/src/Logger/DeprecationProcessingLogger.php
new file mode 100644
index 000000000..fb2d23df0
--- /dev/null
+++ b/vendor/scssphp/scssphp/src/Logger/DeprecationProcessingLogger.php
@@ -0,0 +1,195 @@
+<?php
+
+/**
+ * SCSSPHP
+ *
+ * @copyright 2012-2020 Leaf Corcoran
+ *
+ * @license http://opensource.org/licenses/MIT MIT
+ *
+ * @link http://scssphp.github.io/scssphp
+ */
+
+namespace ScssPhp\ScssPhp\Logger;
+
+use ScssPhp\ScssPhp\Deprecation;
+use ScssPhp\ScssPhp\Exception\SassScriptException;
+use ScssPhp\ScssPhp\Exception\SimpleSassException;
+use ScssPhp\ScssPhp\Exception\SimpleSassRuntimeException;
+use ScssPhp\ScssPhp\StackTrace\Trace;
+use SourceSpan\FileSpan;
+use SourceSpan\SourceSpan;
+
+/**
+ * A logger that wraps an inner logger to have special handling for
+ * deprecation warnings, silencing, making fatal, enabling future, and/or
+ * limiting repetition based on its inputs.
+ *
+ * @internal
+ */
+final class DeprecationProcessingLogger implements LoggerInterface
+{
+ private const MAX_REPETITIONS = 5;
+
+ /**
+ * A map of how many times each deprecation has been emitted by this logger.
+ *
+ * @var array<value-of<Deprecation>, int>
+ */
+ private array $warningCounts = [];
+
+ /**
+ * Deprecation warnings of these types will be ignored.
+ *
+ * @var Deprecation[]
+ */
+ private readonly array $silenceDeprecations;
+
+ /**
+ * Deprecation warnings of one of these types will cause an error to be
+ * thrown.
+ *
+ * Future deprecations in this list will still cause an error even if they
+ * are not also in {@see $futureDeprecations}.
+ *
+ * @var Deprecation[]
+ */
+ private readonly array $fatalDeprecations;
+
+ /**
+ * Future deprecations that the user has explicitly opted into.
+ *
+ * @var Deprecation[]
+ */
+ private readonly array $futureDeprecations;
+
+ /**
+ * @param Deprecation[] $silenceDeprecations
+ * @param Deprecation[] $fatalDeprecations
+ * @param Deprecation[] $futureDeprecations
+ */
+ public function __construct(
+ private readonly LoggerInterface $inner,
+ array $silenceDeprecations,
+ array $fatalDeprecations,
+ array $futureDeprecations,
+ private readonly bool $limitRepetition = true,
+ ) {
+ $this->silenceDeprecations = $silenceDeprecations;
+ $this->futureDeprecations = $futureDeprecations;
+ $this->fatalDeprecations = $fatalDeprecations;
+ }
+
+ /**
+ * Warns if any of the deprecations options are incompatible or unnecessary.
+ */
+ public function validate(): void
+ {
+ foreach ($this->fatalDeprecations as $deprecation) {
+ if ($deprecation->isFuture() && !\in_array($deprecation, $this->futureDeprecations, true)) {
+ $this->warn("Future $deprecation->value deprecation must be enabled before it can be made fatal.");
+ } elseif ($deprecation->getObsoleteIn() !== null) {
+ $this->warn("$deprecation->value deprecation is obsolete, so does not need to be made fatal.");
+ } elseif (\in_array($deprecation, $this->silenceDeprecations, true)) {
+ $this->warn("Ignoring setting to silence $deprecation->value deprecation, since it has also been made fatal.");
+ }
+ }
+
+ foreach ($this->silenceDeprecations as $deprecation) {
+ if ($deprecation === Deprecation::userAuthored) {
+ $this->warn('User-authored deprecations should not be silenced.');
+ } elseif ($deprecation->getObsoleteIn() !== null) {
+ $this->warn("$deprecation->value deprecation is obsolete. If you were previously silencing it, your code may now behave in unexpected ways.");
+ } elseif ($deprecation->isFuture() && \in_array($deprecation, $this->futureDeprecations, true)) {
+ $this->warn("Conflicting options for future $deprecation->value deprecation cancel each other out.");
+ } elseif ($deprecation->isFuture()) {
+ $this->warn("Future $deprecation->value deprecation is not yet active, so silencing it is unnecessary.");
+ }
+ }
+
+ foreach ($this->futureDeprecations as $deprecation) {
+ if (!$deprecation->isFuture()) {
+ $this->warn("$deprecation->value is not a future deprecation, so it does not need to be explicitly enabled.");
+ }
+ }
+ }
+
+ public function warn(string $message, ?Deprecation $deprecation = null, ?FileSpan $span = null, ?Trace $trace = null): void
+ {
+ if ($deprecation !== null) {
+ $this->handleDeprecation($deprecation, $message, $span, $trace);
+ } else {
+ $this->inner->warn($message, $deprecation, $span, $trace);
+ }
+ }
+
+ /**
+ * Processes a deprecation warning.
+ *
+ * If $deprecation is in {@see $fatalDeprecations}, this shows an error.
+ *
+ * If it's a future deprecation that hasn't been opted into or it's a
+ * deprecation that's already been warned for {@see self::MAX_REPETITIONS} times and
+ * {@see limitRepetitions} is true, the warning is dropped.
+ *
+ * Otherwise, this is passed on to {@see warn}.
+ */
+ private function handleDeprecation(Deprecation $deprecation, string $message, ?FileSpan $span = null, ?Trace $trace = null): void
+ {
+ if ($deprecation->isFuture() && !\in_array($deprecation, $this->futureDeprecations, true)) {
+ return;
+ }
+
+ if (\in_array($deprecation, $this->fatalDeprecations, true)) {
+ $message .= "\n\nThis is only an error because you've set the {$deprecation->value} deprecation to be fatal.\nRemove this setting if you need to keep using this feature.";
+
+ if ($span !== null && $trace !== null) {
+ throw new SimpleSassRuntimeException($message, $span, $trace);
+ }
+
+ if ($span !== null) {
+ throw new SimpleSassException($message, $span);
+ }
+
+ throw new SassScriptException($message);
+ }
+
+ if (\in_array($deprecation, $this->silenceDeprecations, true)) {
+ return;
+ }
+
+ if ($this->limitRepetition) {
+ $count = $this->warningCounts[$deprecation->value] = ($this->warningCounts[$deprecation->value] ?? 0) + 1;
+
+ if ($count > self::MAX_REPETITIONS) {
+ return;
+ }
+ }
+
+ $this->inner->warn($message, $deprecation, $span, $trace);
+ }
+
+ public function debug(string $message, SourceSpan $span): void
+ {
+ $this->inner->debug($message, $span);
+ }
+
+ /**
+ * Prints a warning indicating the number of deprecation warnings that were
+ * omitted due to repetition.
+ */
+ public function summarize(): void
+ {
+ $total = 0;
+
+ foreach ($this->warningCounts as $count) {
+ if ($count > self::MAX_REPETITIONS) {
+ $total += $count - self::MAX_REPETITIONS;
+ }
+ }
+
+ if ($total > 0) {
+ $this->inner->warn("$total repetitive deprecation warnings omitted.\nRun in verbose mode to see all warnings.");
+ }
+ }
+}
diff --git a/vendor/scssphp/scssphp/src/Logger/LoggerInterface.php b/vendor/scssphp/scssphp/src/Logger/LoggerInterface.php
index 7c0a2f76e..f50d95c84 100644
--- a/vendor/scssphp/scssphp/src/Logger/LoggerInterface.php
+++ b/vendor/scssphp/scssphp/src/Logger/LoggerInterface.php
@@ -12,6 +12,11 @@
namespace ScssPhp\ScssPhp\Logger;
+use ScssPhp\ScssPhp\Deprecation;
+use ScssPhp\ScssPhp\StackTrace\Trace;
+use SourceSpan\FileSpan;
+use SourceSpan\SourceSpan;
+
/**
* Interface implemented by loggers for warnings and debug messages.
*
@@ -26,23 +31,17 @@ interface LoggerInterface
/**
* Emits a warning with the given message.
*
- * If $deprecation is true, it indicates that this is a deprecation
+ * If $span is passed, it's the location in the Sass source that generated
+ * the warning. If $trace is passed, it's the Sass stack trace when the
+ * warning was issued.
+ * If $deprecation is non-null, it indicates that this is a deprecation
* warning. Implementations should surface all this information to
* the end user.
- *
- * @param string $message
- * @param bool $deprecation
- *
- * @return void
*/
- public function warn($message, $deprecation = false);
+ public function warn(string $message, ?Deprecation $deprecation = null, ?FileSpan $span = null, ?Trace $trace = null): void;
/**
- * Emits a debugging message.
- *
- * @param string $message
- *
- * @return void
+ * Emits a debugging message associated with the given span.
*/
- public function debug($message);
+ public function debug(string $message, SourceSpan $span): void;
}
diff --git a/vendor/scssphp/scssphp/src/Logger/QuietLogger.php b/vendor/scssphp/scssphp/src/Logger/QuietLogger.php
index ad7c07537..7cd848057 100644
--- a/vendor/scssphp/scssphp/src/Logger/QuietLogger.php
+++ b/vendor/scssphp/scssphp/src/Logger/QuietLogger.php
@@ -12,18 +12,21 @@
namespace ScssPhp\ScssPhp\Logger;
+use ScssPhp\ScssPhp\Deprecation;
+use ScssPhp\ScssPhp\StackTrace\Trace;
+use SourceSpan\FileSpan;
+use SourceSpan\SourceSpan;
+
/**
* A logger that silently ignores all messages.
- *
- * @final
*/
-class QuietLogger implements LoggerInterface
+final class QuietLogger implements LoggerInterface
{
- public function warn($message, $deprecation = false)
+ public function warn(string $message, ?Deprecation $deprecation = null, ?FileSpan $span = null, ?Trace $trace = null): void
{
}
- public function debug($message)
+ public function debug(string $message, SourceSpan $span): void
{
}
}
diff --git a/vendor/scssphp/scssphp/src/Logger/StreamLogger.php b/vendor/scssphp/scssphp/src/Logger/StreamLogger.php
index 7db7cc189..f11f0f19e 100644
--- a/vendor/scssphp/scssphp/src/Logger/StreamLogger.php
+++ b/vendor/scssphp/scssphp/src/Logger/StreamLogger.php
@@ -12,12 +12,17 @@
namespace ScssPhp\ScssPhp\Logger;
+use ScssPhp\ScssPhp\Deprecation;
+use ScssPhp\ScssPhp\StackTrace\Trace;
+use ScssPhp\ScssPhp\Util;
+use ScssPhp\ScssPhp\Util\Path;
+use SourceSpan\FileSpan;
+use SourceSpan\SourceSpan;
+
/**
* A logger that prints to a PHP stream (for instance stderr)
- *
- * @final
*/
-class StreamLogger implements LoggerInterface
+final class StreamLogger implements LoggerInterface
{
private $stream;
private $closeOnDestruct;
@@ -26,7 +31,7 @@ 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, $closeOnDestruct = false)
+ public function __construct($stream, bool $closeOnDestruct = false)
{
$this->stream = $stream;
$this->closeOnDestruct = $closeOnDestruct;
@@ -42,21 +47,33 @@ class StreamLogger implements LoggerInterface
}
}
- /**
- * @inheritDoc
- */
- public function warn($message, $deprecation = false)
+ public function warn(string $message, ?Deprecation $deprecation = null, ?FileSpan $span = null, ?Trace $trace = null): void
{
- $prefix = ($deprecation ? 'DEPRECATION ' : '') . 'WARNING: ';
+ $prefix = ($deprecation !== null ? 'DEPRECATION ' : '') . 'WARNING';
+
+ 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 . "\n\n" . $span->highlight();
+ } else {
+ $formattedMessage = ' on ' . $span->message("\n" . $message);
+ }
- fwrite($this->stream, $prefix . $message . "\n\n");
+ if ($trace !== null) {
+ $formattedMessage .= "\n" . Util::indent(rtrim($trace->getFormattedTrace()), 4);
+ }
+
+ fwrite($this->stream, $prefix . $formattedMessage . "\n\n");
}
- /**
- * @inheritDoc
- */
- public function debug($message)
+ public function debug(string $message, SourceSpan $span): void
{
- fwrite($this->stream, $message . "\n");
+ $url = $span->getStart()->getSourceUrl() === null ? '-' : Path::prettyUri($span->getStart()->getSourceUrl());
+ $line = $span->getStart()->getLine() + 1;
+ $location = "$url:$line ";
+
+ fwrite($this->stream, \sprintf("%sDEBUG: %s", $location, $message) . "\n");
}
}
diff --git a/vendor/scssphp/scssphp/src/Node/Number.php b/vendor/scssphp/scssphp/src/Node/Number.php
index 6c0445876..a8a30fb5c 100644
--- a/vendor/scssphp/scssphp/src/Node/Number.php
+++ b/vendor/scssphp/scssphp/src/Node/Number.php
@@ -12,13 +12,11 @@
namespace ScssPhp\ScssPhp\Node;
-use ScssPhp\ScssPhp\Base\Range;
use ScssPhp\ScssPhp\Compiler;
-use ScssPhp\ScssPhp\Exception\RangeException;
use ScssPhp\ScssPhp\Exception\SassScriptException;
use ScssPhp\ScssPhp\Node;
use ScssPhp\ScssPhp\Type;
-use ScssPhp\ScssPhp\Util;
+use ScssPhp\ScssPhp\Util\NumberUtil;
/**
* Dimension + optional units
@@ -33,23 +31,17 @@ use ScssPhp\ScssPhp\Util;
*
* @template-implements \ArrayAccess<int, mixed>
*/
-class Number extends Node implements \ArrayAccess, \JsonSerializable
+final 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<string, array<string, float|int>>
*/
- protected static $unitTable = [
+ private static $unitTable = [
'in' => [
'in' => 1,
'pc' => 6,
@@ -112,7 +104,6 @@ class Number extends Node implements \ArrayAccess, \JsonSerializable
if (is_string($numeratorUnits)) {
$numeratorUnits = $numeratorUnits ? [$numeratorUnits] : [];
} elseif (isset($numeratorUnits['numerator_units'], $numeratorUnits['denominator_units'])) {
- // TODO get rid of this once `$number[2]` is not used anymore
$denominatorUnits = $numeratorUnits['denominator_units'];
$numeratorUnits = $numeratorUnits['numerator_units'];
}
@@ -278,16 +269,12 @@ class Number extends Node implements \ArrayAccess, \JsonSerializable
* @param float|int $max
* @param string|null $name
*
- * @return float|int
+ * @return float
* @throws SassScriptException
*/
public function valueInRange($min, $max, $name = null)
{
- try {
- return Util::checkRange('', new Range($min, $max), $this);
- } catch (RangeException $e) {
- throw SassScriptException::forArgument(sprintf('Expected %s to be within %s%s and %s%3$s.', $this, $min, $this->unitStr(), $max), $name);
- }
+ return NumberUtil::fuzzyCheckRange($this->dimension, $min, $max) ?? throw SassScriptException::forArgument(sprintf('Expected %s to be within %s%s and %s%3$s.', $this, $min, $this->unitStr(), $max), $name);
}
/**
@@ -296,18 +283,14 @@ class Number extends Node implements \ArrayAccess, \JsonSerializable
* @param string $name
* @param string $unit
*
- * @return float|int
+ * @return float
* @throws SassScriptException
*
* @internal
*/
public function valueInRangeWithUnit($min, $max, $name, $unit)
{
- try {
- return Util::checkRange('', new Range($min, $max), $this);
- } catch (RangeException $e) {
- throw SassScriptException::forArgument(sprintf('Expected %s to be within %s%s and %s%3$s.', $this, $min, $unit, $max), $name);
- }
+ return NumberUtil::fuzzyCheckRange($this->dimension, $min, $max) ?? throw SassScriptException::forArgument(sprintf('Expected %s to be within %s%s and %s%3$s.', $this, $min, $unit, $max), $name);
}
/**
@@ -578,7 +561,7 @@ class Number extends Node implements \ArrayAccess, \JsonSerializable
*
* @return string
*/
- public function output(Compiler $compiler = null)
+ public function output(?Compiler $compiler = null)
{
$dimension = round($this->dimension, self::PRECISION);
@@ -805,7 +788,7 @@ class Number extends Node implements \ArrayAccess, \JsonSerializable
return 1;
}
- foreach (static::$unitTable as $unitVariants) {
+ foreach (self::$unitTable as $unitVariants) {
if (isset($unitVariants[$unit1]) && isset($unitVariants[$unit2])) {
return $unitVariants[$unit1] / $unitVariants[$unit2];
}
diff --git a/vendor/scssphp/scssphp/src/OutputStyle.php b/vendor/scssphp/scssphp/src/OutputStyle.php
index a1d8b4255..8a9880f28 100644
--- a/vendor/scssphp/scssphp/src/OutputStyle.php
+++ b/vendor/scssphp/scssphp/src/OutputStyle.php
@@ -12,51 +12,37 @@
namespace ScssPhp\ScssPhp;
-final class OutputStyle
+enum OutputStyle: string
{
- const EXPANDED = 'expanded';
- const COMPRESSED = 'compressed';
+ case EXPANDED = 'expanded';
+ case COMPRESSED = 'compressed';
/**
* Converts a string to an output style.
*
* Using this method allows to write code which will support both
- * versions 1.12+ and 2.0 of Scssphp. In 2.0, OutputStyle will be
- * an enum instead of using string constants.
- *
- * @param string $string
- *
- * @return self::*
+ * versions 1.12+ and 2.0 of Scssphp. In 1.x, OutputStyle was using
+ * string constants.
*/
- public static function fromString($string)
+ public static function fromString(string $string): OutputStyle
{
- switch ($string) {
- case 'expanded':
- return self::EXPANDED;
-
- case 'compressed':
- return self::COMPRESSED;
-
- default:
- throw new \InvalidArgumentException('Invalid output style');
- }
+ return match ($string) {
+ 'expanded' => self::EXPANDED,
+ 'compressed' => self::COMPRESSED,
+ default => throw new \InvalidArgumentException('Invalid output style'),
+ };
}
/**
* Converts an output style to a string supported by {@see OutputStyle::fromString()}.
*
* Using this method allows to write code which will support both
- * versions 1.12+ and 2.0 of Scssphp. In 2.0, OutputStyle will be
- * an enum instead of using string constants.
+ * versions 1.12+ and 2.0 of Scssphp.
* The returned string representation is guaranteed to be compatible
* between 1.12 and 2.0.
- *
- * @param self::* $outputStyle
- *
- * @return string
*/
- public static function toString($outputStyle)
+ public static function toString(OutputStyle $outputStyle): string
{
- return $outputStyle;
+ return $outputStyle->value;
}
}
diff --git a/vendor/scssphp/scssphp/src/Parser.php b/vendor/scssphp/scssphp/src/Parser.php
deleted file mode 100644
index 2666a263c..000000000
--- a/vendor/scssphp/scssphp/src/Parser.php
+++ /dev/null
@@ -1,4220 +0,0 @@
-<?php
-
-/**
- * SCSSPHP
- *
- * @copyright 2012-2020 Leaf Corcoran
- *
- * @license http://opensource.org/licenses/MIT MIT
- *
- * @link http://scssphp.github.io/scssphp
- */
-
-namespace ScssPhp\ScssPhp;
-
-use ScssPhp\ScssPhp\Block\AtRootBlock;
-use ScssPhp\ScssPhp\Block\CallableBlock;
-use ScssPhp\ScssPhp\Block\ContentBlock;
-use ScssPhp\ScssPhp\Block\DirectiveBlock;
-use ScssPhp\ScssPhp\Block\EachBlock;
-use ScssPhp\ScssPhp\Block\ElseBlock;
-use ScssPhp\ScssPhp\Block\ElseifBlock;
-use ScssPhp\ScssPhp\Block\ForBlock;
-use ScssPhp\ScssPhp\Block\IfBlock;
-use ScssPhp\ScssPhp\Block\MediaBlock;
-use ScssPhp\ScssPhp\Block\NestedPropertyBlock;
-use ScssPhp\ScssPhp\Block\WhileBlock;
-use ScssPhp\ScssPhp\Exception\ParserException;
-use ScssPhp\ScssPhp\Logger\LoggerInterface;
-use ScssPhp\ScssPhp\Logger\QuietLogger;
-use ScssPhp\ScssPhp\Node\Number;
-
-/**
- * Parser
- *
- * @author Leaf Corcoran <leafot@gmail.com>
- *
- * @internal
- */
-class Parser
-{
- const SOURCE_INDEX = -1;
- const SOURCE_LINE = -2;
- const SOURCE_COLUMN = -3;
-
- /**
- * @var array<string, int>
- */
- protected static $precedence = [
- '=' => 0,
- 'or' => 1,
- 'and' => 2,
- '==' => 3,
- '!=' => 3,
- '<=' => 4,
- '>=' => 4,
- '<' => 4,
- '>' => 4,
- '+' => 5,
- '-' => 5,
- '*' => 6,
- '/' => 6,
- '%' => 6,
- ];
-
- /**
- * @var string
- */
- protected static $commentPattern;
- /**
- * @var string
- */
- protected static $operatorPattern;
- /**
- * @var string
- */
- protected static $whitePattern;
-
- /**
- * @var Cache|null
- */
- protected $cache;
-
- private $sourceName;
- private $sourceIndex;
- /**
- * @var array<int, int>
- */
- private $sourcePositions;
- /**
- * The current offset in the buffer
- *
- * @var int
- */
- private $count;
- /**
- * @var Block|null
- */
- private $env;
- /**
- * @var bool
- */
- private $inParens;
- /**
- * @var bool
- */
- private $eatWhiteDefault;
- /**
- * @var bool
- */
- private $discardComments;
- private $allowVars;
- /**
- * @var string
- */
- private $buffer;
- private $utf8;
- /**
- * @var string|null
- */
- private $encoding;
- private $patternModifiers;
- private $commentsSeen;
-
- private $cssOnly;
-
- /**
- * @var LoggerInterface
- */
- private $logger;
-
- /**
- * Constructor
- *
- * @api
- *
- * @param string|null $sourceName
- * @param int $sourceIndex
- * @param string|null $encoding
- * @param Cache|null $cache
- * @param bool $cssOnly
- * @param LoggerInterface|null $logger
- */
- 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->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(static::$operatorPattern)) {
- static::$operatorPattern = '([*\/%+-]|[!=]\=|\>\=?|\<\=?|and|or)';
-
- $commentSingle = '\/\/';
- $commentMultiLeft = '\/\*';
- $commentMultiRight = '\*\/';
-
- 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;
- }
-
- /**
- * Get source file name
- *
- * @api
- *
- * @return 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($msg = 'parse error')
- {
- list($line, $column) = $this->getSourcePosition($this->count);
-
- $loc = empty($this->sourceName)
- ? "line: $line, column: $column"
- : "$this->sourceName on line $line, at column $column";
-
- if ($this->peek('(.*?)(\n|$)', $m, $this->count)) {
- $this->restoreEncoding();
-
- $e = new ParserException("$msg: failed at `$m[1]` $loc");
- $e->setSourcePosition([$this->sourceName, $line, $column]);
-
- return $e;
- }
-
- $this->restoreEncoding();
-
- $e = new ParserException("$msg: $loc");
- $e->setSourcePosition([$this->sourceName, $line, $column]);
-
- return $e;
- }
-
- /**
- * Parser buffer
- *
- * @api
- *
- * @param string $buffer
- *
- * @return Block
- */
- public function parse($buffer)
- {
- if ($this->cache) {
- $cacheKey = $this->sourceName . ':' . md5($buffer);
- $parseOptions = [
- 'utf8' => $this->utf8,
- ];
- $v = $this->cache->getCache('parse', $cacheKey, $parseOptions);
-
- if (! \is_null($v)) {
- return $v;
- }
- }
-
- // strip BOM (byte order marker)
- if (substr($buffer, 0, 3) === "\xef\xbb\xbf") {
- $buffer = substr($buffer, 3);
- }
-
- $this->buffer = rtrim($buffer, "\x00..\x1f");
- $this->count = 0;
- $this->env = null;
- $this->inParens = false;
- $this->eatWhiteDefault = true;
-
- $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);
- $this->popBlock();
-
- while ($this->parseChunk()) {
- ;
- }
-
- if ($this->count !== \strlen($this->buffer)) {
- throw $this->parseError();
- }
-
- if (! empty($this->env->parent)) {
- throw $this->parseError('unclosed block');
- }
-
- $this->restoreEncoding();
- assert($this->env !== null);
-
- if ($this->cache) {
- $this->cache->setCache('parse', $cacheKey, $this->env, $parseOptions);
- }
-
- return $this->env;
- }
-
- /**
- * Parse a value or value list
- *
- * @api
- *
- * @param string $buffer
- * @param string|array $out
- *
- * @return bool
- */
- public function parseValue($buffer, &$out)
- {
- $this->count = 0;
- $this->env = null;
- $this->inParens = false;
- $this->eatWhiteDefault = true;
- $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;
- }
-
- /**
- * Parse a selector or selector list
- *
- * @api
- *
- * @param string $buffer
- * @param string|array $out
- * @param bool $shouldValidate
- *
- * @return bool
- */
- public function parseSelector($buffer, &$out, $shouldValidate = true)
- {
- $this->count = 0;
- $this->env = null;
- $this->inParens = false;
- $this->eatWhiteDefault = true;
- $this->buffer = (string) $buffer;
-
- $this->saveEncoding();
- $this->extractLineNumbers($this->buffer);
-
- // discard space/comments at the start
- $this->discardComments = true;
- $this->whitespace();
- $this->discardComments = false;
-
- $selector = $this->selectors($out);
-
- $this->restoreEncoding();
-
- if ($shouldValidate && $this->count !== strlen($buffer)) {
- throw $this->parseError("`" . substr($buffer, $this->count) . "` is not a valid Selector in `$buffer`");
- }
-
- return $selector;
- }
-
- /**
- * Parse a media Query
- *
- * @api
- *
- * @param string $buffer
- * @param array $out
- *
- * @return bool
- */
- public function parseMediaQueryList($buffer, &$out)
- {
- $this->count = 0;
- $this->env = null;
- $this->inParens = false;
- $this->eatWhiteDefault = true;
- $this->buffer = (string) $buffer;
- $this->discardComments = true;
-
- $this->saveEncoding();
- $this->extractLineNumbers($this->buffer);
-
- $this->whitespace();
-
- $isMediaQuery = $this->mediaQueryList($out);
-
- $this->restoreEncoding();
-
- return $isMediaQuery;
- }
-
- /**
- * Parse a single chunk off the head of the buffer and append it to the
- * current parse environment.
- *
- * Returns false when the buffer is empty, or when there is an error.
- *
- * This function is called repeatedly until the entire document is
- * parsed.
- *
- * This parser is most similar to a recursive descent parser. Single
- * functions represent discrete grammatical rules for the language, and
- * they are able to capture the text that represents those rules.
- *
- * Consider the function Compiler::keyword(). (All parse functions are
- * structured the same.)
- *
- * The function takes a single reference argument. When calling the
- * function it will attempt to match a keyword on the head of the buffer.
- * If it is successful, it will place the keyword in the referenced
- * argument, advance the position in the buffer, and return true. If it
- * fails then it won't advance the buffer and it will return false.
- *
- * All of these parse functions are powered by Compiler::match(), which behaves
- * the same way, but takes a literal regular expression. Sometimes it is
- * more convenient to use match instead of creating a new function.
- *
- * Because of the format of the functions, to parse an entire string of
- * grammatical rules, you can chain them together using &&.
- *
- * But, if some of the rules in the chain succeed before one fails, then
- * the buffer position will be left at an invalid state. In order to
- * avoid this, Compiler::seek() is used to remember and set buffer positions.
- *
- * Before parsing a chain, use $s = $this->count to remember the current
- * position into $s. Then if a chain fails, use $this->seek($s) to
- * go back where we started.
- *
- * @return bool
- */
- protected function parseChunk()
- {
- $s = $this->count;
-
- // the directives
- if (isset($this->buffer[$this->count]) && $this->buffer[$this->count] === '@') {
- if (
- $this->literal('@at-root', 8) &&
- ($this->selectors($selector) || true) &&
- ($this->map($with) || true) &&
- (($this->matchChar('(') &&
- $this->interpolation($with) &&
- $this->matchChar(')')) || true) &&
- $this->matchChar('{', false)
- ) {
- ! $this->cssOnly || $this->assertPlainCssValid(false, $s);
-
- $atRoot = new AtRootBlock();
- $this->registerPushedBlock($atRoot, $s);
- $atRoot->selector = $selector;
- $atRoot->with = $with;
-
- return true;
- }
-
- $this->seek($s);
-
- if (
- $this->literal('@media', 6) &&
- $this->mediaQueryList($mediaQueryList) &&
- $this->matchChar('{', false)
- ) {
- $media = new MediaBlock();
- $this->registerPushedBlock($media, $s);
- $media->queryList = $mediaQueryList[2];
-
- return true;
- }
-
- $this->seek($s);
-
- if (
- $this->literal('@mixin', 6) &&
- $this->keyword($mixinName) &&
- ($this->argumentDef($args) || true) &&
- $this->matchChar('{', false)
- ) {
- ! $this->cssOnly || $this->assertPlainCssValid(false, $s);
-
- $mixin = new CallableBlock(Type::T_MIXIN);
- $this->registerPushedBlock($mixin, $s);
- $mixin->name = $mixinName;
- $mixin->args = $args;
-
- return true;
- }
-
- $this->seek($s);
-
- if (
- ($this->literal('@include', 8) &&
- $this->keyword($mixinName) &&
- ($this->matchChar('(') &&
- ($this->argValues($argValues) || true) &&
- $this->matchChar(')') || true) &&
- ($this->end()) ||
- ($this->literal('using', 5) &&
- $this->argumentDef($argUsing) &&
- ($this->end() || $this->matchChar('{') && $hasBlock = true)) ||
- $this->matchChar('{') && $hasBlock = true)
- ) {
- ! $this->cssOnly || $this->assertPlainCssValid(false, $s);
-
- $child = [
- Type::T_INCLUDE,
- $mixinName,
- isset($argValues) ? $argValues : null,
- null,
- isset($argUsing) ? $argUsing : null
- ];
-
- if (! empty($hasBlock)) {
- $include = new ContentBlock();
- $this->registerPushedBlock($include, $s);
- $include->child = $child;
- } else {
- $this->append($child, $s);
- }
-
- return true;
- }
-
- $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) &&
- $importPath[0] !== Type::T_FUNCTION_CALL &&
- $this->end()
- ) {
- if ($this->cssOnly) {
- $this->assertPlainCssValid([Type::T_IMPORT, $importPath], $s);
- $this->append([Type::T_COMMENT, rtrim(substr($this->buffer, $s, $this->count - $s))]);
- return true;
- }
-
- $this->append([Type::T_IMPORT, $importPath], $s);
-
- return true;
- }
-
- $this->seek($s);
-
- if (
- $this->literal('@import', 7) &&
- $this->url($importPath) &&
- $this->end()
- ) {
- if ($this->cssOnly) {
- $this->assertPlainCssValid([Type::T_IMPORT, $importPath], $s);
- $this->append([Type::T_COMMENT, rtrim(substr($this->buffer, $s, $this->count - $s))]);
- return true;
- }
-
- $this->append([Type::T_IMPORT, $importPath], $s);
-
- return true;
- }
-
- $this->seek($s);
-
- if (
- $this->literal('@extend', 7) &&
- $this->selectors($selectors) &&
- $this->end()
- ) {
- ! $this->cssOnly || $this->assertPlainCssValid(false, $s);
-
- // check for '!flag'
- $optional = $this->stripOptionalFlag($selectors);
- $this->append([Type::T_EXTEND, $selectors, $optional], $s);
-
- return true;
- }
-
- $this->seek($s);
-
- if (
- $this->literal('@function', 9) &&
- $this->keyword($fnName) &&
- $this->argumentDef($args) &&
- $this->matchChar('{', false)
- ) {
- ! $this->cssOnly || $this->assertPlainCssValid(false, $s);
-
- $func = new CallableBlock(Type::T_FUNCTION);
- $this->registerPushedBlock($func, $s);
- $func->name = $fnName;
- $func->args = $args;
-
- return true;
- }
-
- $this->seek($s);
-
- if (
- $this->literal('@return', 7) &&
- ($this->valueList($retVal) || true) &&
- $this->end()
- ) {
- ! $this->cssOnly || $this->assertPlainCssValid(false, $s);
-
- $this->append([Type::T_RETURN, isset($retVal) ? $retVal : [Type::T_NULL]], $s);
-
- return true;
- }
-
- $this->seek($s);
-
- if (
- $this->literal('@each', 5) &&
- $this->genericList($varNames, 'variable', ',', false) &&
- $this->literal('in', 2) &&
- $this->valueList($list) &&
- $this->matchChar('{', false)
- ) {
- ! $this->cssOnly || $this->assertPlainCssValid(false, $s);
-
- $each = new EachBlock();
- $this->registerPushedBlock($each, $s);
-
- foreach ($varNames[2] as $varName) {
- $each->vars[] = $varName[1];
- }
-
- $each->list = $list;
-
- return true;
- }
-
- $this->seek($s);
-
- if (
- $this->literal('@while', 6) &&
- $this->expression($cond) &&
- $this->matchChar('{', false)
- ) {
- ! $this->cssOnly || $this->assertPlainCssValid(false, $s);
-
- while (
- $cond[0] === Type::T_LIST &&
- ! empty($cond['enclosing']) &&
- $cond['enclosing'] === 'parent' &&
- \count($cond[2]) == 1
- ) {
- $cond = reset($cond[2]);
- }
-
- $while = new WhileBlock();
- $this->registerPushedBlock($while, $s);
- $while->cond = $cond;
-
- return true;
- }
-
- $this->seek($s);
-
- if (
- $this->literal('@for', 4) &&
- $this->variable($varName) &&
- $this->literal('from', 4) &&
- $this->expression($start) &&
- ($this->literal('through', 7) ||
- ($forUntil = true && $this->literal('to', 2))) &&
- $this->expression($end) &&
- $this->matchChar('{', false)
- ) {
- ! $this->cssOnly || $this->assertPlainCssValid(false, $s);
-
- $for = new ForBlock();
- $this->registerPushedBlock($for, $s);
- $for->var = $varName[1];
- $for->start = $start;
- $for->end = $end;
- $for->until = isset($forUntil);
-
- return true;
- }
-
- $this->seek($s);
-
- if (
- $this->literal('@if', 3) &&
- $this->functionCallArgumentsList($cond, false, '{', false)
- ) {
- ! $this->cssOnly || $this->assertPlainCssValid(false, $s);
-
- $if = new IfBlock();
- $this->registerPushedBlock($if, $s);
-
- while (
- $cond[0] === Type::T_LIST &&
- ! empty($cond['enclosing']) &&
- $cond['enclosing'] === 'parent' &&
- \count($cond[2]) == 1
- ) {
- $cond = reset($cond[2]);
- }
-
- $if->cond = $cond;
- $if->cases = [];
-
- return true;
- }
-
- $this->seek($s);
-
- if (
- $this->literal('@debug', 6) &&
- $this->functionCallArgumentsList($value, false)
- ) {
- ! $this->cssOnly || $this->assertPlainCssValid(false, $s);
-
- $this->append([Type::T_DEBUG, $value], $s);
-
- return true;
- }
-
- $this->seek($s);
-
- if (
- $this->literal('@warn', 5) &&
- $this->functionCallArgumentsList($value, false)
- ) {
- ! $this->cssOnly || $this->assertPlainCssValid(false, $s);
-
- $this->append([Type::T_WARN, $value], $s);
-
- return true;
- }
-
- $this->seek($s);
-
- if (
- $this->literal('@error', 6) &&
- $this->functionCallArgumentsList($value, false)
- ) {
- ! $this->cssOnly || $this->assertPlainCssValid(false, $s);
-
- $this->append([Type::T_ERROR, $value], $s);
-
- return true;
- }
-
- $this->seek($s);
-
- if (
- $this->literal('@content', 8) &&
- ($this->end() ||
- $this->matchChar('(') &&
- $this->argValues($argContent) &&
- $this->matchChar(')') &&
- $this->end())
- ) {
- ! $this->cssOnly || $this->assertPlainCssValid(false, $s);
-
- $this->append([Type::T_MIXIN_CONTENT, isset($argContent) ? $argContent : null], $s);
-
- return true;
- }
-
- $this->seek($s);
-
- $last = $this->last();
-
- if (isset($last) && $last[0] === Type::T_IF) {
- list(, $if) = $last;
- assert($if instanceof IfBlock);
-
- if ($this->literal('@else', 5)) {
- if ($this->matchChar('{', false)) {
- $else = new ElseBlock();
- } elseif (
- $this->literal('if', 2) &&
- $this->functionCallArgumentsList($cond, false, '{', false)
- ) {
- $else = new ElseifBlock();
- $else->cond = $cond;
- }
-
- if (isset($else)) {
- $this->registerPushedBlock($else, $s);
- $if->cases[] = $else;
-
- return true;
- }
- }
-
- $this->seek($s);
- }
-
- // only retain the first @charset directive encountered
- if (
- $this->literal('@charset', 8) &&
- $this->valueList($charset) &&
- $this->end()
- ) {
- return true;
- }
-
- $this->seek($s);
-
- if (
- $this->literal('@supports', 9) &&
- ($t1 = $this->supportsQuery($supportQuery)) &&
- ($t2 = $this->matchChar('{', false))
- ) {
- $directive = new DirectiveBlock();
- $this->registerPushedBlock($directive, $s);
- $directive->name = 'supports';
- $directive->value = $supportQuery;
-
- return true;
- }
-
- $this->seek($s);
-
- // doesn't match built in directive, do generic one
- if (
- $this->matchChar('@', false) &&
- $this->mixedKeyword($dirName) &&
- $this->directiveValue($dirValue, '{')
- ) {
- if (count($dirName) === 1 && is_string(reset($dirName))) {
- $dirName = reset($dirName);
- } else {
- $dirName = [Type::T_STRING, '', $dirName];
- }
- if ($dirName === 'media') {
- $directive = new MediaBlock();
- } else {
- $directive = new DirectiveBlock();
- $directive->name = $dirName;
- }
- $this->registerPushedBlock($directive, $s);
-
- if (isset($dirValue)) {
- ! $this->cssOnly || ($dirValue = $this->assertPlainCssValid($dirValue));
- $directive->value = $dirValue;
- }
-
- return true;
- }
-
- $this->seek($s);
-
- // maybe it's a generic blockless directive
- if (
- $this->matchChar('@', false) &&
- $this->mixedKeyword($dirName) &&
- ! $this->isKnownGenericDirective($dirName) &&
- ($this->end(false) || ($this->directiveValue($dirValue, '') && $this->end(false)))
- ) {
- if (\count($dirName) === 1 && \is_string(\reset($dirName))) {
- $dirName = \reset($dirName);
- } else {
- $dirName = [Type::T_STRING, '', $dirName];
- }
- if (
- ! empty($this->env->parent) &&
- $this->env->type &&
- ! \in_array($this->env->type, [Type::T_DIRECTIVE, Type::T_MEDIA])
- ) {
- $plain = \trim(\substr($this->buffer, $s, $this->count - $s));
- throw $this->parseError(
- "Unknown directive `{$plain}` not allowed in `" . $this->env->type . "` block"
- );
- }
- // blockless directives with a blank line after keeps their blank lines after
- // sass-spec compliance purpose
- $s = $this->count;
- $hasBlankLine = false;
- if ($this->match('\s*?\n\s*\n', $out, false)) {
- $hasBlankLine = true;
- $this->seek($s);
- }
- $isNotRoot = ! empty($this->env->parent);
- $this->append([Type::T_DIRECTIVE, [$dirName, $dirValue, $hasBlankLine, $isNotRoot]], $s);
- $this->whitespace();
-
- return true;
- }
-
- $this->seek($s);
-
- return false;
- }
-
- $inCssSelector = null;
- if ($this->cssOnly) {
- $inCssSelector = (! empty($this->env->parent) &&
- ! in_array($this->env->type, [Type::T_DIRECTIVE, Type::T_MEDIA]));
- }
- // custom properties : right part is static
- if (($this->customProperty($name) ) && $this->matchChar(':', false)) {
- $start = $this->count;
-
- // but can be complex and finish with ; or }
- foreach ([';','}'] as $ending) {
- if (
- $this->openString($ending, $stringValue, '(', ')', false) &&
- $this->end()
- ) {
- $end = $this->count;
- $value = $stringValue;
-
- // check if we have only a partial value due to nested [] or { } to take in account
- $nestingPairs = [['[', ']'], ['{', '}']];
-
- foreach ($nestingPairs as $nestingPair) {
- $p = strpos($this->buffer, $nestingPair[0], $start);
-
- if ($p && $p < $end) {
- $this->seek($start);
-
- if (
- $this->openString($ending, $stringValue, $nestingPair[0], $nestingPair[1], false) &&
- $this->end() &&
- $this->count > $end
- ) {
- $end = $this->count;
- $value = $stringValue;
- }
- }
- }
-
- $this->seek($end);
- $this->append([Type::T_CUSTOM_PROPERTY, $name, $value], $s);
-
- return true;
- }
- }
-
- // TODO: output an error here if nothing found according to sass spec
- }
-
- $this->seek($s);
-
- // property shortcut
- // captures most properties before having to parse a selector
- if (
- $this->keyword($name, false) &&
- $this->literal(': ', 2) &&
- $this->valueList($value) &&
- $this->end()
- ) {
- $name = [Type::T_STRING, '', [$name]];
- $this->append([Type::T_ASSIGN, $name, $value], $s);
-
- return true;
- }
-
- $this->seek($s);
-
- // variable assigns
- if (
- $this->variable($name) &&
- $this->matchChar(':') &&
- $this->valueList($value) &&
- $this->end()
- ) {
- ! $this->cssOnly || $this->assertPlainCssValid(false, $s);
-
- // check for '!flag'
- $assignmentFlags = $this->stripAssignmentFlags($value);
- $this->append([Type::T_ASSIGN, $name, $value, $assignmentFlags], $s);
-
- return true;
- }
-
- $this->seek($s);
-
- // opening css block
- if (
- $this->selectors($selectors) &&
- $this->matchChar('{', false)
- ) {
- ! $this->cssOnly || ! $inCssSelector || $this->assertPlainCssValid(false);
-
- $this->pushBlock($selectors, $s);
-
- if ($this->eatWhiteDefault) {
- $this->whitespace();
- $this->append(null); // collect comments at the beginning if needed
- }
-
- return true;
- }
-
- $this->seek($s);
-
- // property assign, or nested assign
- if (
- $this->propertyName($name) &&
- $this->matchChar(':')
- ) {
- $foundSomething = false;
-
- if ($this->valueList($value)) {
- if (empty($this->env->parent)) {
- throw $this->parseError('expected "{"');
- }
-
- $this->append([Type::T_ASSIGN, $name, $value], $s);
- $foundSomething = true;
- }
-
- if ($this->matchChar('{', false)) {
- ! $this->cssOnly || $this->assertPlainCssValid(false);
-
- $propBlock = new NestedPropertyBlock();
- $this->registerPushedBlock($propBlock, $s);
- $propBlock->prefix = $name;
- $propBlock->hasValue = $foundSomething;
-
- $foundSomething = true;
- } elseif ($foundSomething) {
- $foundSomething = $this->end();
- }
-
- if ($foundSomething) {
- return true;
- }
- }
-
- $this->seek($s);
-
- // closing a block
- if ($this->matchChar('}', false)) {
- $block = $this->popBlock();
-
- if (! isset($block->type) || $block->type !== Type::T_IF) {
- assert($this->env !== null);
-
- if ($this->env->parent) {
- $this->append(null); // collect comments before next statement if needed
- }
- }
-
- if ($block instanceof ContentBlock) {
- $include = $block->child;
- assert(\is_array($include));
- unset($block->child);
- $include[3] = $block;
- $this->append($include, $s);
- } elseif (!$block instanceof ElseBlock && !$block instanceof ElseifBlock) {
- $type = isset($block->type) ? $block->type : Type::T_BLOCK;
- $this->append([$type, $block], $s);
- }
-
- // collect comments just after the block closing if needed
- if ($this->eatWhiteDefault) {
- $this->whitespace();
- assert($this->env !== null);
-
- if ($this->env->comments) {
- $this->append(null);
- }
- }
-
- return true;
- }
-
- // extra stuff
- if ($this->matchChar(';')) {
- return true;
- }
-
- return false;
- }
-
- /**
- * Push block onto parse tree
- *
- * @param array|null $selectors
- * @param int $pos
- *
- * @return Block
- */
- protected function pushBlock($selectors, $pos = 0)
- {
- $b = new Block();
- $b->selectors = $selectors;
-
- $this->registerPushedBlock($b, $pos);
-
- return $b;
- }
-
- /**
- * @param Block $b
- * @param int $pos
- *
- * @return void
- */
- private function registerPushedBlock(Block $b, $pos)
- {
- list($line, $column) = $this->getSourcePosition($pos);
-
- $b->sourceName = $this->sourceName;
- $b->sourceLine = $line;
- $b->sourceColumn = $column;
- $b->sourceIndex = $this->sourceIndex;
- $b->comments = [];
- $b->parent = $this->env;
-
- if (! $this->env) {
- $b->children = [];
- } elseif (empty($this->env->children)) {
- $this->env->children = $this->env->comments;
- $b->children = [];
- $this->env->comments = [];
- } else {
- $b->children = $this->env->comments;
- $this->env->comments = [];
- }
-
- $this->env = $b;
-
- // collect comments at the beginning of a block if needed
- if ($this->eatWhiteDefault) {
- $this->whitespace();
- assert($this->env !== null);
-
- if ($this->env->comments) {
- $this->append(null);
- }
- }
- }
-
- /**
- * Push special (named) block onto parse tree
- *
- * @deprecated
- *
- * @param string $type
- * @param int $pos
- *
- * @return Block
- */
- protected function pushSpecialBlock($type, $pos)
- {
- $block = $this->pushBlock(null, $pos);
- $block->type = $type;
-
- return $block;
- }
-
- /**
- * Pop scope and return last block
- *
- * @return Block
- *
- * @throws \Exception
- */
- protected function popBlock()
- {
- assert($this->env !== null);
-
- // collect comments ending just before of a block closing
- if ($this->env->comments) {
- $this->append(null);
- }
-
- // pop the block
- $block = $this->env;
-
- if (empty($block->parent)) {
- throw $this->parseError('unexpected }');
- }
-
- if ($block->type == Type::T_AT_ROOT) {
- // keeps the parent in case of self selector &
- $block->selfParent = $block->parent;
- }
-
- $this->env = $block->parent;
-
- unset($block->parent);
-
- return $block;
- }
-
- /**
- * Peek input stream
- *
- * @param string $regex
- * @param array $out
- * @param int $from
- *
- * @return int
- */
- protected function peek($regex, &$out, $from = null)
- {
- if (! isset($from)) {
- $from = $this->count;
- }
-
- $r = '/' . $regex . '/' . $this->patternModifiers;
- $result = preg_match($r, $this->buffer, $out, 0, $from);
-
- return $result;
- }
-
- /**
- * Seek to position in input stream (or return current position in input stream)
- *
- * @param int $where
- *
- * @return void
- */
- protected function seek($where)
- {
- $this->count = $where;
- }
-
- /**
- * Assert a parsed part is plain CSS Valid
- *
- * @param array|false $parsed
- * @param int $startPos
- *
- * @return array
- *
- * @throws ParserException
- */
- protected function assertPlainCssValid($parsed, $startPos = null)
- {
- $type = '';
- if ($parsed) {
- $type = $parsed[0];
- $parsed = $this->isPlainCssValidElement($parsed);
- }
- if (! $parsed) {
- if (! \is_null($startPos)) {
- $plain = rtrim(substr($this->buffer, $startPos, $this->count - $startPos));
- $message = "Error : `{$plain}` isn't allowed in plain CSS";
- } else {
- $message = 'Error: SCSS syntax not allowed in CSS file';
- }
- if ($type) {
- $message .= " ($type)";
- }
- throw $this->parseError($message);
- }
-
- return $parsed;
- }
-
- /**
- * Check a parsed element is plain CSS Valid
- *
- * @param array $parsed
- * @param bool $allowExpression
- *
- * @return array|false
- */
- protected function isPlainCssValidElement($parsed, $allowExpression = false)
- {
- // keep string as is
- if (is_string($parsed)) {
- return $parsed;
- }
-
- if (
- \in_array($parsed[0], [Type::T_FUNCTION, Type::T_FUNCTION_CALL]) &&
- !\in_array($parsed[1], [
- 'alpha',
- 'attr',
- 'calc',
- 'cubic-bezier',
- 'env',
- 'grayscale',
- 'hsl',
- 'hsla',
- 'hwb',
- 'invert',
- 'linear-gradient',
- 'min',
- 'max',
- 'radial-gradient',
- 'repeating-linear-gradient',
- 'repeating-radial-gradient',
- 'rgb',
- 'rgba',
- 'rotate',
- 'saturate',
- 'var',
- ]) &&
- Compiler::isNativeFunction($parsed[1])
- ) {
- return false;
- }
-
- switch ($parsed[0]) {
- case Type::T_BLOCK:
- case Type::T_KEYWORD:
- case Type::T_NULL:
- case Type::T_NUMBER:
- case Type::T_MEDIA:
- return $parsed;
-
- case Type::T_COMMENT:
- if (isset($parsed[2])) {
- return false;
- }
- return $parsed;
-
- case Type::T_DIRECTIVE:
- if (\is_array($parsed[1])) {
- $parsed[1][1] = $this->isPlainCssValidElement($parsed[1][1]);
- if (! $parsed[1][1]) {
- return false;
- }
- }
-
- return $parsed;
-
- case Type::T_IMPORT:
- if ($parsed[1][0] === Type::T_LIST) {
- return false;
- }
- $parsed[1] = $this->isPlainCssValidElement($parsed[1]);
- if ($parsed[1] === false) {
- return false;
- }
- return $parsed;
-
- case Type::T_STRING:
- foreach ($parsed[2] as $k => $substr) {
- if (\is_array($substr)) {
- $parsed[2][$k] = $this->isPlainCssValidElement($substr);
- if (! $parsed[2][$k]) {
- return false;
- }
- }
- }
- return $parsed;
-
- case Type::T_LIST:
- if (!empty($parsed['enclosing'])) {
- return false;
- }
- foreach ($parsed[2] as $k => $listElement) {
- $parsed[2][$k] = $this->isPlainCssValidElement($listElement);
- if (! $parsed[2][$k]) {
- return false;
- }
- }
- return $parsed;
-
- case Type::T_ASSIGN:
- foreach ([1, 2, 3] as $k) {
- if (! empty($parsed[$k])) {
- $parsed[$k] = $this->isPlainCssValidElement($parsed[$k]);
- if (! $parsed[$k]) {
- return false;
- }
- }
- }
- return $parsed;
-
- case Type::T_EXPRESSION:
- list( ,$op, $lhs, $rhs, $inParens, $whiteBefore, $whiteAfter) = $parsed;
- if (! $allowExpression && ! \in_array($op, ['and', 'or', '/'])) {
- return false;
- }
- $lhs = $this->isPlainCssValidElement($lhs, true);
- if (! $lhs) {
- return false;
- }
- $rhs = $this->isPlainCssValidElement($rhs, true);
- if (! $rhs) {
- return false;
- }
-
- return [
- Type::T_STRING,
- '', [
- $this->inParens ? '(' : '',
- $lhs,
- ($whiteBefore ? ' ' : '') . $op . ($whiteAfter ? ' ' : ''),
- $rhs,
- $this->inParens ? ')' : ''
- ]
- ];
-
- case Type::T_CUSTOM_PROPERTY:
- case Type::T_UNARY:
- $parsed[2] = $this->isPlainCssValidElement($parsed[2]);
- if (! $parsed[2]) {
- return false;
- }
- return $parsed;
-
- case Type::T_FUNCTION:
- $argsList = $parsed[2];
- foreach ($argsList[2] as $argElement) {
- if (! $this->isPlainCssValidElement($argElement)) {
- return false;
- }
- }
- return $parsed;
-
- case Type::T_FUNCTION_CALL:
- $parsed[0] = Type::T_FUNCTION;
- $argsList = [Type::T_LIST, ',', []];
- foreach ($parsed[2] as $arg) {
- if ($arg[0] || ! empty($arg[2])) {
- // no named arguments possible in a css function call
- // nor ... argument
- return false;
- }
- $arg = $this->isPlainCssValidElement($arg[1], $parsed[1] === 'calc');
- if (! $arg) {
- return false;
- }
- $argsList[2][] = $arg;
- }
- $parsed[2] = $argsList;
- return $parsed;
- }
-
- return false;
- }
-
- /**
- * Match string looking for either ending delim, escape, or string interpolation
- *
- * {@internal This is a workaround for preg_match's 250K string match limit. }}
- *
- * @param array $m Matches (passed by reference)
- * @param string $delim Delimiter
- *
- * @return bool True if match; false otherwise
- *
- * @phpstan-impure
- */
- protected function matchString(&$m, $delim)
- {
- $token = null;
-
- $end = \strlen($this->buffer);
-
- // look for either ending delim, escape, or string interpolation
- foreach (['#{', '\\', "\r", $delim] as $lookahead) {
- $pos = strpos($this->buffer, $lookahead, $this->count);
-
- if ($pos !== false && $pos < $end) {
- $end = $pos;
- $token = $lookahead;
- }
- }
-
- if (! isset($token)) {
- return false;
- }
-
- $match = substr($this->buffer, $this->count, $end - $this->count);
- $m = [
- $match . $token,
- $match,
- $token
- ];
- $this->count = $end + \strlen($token);
-
- return true;
- }
-
- /**
- * Try to match something on head of buffer
- *
- * @param string $regex
- * @param array $out
- * @param bool $eatWhitespace
- *
- * @return bool
- *
- * @phpstan-impure
- */
- protected function match($regex, &$out, $eatWhitespace = null)
- {
- $r = '/' . $regex . '/' . $this->patternModifiers;
-
- if (! preg_match($r, $this->buffer, $out, 0, $this->count)) {
- return false;
- }
-
- $this->count += \strlen($out[0]);
-
- if (! isset($eatWhitespace)) {
- $eatWhitespace = $this->eatWhiteDefault;
- }
-
- if ($eatWhitespace) {
- $this->whitespace();
- }
-
- return true;
- }
-
- /**
- * Match a single string
- *
- * @param string $char
- * @param bool $eatWhitespace
- *
- * @return bool
- *
- * @phpstan-impure
- */
- protected function matchChar($char, $eatWhitespace = null)
- {
- if (! isset($this->buffer[$this->count]) || $this->buffer[$this->count] !== $char) {
- return false;
- }
-
- $this->count++;
-
- if (! isset($eatWhitespace)) {
- $eatWhitespace = $this->eatWhiteDefault;
- }
-
- if ($eatWhitespace) {
- $this->whitespace();
- }
-
- return true;
- }
-
- /**
- * Match literal string
- *
- * @param string $what
- * @param int $len
- * @param bool $eatWhitespace
- *
- * @return bool
- *
- * @phpstan-impure
- */
- protected function literal($what, $len, $eatWhitespace = null)
- {
- if (strcasecmp(substr($this->buffer, $this->count, $len), $what) !== 0) {
- return false;
- }
-
- $this->count += $len;
-
- if (! isset($eatWhitespace)) {
- $eatWhitespace = $this->eatWhiteDefault;
- }
-
- if ($eatWhitespace) {
- $this->whitespace();
- }
-
- return true;
- }
-
- /**
- * Match some whitespace
- *
- * @return bool
- *
- * @phpstan-impure
- */
- protected function whitespace()
- {
- $gotWhite = false;
-
- 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 = [];
- $startCommentCount = $this->count;
- $endCommentCount = $this->count + \strlen($m[1]);
-
- // find interpolations in comment
- $p = strpos($this->buffer, '#{', $this->count);
-
- while ($p !== false && $p < $endCommentCount) {
- $c = substr($this->buffer, $this->count, $p - $this->count);
- $comment[] = $c;
- $this->count = $p;
- $out = null;
-
- if ($this->interpolation($out)) {
- // keep right spaces in the following string part
- if ($out[3]) {
- while ($this->buffer[$this->count - 1] !== '}') {
- $this->count--;
- }
-
- $out[3] = '';
- }
-
- $comment[] = [Type::T_COMMENT, substr($this->buffer, $p, $this->count - $p), $out];
- } else {
- list($line, $column) = $this->getSourcePosition($this->count);
- $file = $this->sourceName;
- if (!$this->discardComments) {
- $this->logger->warn("Unterminated interpolations in multiline comments are deprecated and will be removed in ScssPhp 2.0, in \"$file\", line $line, column $column.", true);
- }
- $comment[] = substr($this->buffer, $this->count, 2);
-
- $this->count += 2;
- }
-
- $p = strpos($this->buffer, '#{', $this->count);
- }
-
- // remaining part
- $c = substr($this->buffer, $this->count, $endCommentCount - $this->count);
-
- if (! $comment) {
- // single part static comment
- $commentStatement = [Type::T_COMMENT, $c];
- } else {
- $comment[] = $c;
- $staticComment = substr($this->buffer, $startCommentCount, $endCommentCount - $startCommentCount);
- $commentStatement = [Type::T_COMMENT, $staticComment, [Type::T_STRING, '', $comment]];
- }
-
- list($line, $column) = $this->getSourcePosition($startCommentCount);
- $commentStatement[self::SOURCE_LINE] = $line;
- $commentStatement[self::SOURCE_COLUMN] = $column;
- $commentStatement[self::SOURCE_INDEX] = $this->sourceIndex;
-
- $this->appendComment($commentStatement);
-
- $this->commentsSeen[$startCommentCount] = true;
- $this->count = $endCommentCount;
- } else {
- // comment that are ignored and not kept in the output css
- $this->count += \strlen($m[0]);
- // silent comments are not allowed in plain CSS files
- ! $this->cssOnly
- || ! \strlen(trim($m[0]))
- || $this->assertPlainCssValid(false, $this->count - \strlen($m[0]));
- }
-
- $gotWhite = true;
- }
-
- return $gotWhite;
- }
-
- /**
- * Append comment to current block
- *
- * @param array $comment
- *
- * @return void
- */
- protected function appendComment($comment)
- {
- if (! $this->discardComments) {
- assert($this->env !== null);
-
- $this->env->comments[] = $comment;
- }
- }
-
- /**
- * Append statement to current block
- *
- * @param array|null $statement
- * @param int $pos
- *
- * @return void
- */
- protected function append($statement, $pos = null)
- {
- assert($this->env !== null);
-
- if (! \is_null($statement)) {
- ! $this->cssOnly || ($statement = $this->assertPlainCssValid($statement, $pos));
-
- if (! \is_null($pos)) {
- list($line, $column) = $this->getSourcePosition($pos);
-
- $statement[static::SOURCE_LINE] = $line;
- $statement[static::SOURCE_COLUMN] = $column;
- $statement[static::SOURCE_INDEX] = $this->sourceIndex;
- }
-
- $this->env->children[] = $statement;
- }
-
- $comments = $this->env->comments;
-
- if ($comments) {
- $this->env->children = array_merge($this->env->children, $comments);
- $this->env->comments = [];
- }
- }
-
- /**
- * Returns last child was appended
- *
- * @return array|null
- */
- protected function last()
- {
- assert($this->env !== null);
-
- $i = \count($this->env->children) - 1;
-
- if (isset($this->env->children[$i])) {
- return $this->env->children[$i];
- }
-
- return null;
- }
-
- /**
- * Parse media query list
- *
- * @param array $out
- *
- * @return bool
- */
- protected function mediaQueryList(&$out)
- {
- return $this->genericList($out, 'mediaQuery', ',', false);
- }
-
- /**
- * Parse media query
- *
- * @param array $out
- *
- * @return bool
- */
- protected function mediaQuery(&$out)
- {
- $expressions = null;
- $parts = [];
-
- if (
- ($this->literal('only', 4) && ($only = true) ||
- $this->literal('not', 3) && ($not = true) || true) &&
- $this->mixedKeyword($mediaType)
- ) {
- $prop = [Type::T_MEDIA_TYPE];
-
- if (isset($only)) {
- $prop[] = [Type::T_KEYWORD, 'only'];
- }
-
- if (isset($not)) {
- $prop[] = [Type::T_KEYWORD, 'not'];
- }
-
- $media = [Type::T_LIST, '', []];
-
- foreach ((array) $mediaType as $type) {
- if (\is_array($type)) {
- $media[2][] = $type;
- } else {
- $media[2][] = [Type::T_KEYWORD, $type];
- }
- }
-
- $prop[] = $media;
- $parts[] = $prop;
- }
-
- if (empty($parts) || $this->literal('and', 3)) {
- $this->genericList($expressions, 'mediaExpression', 'and', false);
-
- if (\is_array($expressions)) {
- $parts = array_merge($parts, $expressions[2]);
- }
- }
-
- $out = $parts;
-
- return true;
- }
-
- /**
- * Parse supports query
- *
- * @param array $out
- *
- * @return bool
- */
- protected function supportsQuery(&$out)
- {
- $expressions = null;
- $parts = [];
-
- $s = $this->count;
-
- $not = false;
-
- if (
- ($this->literal('not', 3) && ($not = true) || true) &&
- $this->matchChar('(') &&
- ($this->expression($property)) &&
- $this->literal(': ', 2) &&
- $this->valueList($value) &&
- $this->matchChar(')')
- ) {
- $support = [Type::T_STRING, '', [[Type::T_KEYWORD, ($not ? 'not ' : '') . '(']]];
- $support[2][] = $property;
- $support[2][] = [Type::T_KEYWORD, ': '];
- $support[2][] = $value;
- $support[2][] = [Type::T_KEYWORD, ')'];
-
- $parts[] = $support;
- $s = $this->count;
- } else {
- $this->seek($s);
- }
-
- if (
- $this->matchChar('(') &&
- $this->supportsQuery($subQuery) &&
- $this->matchChar(')')
- ) {
- $parts[] = [Type::T_STRING, '', [[Type::T_KEYWORD, '('], $subQuery, [Type::T_KEYWORD, ')']]];
- $s = $this->count;
- } else {
- $this->seek($s);
- }
-
- if (
- $this->literal('not', 3) &&
- $this->supportsQuery($subQuery)
- ) {
- $parts[] = [Type::T_STRING, '', [[Type::T_KEYWORD, 'not '], $subQuery]];
- $s = $this->count;
- } else {
- $this->seek($s);
- }
-
- if (
- $this->literal('selector(', 9) &&
- $this->selector($selector) &&
- $this->matchChar(')')
- ) {
- $support = [Type::T_STRING, '', [[Type::T_KEYWORD, 'selector(']]];
-
- $selectorList = [Type::T_LIST, '', []];
-
- foreach ($selector as $sc) {
- $compound = [Type::T_STRING, '', []];
-
- foreach ($sc as $scp) {
- if (\is_array($scp)) {
- $compound[2][] = $scp;
- } else {
- $compound[2][] = [Type::T_KEYWORD, $scp];
- }
- }
-
- $selectorList[2][] = $compound;
- }
-
- $support[2][] = $selectorList;
- $support[2][] = [Type::T_KEYWORD, ')'];
- $parts[] = $support;
- $s = $this->count;
- } else {
- $this->seek($s);
- }
-
- if ($this->variable($var) or $this->interpolation($var)) {
- $parts[] = $var;
- $s = $this->count;
- } else {
- $this->seek($s);
- }
-
- if (
- $this->literal('and', 3) &&
- $this->genericList($expressions, 'supportsQuery', ' and', false)
- ) {
- array_unshift($expressions[2], [Type::T_STRING, '', $parts]);
-
- $parts = [$expressions];
- $s = $this->count;
- } else {
- $this->seek($s);
- }
-
- if (
- $this->literal('or', 2) &&
- $this->genericList($expressions, 'supportsQuery', ' or', false)
- ) {
- array_unshift($expressions[2], [Type::T_STRING, '', $parts]);
-
- $parts = [$expressions];
- $s = $this->count;
- } else {
- $this->seek($s);
- }
-
- if (\count($parts)) {
- if ($this->eatWhiteDefault) {
- $this->whitespace();
- }
-
- $out = [Type::T_STRING, '', $parts];
-
- return true;
- }
-
- return false;
- }
-
-
- /**
- * Parse media expression
- *
- * @param array $out
- *
- * @return bool
- */
- protected function mediaExpression(&$out)
- {
- $s = $this->count;
- $value = null;
-
- if (
- $this->matchChar('(') &&
- $this->expression($feature) &&
- ($this->matchChar(':') &&
- $this->expression($value) || true) &&
- $this->matchChar(')')
- ) {
- $out = [Type::T_MEDIA_EXPRESSION, $feature];
-
- if ($value) {
- $out[] = $value;
- }
-
- return true;
- }
-
- $this->seek($s);
-
- return false;
- }
-
- /**
- * Parse argument values
- *
- * @param array $out
- *
- * @return bool
- */
- protected function argValues(&$out)
- {
- $discardComments = $this->discardComments;
- $this->discardComments = true;
-
- if ($this->genericList($list, 'argValue', ',', false)) {
- $out = $list[2];
-
- $this->discardComments = $discardComments;
-
- return true;
- }
-
- $this->discardComments = $discardComments;
-
- return false;
- }
-
- /**
- * Parse argument value
- *
- * @param array $out
- *
- * @return bool
- */
- protected function argValue(&$out)
- {
- $s = $this->count;
-
- $keyword = null;
-
- if (! $this->variable($keyword) || ! $this->matchChar(':')) {
- $this->seek($s);
-
- $keyword = null;
- }
-
- if ($this->genericList($value, 'expression', '', true)) {
- $out = [$keyword, $value, false];
- $s = $this->count;
-
- if ($this->literal('...', 3)) {
- $out[2] = true;
- } else {
- $this->seek($s);
- }
-
- return true;
- }
-
- return false;
- }
-
- /**
- * Check if a generic directive is known to be able to allow almost any syntax or not
- * @param mixed $directiveName
- * @return bool
- */
- protected function isKnownGenericDirective($directiveName)
- {
- if (\is_array($directiveName) && \is_string(reset($directiveName))) {
- $directiveName = reset($directiveName);
- }
- if (! \is_string($directiveName)) {
- return false;
- }
- if (
- \in_array($directiveName, [
- 'at-root',
- 'media',
- 'mixin',
- 'include',
- 'scssphp-import-once',
- 'import',
- 'extend',
- 'function',
- 'break',
- 'continue',
- 'return',
- 'each',
- 'while',
- 'for',
- 'if',
- 'debug',
- 'warn',
- 'error',
- 'content',
- 'else',
- 'charset',
- 'supports',
- // Todo
- 'use',
- 'forward',
- ])
- ) {
- return true;
- }
- return false;
- }
-
- /**
- * Parse directive value list that considers $vars as keyword
- *
- * @param array $out
- * @param string|false $endChar
- *
- * @return bool
- *
- * @phpstan-impure
- */
- protected function directiveValue(&$out, $endChar = false)
- {
- $s = $this->count;
-
- if ($this->variable($out)) {
- if ($endChar && $this->matchChar($endChar, false)) {
- return true;
- }
-
- if (! $endChar && $this->end()) {
- return true;
- }
- }
-
- $this->seek($s);
-
- if (\is_string($endChar) && $this->openString($endChar ? $endChar : ';', $out, null, null, true, ";}{")) {
- if ($endChar && $this->matchChar($endChar, false)) {
- return true;
- }
- $ss = $this->count;
- if (!$endChar && $this->end()) {
- $this->seek($ss);
- return true;
- }
- }
-
- $this->seek($s);
-
- $allowVars = $this->allowVars;
- $this->allowVars = false;
-
- $res = $this->genericList($out, 'spaceList', ',');
- $this->allowVars = $allowVars;
-
- if ($res) {
- if ($endChar && $this->matchChar($endChar, false)) {
- return true;
- }
-
- if (! $endChar && $this->end()) {
- return true;
- }
- }
-
- $this->seek($s);
-
- if ($endChar && $this->matchChar($endChar, false)) {
- return true;
- }
-
- return false;
- }
-
- /**
- * Parse comma separated value list
- *
- * @param array $out
- *
- * @return bool
- */
- protected function valueList(&$out)
- {
- $discardComments = $this->discardComments;
- $this->discardComments = true;
- $res = $this->genericList($out, 'spaceList', ',');
- $this->discardComments = $discardComments;
-
- return $res;
- }
-
- /**
- * Parse a function call, where externals () are part of the call
- * and not of the value list
- *
- * @param array $out
- * @param bool $mandatoryEnclos
- * @param null|string $charAfter
- * @param null|bool $eatWhiteSp
- *
- * @return bool
- */
- protected function functionCallArgumentsList(&$out, $mandatoryEnclos = true, $charAfter = null, $eatWhiteSp = null)
- {
- $s = $this->count;
-
- if (
- $this->matchChar('(') &&
- $this->valueList($out) &&
- $this->matchChar(')') &&
- ($charAfter ? $this->matchChar($charAfter, $eatWhiteSp) : $this->end())
- ) {
- return true;
- }
-
- if (! $mandatoryEnclos) {
- $this->seek($s);
-
- if (
- $this->valueList($out) &&
- ($charAfter ? $this->matchChar($charAfter, $eatWhiteSp) : $this->end())
- ) {
- return true;
- }
- }
-
- $this->seek($s);
-
- return false;
- }
-
- /**
- * Parse space separated value list
- *
- * @param array $out
- *
- * @return bool
- */
- protected function spaceList(&$out)
- {
- return $this->genericList($out, 'expression');
- }
-
- /**
- * Parse generic list
- *
- * @param array $out
- * @param string $parseItem The name of the method used to parse items
- * @param string $delim
- * @param bool $flatten
- *
- * @return bool
- */
- protected function genericList(&$out, $parseItem, $delim = '', $flatten = true)
- {
- $s = $this->count;
- $items = [];
- /** @var array|Number|null $value */
- $value = null;
-
- while ($this->$parseItem($value)) {
- $trailing_delim = false;
- $items[] = $value;
-
- if ($delim) {
- if (! $this->literal($delim, \strlen($delim))) {
- break;
- }
-
- $trailing_delim = true;
- } else {
- assert(\is_array($value) || $value instanceof Number);
- // if no delim watch that a keyword didn't eat the single/double quote
- // from the following starting string
- if ($value[0] === Type::T_KEYWORD) {
- assert(\is_array($value));
- /** @var string $word */
- $word = $value[1];
-
- $last_char = substr($word, -1);
-
- if (
- strlen($word) > 1 &&
- in_array($last_char, [ "'", '"']) &&
- substr($word, -2, 1) !== '\\'
- ) {
- // if there is a non escaped opening quote in the keyword, this seems unlikely a mistake
- $word = str_replace('\\' . $last_char, '\\\\', $word);
- if (strpos($word, $last_char) < strlen($word) - 1) {
- continue;
- }
-
- $currentCount = $this->count;
-
- // let's try to rewind to previous char and try a parse
- $this->count--;
- // in case the keyword also eat spaces
- while (substr($this->buffer, $this->count, 1) !== $last_char) {
- $this->count--;
- }
-
- /** @var array|Number|null $nextValue */
- $nextValue = null;
- if ($this->$parseItem($nextValue)) {
- assert(\is_array($nextValue) || $nextValue instanceof Number);
- if ($nextValue[0] === Type::T_KEYWORD && $nextValue[1] === $last_char) {
- // bad try, forget it
- $this->seek($currentCount);
- continue;
- }
- if ($nextValue[0] !== Type::T_STRING) {
- // bad try, forget it
- $this->seek($currentCount);
- continue;
- }
-
- // OK it was a good idea
- $value[1] = substr($value[1], 0, -1);
- array_pop($items);
- $items[] = $value;
- $items[] = $nextValue;
- } else {
- // bad try, forget it
- $this->seek($currentCount);
- continue;
- }
- }
- }
- }
- }
-
- if (! $items) {
- $this->seek($s);
-
- return false;
- }
-
- if ($trailing_delim) {
- $items[] = [Type::T_NULL];
- }
-
- if ($flatten && \count($items) === 1) {
- $out = $items[0];
- } else {
- $out = [Type::T_LIST, $delim, $items];
- }
-
- return true;
- }
-
- /**
- * Parse expression
- *
- * @param array $out
- * @param bool $listOnly
- * @param bool $lookForExp
- *
- * @return bool
- *
- * @phpstan-impure
- */
- protected function expression(&$out, $listOnly = false, $lookForExp = true)
- {
- $s = $this->count;
- $discard = $this->discardComments;
- $this->discardComments = true;
- $allowedTypes = ($listOnly ? [Type::T_LIST] : [Type::T_LIST, Type::T_MAP]);
-
- if ($this->matchChar('(')) {
- if ($this->enclosedExpression($lhs, $s, ')', $allowedTypes)) {
- if ($lookForExp) {
- $out = $this->expHelper($lhs, 0);
- } else {
- $out = $lhs;
- }
-
- $this->discardComments = $discard;
-
- return true;
- }
-
- $this->seek($s);
- }
-
- if (\in_array(Type::T_LIST, $allowedTypes) && $this->matchChar('[')) {
- if ($this->enclosedExpression($lhs, $s, ']', [Type::T_LIST])) {
- if ($lookForExp) {
- $out = $this->expHelper($lhs, 0);
- } else {
- $out = $lhs;
- }
-
- $this->discardComments = $discard;
-
- return true;
- }
-
- $this->seek($s);
- }
-
- if (! $listOnly && $this->value($lhs)) {
- if ($lookForExp) {
- $out = $this->expHelper($lhs, 0);
- } else {
- $out = $lhs;
- }
-
- $this->discardComments = $discard;
-
- return true;
- }
-
- $this->discardComments = $discard;
-
- return false;
- }
-
- /**
- * Parse expression specifically checking for lists in parenthesis or brackets
- *
- * @param array $out
- * @param int $s
- * @param string $closingParen
- * @param string[] $allowedTypes
- *
- * @return bool
- *
- * @phpstan-param array<Type::*> $allowedTypes
- */
- 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, '', []];
-
- switch ($closingParen) {
- case ')':
- $out['enclosing'] = 'parent'; // parenthesis list
- break;
-
- case ']':
- $out['enclosing'] = 'bracket'; // bracketed list
- break;
- }
-
- return true;
- }
-
- if (
- $this->valueList($out) &&
- $this->matchChar($closingParen) && ! ($closingParen === ')' &&
- \in_array($out[0], [Type::T_EXPRESSION, Type::T_UNARY])) &&
- \in_array(Type::T_LIST, $allowedTypes)
- ) {
- if ($out[0] !== Type::T_LIST || ! empty($out['enclosing'])) {
- $out = [Type::T_LIST, '', [$out]];
- }
-
- switch ($closingParen) {
- case ')':
- $out['enclosing'] = 'parent'; // parenthesis list
- break;
-
- case ']':
- $out['enclosing'] = 'bracket'; // bracketed list
- break;
- }
-
- return true;
- }
-
- $this->seek($s);
-
- if (\in_array(Type::T_MAP, $allowedTypes) && $this->map($out)) {
- return true;
- }
-
- return false;
- }
-
- /**
- * Parse left-hand side of subexpression
- *
- * @param array $lhs
- * @param int $minP
- *
- * @return array
- */
- protected function expHelper($lhs, $minP)
- {
- $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) && static::$precedence[strtolower($m[1])] >= $minP) {
- $whiteAfter = isset($this->buffer[$this->count]) &&
- ctype_space($this->buffer[$this->count]);
- $varAfter = isset($this->buffer[$this->count]) &&
- $this->buffer[$this->count] === '$';
-
- $this->whitespace();
-
- $op = $m[1];
-
- // don't turn negative numbers into expressions
- if ($op === '-' && $whiteBefore && ! $whiteAfter && ! $varAfter) {
- break;
- }
-
- if (! $this->value($rhs) && ! $this->expression($rhs, true, false)) {
- break;
- }
-
- if ($op === '-' && ! $whiteAfter && $rhs[0] === Type::T_KEYWORD) {
- break;
- }
-
- // consume higher-precedence operators on the right-hand side
- $rhs = $this->expHelper($rhs, static::$precedence[strtolower($op)] + 1);
-
- $lhs = [Type::T_EXPRESSION, $op, $lhs, $rhs, $this->inParens, $whiteBefore, $whiteAfter];
-
- $ss = $this->count;
- $whiteBefore = isset($this->buffer[$this->count - 1]) &&
- ctype_space($this->buffer[$this->count - 1]);
- }
-
- $this->seek($ss);
-
- return $lhs;
- }
-
- /**
- * Parse value
- *
- * @param array $out
- *
- * @return bool
- */
- protected function value(&$out)
- {
- if (! isset($this->buffer[$this->count])) {
- return false;
- }
-
- $s = $this->count;
- $char = $this->buffer[$this->count];
-
- if (
- $this->literal('url(', 4) &&
- $this->match('data:([a-z]+)\/([a-z0-9.+-]+);base64,', $m, false)
- ) {
- $len = strspn(
- $this->buffer,
- 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwyxz0123456789+/=',
- $this->count
- );
-
- $this->count += $len;
-
- if ($this->matchChar(')')) {
- $content = substr($this->buffer, $s, $this->count - $s);
- $out = [Type::T_KEYWORD, $content];
-
- return true;
- }
- }
-
- $this->seek($s);
-
- if (
- $this->literal('url(', 4, false) &&
- $this->match('\s*(\/\/[^\s\)]+)\s*', $m)
- ) {
- $content = 'url(' . $m[1];
-
- if ($this->matchChar(')')) {
- $content .= ')';
- $out = [Type::T_KEYWORD, $content];
-
- return true;
- }
- }
-
- $this->seek($s);
-
- // not
- if ($char === 'n' && $this->literal('not', 3, false)) {
- if (
- $this->whitespace() &&
- $this->value($inner)
- ) {
- $out = [Type::T_UNARY, 'not', $inner, $this->inParens];
-
- return true;
- }
-
- $this->seek($s);
-
- if ($this->parenValue($inner)) {
- $out = [Type::T_UNARY, 'not', $inner, $this->inParens];
-
- return true;
- }
-
- $this->seek($s);
- }
-
- // addition
- if ($char === '+') {
- $this->count++;
-
- $follow_white = $this->whitespace();
-
- if ($this->value($inner)) {
- $out = [Type::T_UNARY, '+', $inner, $this->inParens];
-
- return true;
- }
-
- if ($follow_white) {
- $out = [Type::T_KEYWORD, $char];
- return true;
- }
-
- $this->seek($s);
-
- return false;
- }
-
- // negation
- if ($char === '-') {
- if ($this->customProperty($out)) {
- return true;
- }
-
- $this->count++;
-
- $follow_white = $this->whitespace();
-
- if ($this->variable($inner) || $this->unit($inner) || $this->parenValue($inner)) {
- $out = [Type::T_UNARY, '-', $inner, $this->inParens];
-
- return true;
- }
-
- if (
- $this->keyword($inner) &&
- ! $this->func($inner, $out)
- ) {
- $out = [Type::T_UNARY, '-', $inner, $this->inParens];
-
- return true;
- }
-
- if ($follow_white) {
- $out = [Type::T_KEYWORD, $char];
-
- return true;
- }
-
- $this->seek($s);
- }
-
- // paren
- if ($char === '(' && $this->parenValue($out)) {
- return true;
- }
-
- if ($char === '#') {
- if ($this->interpolation($out) || $this->color($out)) {
- return true;
- }
-
- $this->count++;
-
- if ($this->keyword($keyword)) {
- $out = [Type::T_KEYWORD, '#' . $keyword];
-
- return true;
- }
-
- $this->count--;
- }
-
- if ($this->matchChar('&', true)) {
- $out = [Type::T_SELF];
-
- return true;
- }
-
- if ($char === '$' && $this->variable($out)) {
- return true;
- }
-
- if ($char === 'p' && $this->progid($out)) {
- return true;
- }
-
- if (($char === '"' || $char === "'") && $this->string($out)) {
- return true;
- }
-
- if ($this->unit($out)) {
- return true;
- }
-
- // unicode range with wildcards
- if (
- $this->literal('U+', 2) &&
- $this->match('\?+|([0-9A-F]+(\?+|(-[0-9A-F]+))?)', $m, false)
- ) {
- $unicode = explode('-', $m[0]);
- if (strlen(reset($unicode)) <= 6 && strlen(end($unicode)) <= 6) {
- $out = [Type::T_KEYWORD, 'U+' . $m[0]];
-
- return true;
- }
- $this->count -= strlen($m[0]) + 2;
- }
-
- if ($this->keyword($keyword, false)) {
- if ($this->func($keyword, $out)) {
- return true;
- }
-
- $this->whitespace();
-
- if ($keyword === 'null') {
- $out = [Type::T_NULL];
- } else {
- $out = [Type::T_KEYWORD, $keyword];
- }
-
- return true;
- }
-
- return false;
- }
-
- /**
- * Parse parenthesized value
- *
- * @param array $out
- *
- * @return bool
- */
- protected function parenValue(&$out)
- {
- $s = $this->count;
-
- $inParens = $this->inParens;
-
- if ($this->matchChar('(')) {
- if ($this->matchChar(')')) {
- $out = [Type::T_LIST, '', []];
-
- return true;
- }
-
- $this->inParens = true;
-
- if (
- $this->expression($exp) &&
- $this->matchChar(')')
- ) {
- $out = $exp;
- $this->inParens = $inParens;
-
- return true;
- }
- }
-
- $this->inParens = $inParens;
- $this->seek($s);
-
- return false;
- }
-
- /**
- * Parse "progid:"
- *
- * @param array $out
- *
- * @return bool
- */
- protected function progid(&$out)
- {
- $s = $this->count;
-
- if (
- $this->literal('progid:', 7, false) &&
- $this->openString('(', $fn) &&
- $this->matchChar('(')
- ) {
- $this->openString(')', $args, '(');
-
- if ($this->matchChar(')')) {
- $out = [Type::T_STRING, '', [
- 'progid:', $fn, '(', $args, ')'
- ]];
-
- return true;
- }
- }
-
- $this->seek($s);
-
- return false;
- }
-
- /**
- * Parse function call
- *
- * @param string $name
- * @param array $func
- *
- * @return bool
- */
- protected function func($name, &$func)
- {
- $s = $this->count;
-
- if ($this->matchChar('(')) {
- if ($name === 'alpha' && $this->argumentList($args)) {
- $func = [Type::T_FUNCTION, $name, [Type::T_STRING, '', $args]];
-
- return true;
- }
-
- if ($name !== 'expression' && ! preg_match('/^(-[a-z]+-)?calc$/', $name)) {
- $ss = $this->count;
-
- if (
- $this->argValues($args) &&
- $this->matchChar(')')
- ) {
- if (strtolower($name) === 'var' && \count($args) === 2 && $args[1][0] === Type::T_NULL) {
- $args[1] = [null, [Type::T_STRING, '', [' ']], false];
- }
-
- $func = [Type::T_FUNCTION_CALL, $name, $args];
-
- return true;
- }
-
- $this->seek($ss);
- }
-
- if (
- ($this->openString(')', $str, '(') || true) &&
- $this->matchChar(')')
- ) {
- $args = [];
-
- if (! empty($str)) {
- $args[] = [null, [Type::T_STRING, '', [$str]]];
- }
-
- $func = [Type::T_FUNCTION_CALL, $name, $args];
-
- return true;
- }
- }
-
- $this->seek($s);
-
- return false;
- }
-
- /**
- * Parse function call argument list
- *
- * @param array $out
- *
- * @return bool
- */
- protected function argumentList(&$out)
- {
- $s = $this->count;
- $this->matchChar('(');
-
- $args = [];
-
- while ($this->keyword($var)) {
- if (
- $this->matchChar('=') &&
- $this->expression($exp)
- ) {
- $args[] = [Type::T_STRING, '', [$var . '=']];
- $arg = $exp;
- } else {
- break;
- }
-
- $args[] = $arg;
-
- if (! $this->matchChar(',')) {
- break;
- }
-
- $args[] = [Type::T_STRING, '', [', ']];
- }
-
- if (! $this->matchChar(')') || ! $args) {
- $this->seek($s);
-
- return false;
- }
-
- $out = $args;
-
- return true;
- }
-
- /**
- * Parse mixin/function definition argument list
- *
- * @param array $out
- *
- * @return bool
- */
- protected function argumentDef(&$out)
- {
- $s = $this->count;
- $this->matchChar('(');
-
- $args = [];
-
- while ($this->variable($var)) {
- $arg = [$var[1], null, false];
-
- $ss = $this->count;
-
- if (
- $this->matchChar(':') &&
- $this->genericList($defaultVal, 'expression', '', true)
- ) {
- $arg[1] = $defaultVal;
- } else {
- $this->seek($ss);
- }
-
- $ss = $this->count;
-
- if ($this->literal('...', 3)) {
- $sss = $this->count;
-
- if (! $this->matchChar(')')) {
- throw $this->parseError('... has to be after the final argument');
- }
-
- $arg[2] = true;
-
- $this->seek($sss);
- } else {
- $this->seek($ss);
- }
-
- $args[] = $arg;
-
- if (! $this->matchChar(',')) {
- break;
- }
- }
-
- if (! $this->matchChar(')')) {
- $this->seek($s);
-
- return false;
- }
-
- $out = $args;
-
- return true;
- }
-
- /**
- * Parse map
- *
- * @param array $out
- *
- * @return bool
- */
- protected function map(&$out)
- {
- $s = $this->count;
-
- if (! $this->matchChar('(')) {
- return false;
- }
-
- $keys = [];
- $values = [];
-
- while (
- $this->genericList($key, 'expression', '', true) &&
- $this->matchChar(':') &&
- $this->genericList($value, 'expression', '', true)
- ) {
- $keys[] = $key;
- $values[] = $value;
-
- if (! $this->matchChar(',')) {
- break;
- }
- }
-
- if (! $keys || ! $this->matchChar(')')) {
- $this->seek($s);
-
- return false;
- }
-
- $out = [Type::T_MAP, $keys, $values];
-
- return true;
- }
-
- /**
- * Parse color
- *
- * @param array $out
- *
- * @return bool
- */
- protected function color(&$out)
- {
- $s = $this->count;
-
- if ($this->match('(#([0-9a-f]+)\b)', $m)) {
- if (\in_array(\strlen($m[2]), [3,4,6,8])) {
- $out = [Type::T_KEYWORD, $m[0]];
-
- return true;
- }
-
- $this->seek($s);
-
- return false;
- }
-
- return false;
- }
-
- /**
- * Parse number with unit
- *
- * @param array $unit
- *
- * @return bool
- */
- protected function unit(&$unit)
- {
- $s = $this->count;
-
- if ($this->match('([0-9]*(\.)?[0-9]+)([%a-zA-Z]+)?', $m, false)) {
- if (\strlen($this->buffer) === $this->count || ! ctype_digit($this->buffer[$this->count])) {
- $this->whitespace();
-
- $unit = new Node\Number($m[1], empty($m[3]) ? '' : $m[3]);
-
- return true;
- }
-
- $this->seek($s);
- }
-
- return false;
- }
-
- /**
- * Parse string
- *
- * @param array $out
- * @param bool $keepDelimWithInterpolation
- *
- * @return bool
- */
- protected function string(&$out, $keepDelimWithInterpolation = false)
- {
- $s = $this->count;
-
- if ($this->matchChar('"', false)) {
- $delim = '"';
- } elseif ($this->matchChar("'", false)) {
- $delim = "'";
- } else {
- return false;
- }
-
- $content = [];
- $oldWhite = $this->eatWhiteDefault;
- $this->eatWhiteDefault = false;
- $hasInterpolation = false;
-
- while ($this->matchString($m, $delim)) {
- if ($m[1] !== '') {
- $content[] = $m[1];
- }
-
- if ($m[2] === '#{') {
- $this->count -= \strlen($m[2]);
-
- if ($this->interpolation($inter, false)) {
- $content[] = $inter;
- $hasInterpolation = true;
- } else {
- $this->count += \strlen($m[2]);
- $content[] = '#{'; // ignore it
- }
- } elseif ($m[2] === "\r") {
- $content[] = chr(10);
- // TODO : warning
- # DEPRECATION WARNING on line x, column y of zzz:
- # Unescaped multiline strings are deprecated and will be removed in a future version of Sass.
- # To include a newline in a string, use "\a" or "\a " as in CSS.
- if ($this->matchChar("\n", false)) {
- $content[] = ' ';
- }
- } elseif ($m[2] === '\\') {
- if (
- $this->literal("\r\n", 2, false) ||
- $this->matchChar("\r", false) ||
- $this->matchChar("\n", false) ||
- $this->matchChar("\f", false)
- ) {
- // this is a continuation escaping, to be ignored
- } elseif ($this->matchEscapeCharacter($c)) {
- $content[] = $c;
- } else {
- throw $this->parseError('Unterminated escape sequence');
- }
- } else {
- $this->count -= \strlen($delim);
- break; // delim
- }
- }
-
- $this->eatWhiteDefault = $oldWhite;
-
- if ($this->literal($delim, \strlen($delim))) {
- if ($hasInterpolation && ! $keepDelimWithInterpolation) {
- $delim = '"';
- }
-
- $out = [Type::T_STRING, $delim, $content];
-
- return true;
- }
-
- $this->seek($s);
-
- return false;
- }
-
- /**
- * @param string $out
- * @param bool $inKeywords
- *
- * @return bool
- */
- protected function matchEscapeCharacter(&$out, $inKeywords = false)
- {
- $s = $this->count;
- if ($this->match('[a-f0-9]', $m, false)) {
- $hex = $m[0];
-
- for ($i = 5; $i--;) {
- if ($this->match('[a-f0-9]', $m, false)) {
- $hex .= $m[0];
- } else {
- break;
- }
- }
-
- // CSS allows Unicode escape sequences to be followed by a delimiter space
- // (necessary in some cases for shorter sequences to disambiguate their end)
- $this->matchChar(' ', false);
-
- $value = hexdec($hex);
-
- if (!$inKeywords && ($value == 0 || ($value >= 0xD800 && $value <= 0xDFFF) || $value >= 0x10FFFF)) {
- $out = "\xEF\xBF\xBD"; // "\u{FFFD}" but with a syntax supported on PHP 5
- } elseif ($value < 0x20) {
- $out = Util::mbChr($value);
- } else {
- $out = Util::mbChr($value);
- }
-
- return true;
- }
-
- if ($this->match('.', $m, false)) {
- if ($inKeywords && in_array($m[0], ["'",'"','@','&',' ','\\',':','/','%'])) {
- $this->seek($s);
- return false;
- }
- $out = $m[0];
-
- return true;
- }
-
- return false;
- }
-
- /**
- * Parse keyword or interpolation
- *
- * @param array $out
- * @param bool $restricted
- *
- * @return bool
- */
- protected function mixedKeyword(&$out, $restricted = false)
- {
- $parts = [];
-
- $oldWhite = $this->eatWhiteDefault;
- $this->eatWhiteDefault = false;
-
- for (;;) {
- if ($restricted ? $this->restrictedKeyword($key) : $this->keyword($key)) {
- $parts[] = $key;
- continue;
- }
-
- if ($this->interpolation($inter)) {
- $parts[] = $inter;
- continue;
- }
-
- break;
- }
-
- $this->eatWhiteDefault = $oldWhite;
-
- if (! $parts) {
- return false;
- }
-
- if ($this->eatWhiteDefault) {
- $this->whitespace();
- }
-
- $out = $parts;
-
- return true;
- }
-
- /**
- * Parse an unbounded string stopped by $end
- *
- * @param string $end
- * @param array $out
- * @param string $nestOpen
- * @param string $nestClose
- * @param bool $rtrim
- * @param string $disallow
- *
- * @return bool
- */
- protected function openString($end, &$out, $nestOpen = null, $nestClose = null, $rtrim = true, $disallow = null)
- {
- $oldWhite = $this->eatWhiteDefault;
- $this->eatWhiteDefault = false;
-
- if ($nestOpen && ! $nestClose) {
- $nestClose = $end;
- }
-
- $patt = ($disallow ? '[^' . $this->pregQuote($disallow) . ']' : '.');
- $patt = '(' . $patt . '*?)([\'"]|#\{|'
- . $this->pregQuote($end) . '|'
- . (($nestClose && $nestClose !== $end) ? $this->pregQuote($nestClose) . '|' : '')
- . static::$commentPattern . ')';
-
- $nestingLevel = 0;
-
- $content = [];
-
- while ($this->match($patt, $m, false)) {
- if (isset($m[1]) && $m[1] !== '') {
- $content[] = $m[1];
-
- if ($nestOpen) {
- $nestingLevel += substr_count($m[1], $nestOpen);
- }
- }
-
- $tok = $m[2];
-
- $this->count -= \strlen($tok);
-
- if ($tok === $end && ! $nestingLevel) {
- break;
- }
-
- if ($tok === $nestClose) {
- $nestingLevel--;
- }
-
- if (($tok === "'" || $tok === '"') && $this->string($str, true)) {
- $content[] = $str;
- continue;
- }
-
- if ($tok === '#{' && $this->interpolation($inter)) {
- $content[] = $inter;
- continue;
- }
-
- $content[] = $tok;
- $this->count += \strlen($tok);
- }
-
- $this->eatWhiteDefault = $oldWhite;
-
- if (! $content || $tok !== $end) {
- return false;
- }
-
- // trim the end
- if ($rtrim && \is_string(end($content))) {
- $content[\count($content) - 1] = rtrim(end($content));
- }
-
- $out = [Type::T_STRING, '', $content];
-
- return true;
- }
-
- /**
- * Parser interpolation
- *
- * @param string|array $out
- * @param bool $lookWhite save information about whitespace before and after
- *
- * @return bool
- */
- protected function interpolation(&$out, $lookWhite = true)
- {
- $oldWhite = $this->eatWhiteDefault;
- $allowVars = $this->allowVars;
- $this->allowVars = true;
- $this->eatWhiteDefault = true;
-
- $s = $this->count;
-
- if (
- $this->literal('#{', 2) &&
- $this->valueList($value) &&
- $this->matchChar('}', false)
- ) {
- if ($value === [Type::T_SELF]) {
- $out = $value;
- } else {
- if ($lookWhite) {
- $left = ($s > 0 && preg_match('/\s/', $this->buffer[$s - 1])) ? ' ' : '';
- $right = (
- ! empty($this->buffer[$this->count]) &&
- preg_match('/\s/', $this->buffer[$this->count])
- ) ? ' ' : '';
- } else {
- $left = $right = false;
- }
-
- $out = [Type::T_INTERPOLATE, $value, $left, $right];
- }
-
- $this->eatWhiteDefault = $oldWhite;
- $this->allowVars = $allowVars;
-
- if ($this->eatWhiteDefault) {
- $this->whitespace();
- }
-
- return true;
- }
-
- $this->seek($s);
-
- $this->eatWhiteDefault = $oldWhite;
- $this->allowVars = $allowVars;
-
- return false;
- }
-
- /**
- * Parse property name (as an array of parts or a string)
- *
- * @param array $out
- *
- * @return bool
- */
- protected function propertyName(&$out)
- {
- $parts = [];
-
- $oldWhite = $this->eatWhiteDefault;
- $this->eatWhiteDefault = false;
-
- for (;;) {
- if ($this->interpolation($inter)) {
- $parts[] = $inter;
- continue;
- }
-
- if ($this->keyword($text)) {
- $parts[] = $text;
- continue;
- }
-
- if (! $parts && $this->match('[:.#]', $m, false)) {
- // css hacks
- $parts[] = $m[0];
- continue;
- }
-
- break;
- }
-
- $this->eatWhiteDefault = $oldWhite;
-
- if (! $parts) {
- return false;
- }
-
- // match comment hack
- if (preg_match(static::$whitePattern, $this->buffer, $m, 0, $this->count)) {
- if (! empty($m[0])) {
- $parts[] = $m[0];
- $this->count += \strlen($m[0]);
- }
- }
-
- $this->whitespace(); // get any extra whitespace
-
- $out = [Type::T_STRING, '', $parts];
-
- return true;
- }
-
- /**
- * Parse custom property name (as an array of parts or a string)
- *
- * @param array $out
- *
- * @return bool
- */
- protected function customProperty(&$out)
- {
- $s = $this->count;
-
- if (! $this->literal('--', 2, false)) {
- return false;
- }
-
- $parts = ['--'];
-
- $oldWhite = $this->eatWhiteDefault;
- $this->eatWhiteDefault = false;
-
- for (;;) {
- if ($this->interpolation($inter)) {
- $parts[] = $inter;
- continue;
- }
-
- if ($this->matchChar('&', false)) {
- $parts[] = [Type::T_SELF];
- continue;
- }
-
- if ($this->variable($var)) {
- $parts[] = $var;
- continue;
- }
-
- if ($this->keyword($text)) {
- $parts[] = $text;
- continue;
- }
-
- break;
- }
-
- $this->eatWhiteDefault = $oldWhite;
-
- if (\count($parts) == 1) {
- $this->seek($s);
-
- return false;
- }
-
- $this->whitespace(); // get any extra whitespace
-
- $out = [Type::T_STRING, '', $parts];
-
- return true;
- }
-
- /**
- * Parse comma separated selector list
- *
- * @param array $out
- * @param string|bool $subSelector
- *
- * @return bool
- */
- protected function selectors(&$out, $subSelector = false)
- {
- $s = $this->count;
- $selectors = [];
-
- while ($this->selector($sel, $subSelector)) {
- $selectors[] = $sel;
-
- if (! $this->matchChar(',', true)) {
- break;
- }
-
- while ($this->matchChar(',', true)) {
- ; // ignore extra
- }
- }
-
- if (! $selectors) {
- $this->seek($s);
-
- return false;
- }
-
- $out = $selectors;
-
- return true;
- }
-
- /**
- * Parse whitespace separated selector list
- *
- * @param array $out
- * @param string|bool $subSelector
- *
- * @return bool
- */
- protected function selector(&$out, $subSelector = false)
- {
- $selector = [];
-
- $discardComments = $this->discardComments;
- $this->discardComments = true;
-
- for (;;) {
- $s = $this->count;
-
- if ($this->match('[>+~]+', $m, true)) {
- if (
- $subSelector && \is_string($subSelector) && strpos($subSelector, 'nth-') === 0 &&
- $m[0] === '+' && $this->match("(\d+|n\b)", $counter)
- ) {
- $this->seek($s);
- } else {
- $selector[] = [$m[0]];
- continue;
- }
- }
-
- if ($this->selectorSingle($part, $subSelector)) {
- $selector[] = $part;
- $this->whitespace();
- continue;
- }
-
- break;
- }
-
- $this->discardComments = $discardComments;
-
- if (! $selector) {
- return false;
- }
-
- $out = $selector;
-
- return true;
- }
-
- /**
- * 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 ambigous)
- * - other escaped sequences (multibyte chars or 0-9a-f) are kept in their initial escaped form,
- * normalized to lowercase
- *
- * TODO: this is a fallback solution. Ideally escaped chars in selectors should be encoded as the genuine chars,
- * and escaping added when printing in the Compiler, where/if it's mandatory
- * - but this require a better formal selector representation instead of the array we have now
- *
- * @param string $out
- * @param bool $keepEscapedNumber
- *
- * @return bool
- */
- protected function matchEscapeCharacterInSelector(&$out, $keepEscapedNumber = false)
- {
- $s_escape = $this->count;
- if ($this->match('\\\\', $m)) {
- $out = '\\' . $m[0];
- return true;
- }
-
- if ($this->matchEscapeCharacter($escapedout, true)) {
- if (strlen($escapedout) === 1) {
- if (!preg_match(",\w,", $escapedout)) {
- $out = '\\' . $escapedout;
- return true;
- } elseif (! $keepEscapedNumber || ! \is_numeric($escapedout)) {
- $out = $escapedout;
- return true;
- }
- }
- $escape_sequence = rtrim(substr($this->buffer, $s_escape, $this->count - $s_escape));
- if (strlen($escape_sequence) < 6) {
- $escape_sequence .= ' ';
- }
- $out = '\\' . strtolower($escape_sequence);
- return true;
- }
- if ($this->match('\\S', $m)) {
- $out = '\\' . $m[0];
- return true;
- }
-
-
- return false;
- }
-
- /**
- * Parse the parts that make up a selector
- *
- * {@internal
- * div[yes=no]#something.hello.world:nth-child(-2n+1)%placeholder
- * }}
- *
- * @param array $out
- * @param string|bool $subSelector
- *
- * @return bool
- */
- protected function selectorSingle(&$out, $subSelector = false)
- {
- $oldWhite = $this->eatWhiteDefault;
- $this->eatWhiteDefault = false;
-
- $parts = [];
-
- if ($this->matchChar('*', false)) {
- $parts[] = '*';
- }
-
- for (;;) {
- if (! isset($this->buffer[$this->count])) {
- break;
- }
-
- $s = $this->count;
- $char = $this->buffer[$this->count];
-
- // see if we can stop early
- if ($char === '{' || $char === ',' || $char === ';' || $char === '}' || $char === '@') {
- break;
- }
-
- // parsing a sub selector in () stop with the closing )
- if ($subSelector && $char === ')') {
- break;
- }
-
- //self
- switch ($char) {
- case '&':
- $parts[] = Compiler::$selfSelector;
- $this->count++;
- ! $this->cssOnly || $this->assertPlainCssValid(false, $s);
- continue 2;
-
- case '.':
- $parts[] = '.';
- $this->count++;
- continue 2;
-
- case '|':
- $parts[] = '|';
- $this->count++;
- continue 2;
- }
-
- // handling of escaping in selectors : get the escaped char
- if ($char === '\\') {
- $this->count++;
- if ($this->matchEscapeCharacterInSelector($escaped, true)) {
- $parts[] = $escaped;
- continue;
- }
- $this->count--;
- }
-
- if ($char === '%') {
- $this->count++;
-
- if ($this->placeholder($placeholder)) {
- $parts[] = '%';
- $parts[] = $placeholder;
- ! $this->cssOnly || $this->assertPlainCssValid(false, $s);
- continue;
- }
-
- break;
- }
-
- if ($char === '#') {
- if ($this->interpolation($inter)) {
- $parts[] = $inter;
- ! $this->cssOnly || $this->assertPlainCssValid(false, $s);
- continue;
- }
-
- $parts[] = '#';
- $this->count++;
- continue;
- }
-
- // a pseudo selector
- if ($char === ':') {
- if ($this->buffer[$this->count + 1] === ':') {
- $this->count += 2;
- $part = '::';
- } else {
- $this->count++;
- $part = ':';
- }
-
- if ($this->mixedKeyword($nameParts, true)) {
- $parts[] = $part;
-
- foreach ($nameParts as $sub) {
- $parts[] = $sub;
- }
-
- $ss = $this->count;
-
- if (
- $nameParts === ['not'] ||
- $nameParts === ['is'] ||
- $nameParts === ['has'] ||
- $nameParts === ['where'] ||
- $nameParts === ['slotted'] ||
- $nameParts === ['nth-child'] ||
- $nameParts === ['nth-last-child'] ||
- $nameParts === ['nth-of-type'] ||
- $nameParts === ['nth-last-of-type']
- ) {
- if (
- $this->matchChar('(', true) &&
- ($this->selectors($subs, reset($nameParts)) || true) &&
- $this->matchChar(')')
- ) {
- $parts[] = '(';
-
- while ($sub = array_shift($subs)) {
- while ($ps = array_shift($sub)) {
- foreach ($ps as &$p) {
- $parts[] = $p;
- }
-
- if (\count($sub) && reset($sub)) {
- $parts[] = ' ';
- }
- }
-
- if (\count($subs) && reset($subs)) {
- $parts[] = ', ';
- }
- }
-
- $parts[] = ')';
- } else {
- $this->seek($ss);
- }
- } elseif (
- $this->matchChar('(', true) &&
- ($this->openString(')', $str, '(') || true) &&
- $this->matchChar(')')
- ) {
- $parts[] = '(';
-
- if (! empty($str)) {
- $parts[] = $str;
- }
-
- $parts[] = ')';
- } else {
- $this->seek($ss);
- }
-
- continue;
- }
- }
-
- $this->seek($s);
-
- // 2n+1
- if ($subSelector && \is_string($subSelector) && strpos($subSelector, 'nth-') === 0) {
- if ($this->match("(\s*(\+\s*|\-\s*)?(\d+|n|\d+n))+", $counter)) {
- $parts[] = $counter[0];
- //$parts[] = str_replace(' ', '', $counter[0]);
- continue;
- }
- }
-
- $this->seek($s);
-
- // attribute selector
- if (
- $char === '[' &&
- $this->matchChar('[') &&
- ($this->openString(']', $str, '[') || true) &&
- $this->matchChar(']')
- ) {
- $parts[] = '[';
-
- if (! empty($str)) {
- $parts[] = $str;
- }
-
- $parts[] = ']';
- continue;
- }
-
- $this->seek($s);
-
- // for keyframes
- if ($this->unit($unit)) {
- $parts[] = $unit;
- continue;
- }
-
- if ($this->restrictedKeyword($name, false, true)) {
- $parts[] = $name;
- continue;
- }
-
- break;
- }
-
- $this->eatWhiteDefault = $oldWhite;
-
- if (! $parts) {
- return false;
- }
-
- $out = $parts;
-
- return true;
- }
-
- /**
- * Parse a variable
- *
- * @param array $out
- *
- * @return bool
- */
- protected function variable(&$out)
- {
- $s = $this->count;
-
- if (
- $this->matchChar('$', false) &&
- $this->keyword($name)
- ) {
- if ($this->allowVars) {
- $out = [Type::T_VARIABLE, $name];
- } else {
- $out = [Type::T_KEYWORD, '$' . $name];
- }
-
- return true;
- }
-
- $this->seek($s);
-
- return false;
- }
-
- /**
- * Parse a keyword
- *
- * @param string $word
- * @param bool $eatWhitespace
- * @param bool $inSelector
- *
- * @return bool
- */
- protected function keyword(&$word, $eatWhitespace = null, $inSelector = false)
- {
- $s = $this->count;
- $match = $this->match(
- $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
- );
-
- if ($match) {
- $word = $m[1];
-
- // handling of escaping in keyword : get the escaped char
- if (strpos($word, '\\') !== false) {
- $send = $this->count;
- $escapedWord = [];
- $this->seek($s);
- $previousEscape = false;
- while ($this->count < $send) {
- $char = $this->buffer[$this->count];
- $this->count++;
- if (
- $this->count < $send
- && $char === '\\'
- && !$previousEscape
- && (
- $inSelector ?
- $this->matchEscapeCharacterInSelector($out)
- :
- $this->matchEscapeCharacter($out, true)
- )
- ) {
- $escapedWord[] = $out;
- } else {
- if ($previousEscape) {
- $previousEscape = false;
- } elseif ($char === '\\') {
- $previousEscape = true;
- }
- $escapedWord[] = $char;
- }
- }
-
- $word = implode('', $escapedWord);
- }
-
- if (is_null($eatWhitespace) ? $this->eatWhiteDefault : $eatWhitespace) {
- $this->whitespace();
- }
-
- return true;
- }
-
- return false;
- }
-
- /**
- * Parse a keyword that should not start with a number
- *
- * @param string $word
- * @param bool $eatWhitespace
- * @param bool $inSelector
- *
- * @return bool
- */
- protected function restrictedKeyword(&$word, $eatWhitespace = null, $inSelector = false)
- {
- $s = $this->count;
-
- if ($this->keyword($word, $eatWhitespace, $inSelector) && (\ord($word[0]) > 57 || \ord($word[0]) < 48)) {
- return true;
- }
-
- $this->seek($s);
-
- return false;
- }
-
- /**
- * Parse a placeholder
- *
- * @param string|array $placeholder
- *
- * @return bool
- */
- protected function placeholder(&$placeholder)
- {
- $match = $this->match(
- $this->utf8
- ? '([\pL\w\-_]+)'
- : '([\w\-_]+)',
- $m
- );
-
- if ($match) {
- $placeholder = $m[1];
-
- return true;
- }
-
- if ($this->interpolation($placeholder)) {
- return true;
- }
-
- return false;
- }
-
- /**
- * Parse a url
- *
- * @param array $out
- *
- * @return bool
- */
- protected function url(&$out)
- {
- if ($this->literal('url(', 4)) {
- $s = $this->count;
-
- if (
- ($this->string($out) || $this->spaceList($out)) &&
- $this->matchChar(')')
- ) {
- $out = [Type::T_STRING, '', ['url(', $out, ')']];
-
- return true;
- }
-
- $this->seek($s);
-
- if (
- $this->openString(')', $out) &&
- $this->matchChar(')')
- ) {
- $out = [Type::T_STRING, '', ['url(', $out, ')']];
-
- return true;
- }
- }
-
- return false;
- }
-
- /**
- * Consume an end of statement delimiter
- * @param bool $eatWhitespace
- *
- * @return bool
- */
- protected function end($eatWhitespace = null)
- {
- if ($this->matchChar(';', $eatWhitespace)) {
- return true;
- }
-
- if ($this->count === \strlen($this->buffer) || $this->buffer[$this->count] === '}') {
- // if there is end of file or a closing block next then we don't need a ;
- return true;
- }
-
- return false;
- }
-
- /**
- * Strip assignment flag from the list
- *
- * @param array $value
- *
- * @return string[]
- */
- protected function stripAssignmentFlags(&$value)
- {
- $flags = [];
-
- for ($token = &$value; $token[0] === Type::T_LIST && ($s = \count($token[2])); $token = &$lastNode) {
- $lastNode = &$token[2][$s - 1];
-
- while ($lastNode[0] === Type::T_KEYWORD && \in_array($lastNode[1], ['!default', '!global'])) {
- array_pop($token[2]);
-
- $node = end($token[2]);
- $token = $this->flattenList($token);
- $flags[] = $lastNode[1];
- $lastNode = $node;
- }
- }
-
- return $flags;
- }
-
- /**
- * Strip optional flag from selector list
- *
- * @param array $selectors
- *
- * @return bool
- */
- protected function stripOptionalFlag(&$selectors)
- {
- $optional = false;
- $selector = end($selectors);
- $part = end($selector);
-
- if ($part === ['!optional']) {
- array_pop($selectors[\count($selectors) - 1]);
-
- $optional = true;
- }
-
- return $optional;
- }
-
- /**
- * Turn list of length 1 into value type
- *
- * @param array $value
- *
- * @return array
- */
- protected function flattenList($value)
- {
- if ($value[0] === Type::T_LIST && \count($value[2]) === 1) {
- return $this->flattenList($value[2][0]);
- }
-
- return $value;
- }
-
- /**
- * Quote regular expression
- *
- * @param string $what
- *
- * @return string
- */
- private function pregQuote($what)
- {
- return preg_quote($what, '/');
- }
-
- /**
- * Extract line numbers from buffer
- *
- * @param string $buffer
- *
- * @return void
- */
- private function extractLineNumbers($buffer)
- {
- $this->sourcePositions = [0 => 0];
- $prev = 0;
-
- while (($pos = strpos($buffer, "\n", $prev)) !== false) {
- $this->sourcePositions[] = $pos;
- $prev = $pos + 1;
- }
-
- $this->sourcePositions[] = \strlen($buffer);
-
- if (substr($buffer, -1) !== "\n") {
- $this->sourcePositions[] = \strlen($buffer) + 1;
- }
- }
-
- /**
- * Get source line number and column (given character position in the buffer)
- *
- * @param int $pos
- *
- * @return array
- * @phpstan-return array{int, int}
- */
- private function getSourcePosition($pos)
- {
- $low = 0;
- $high = \count($this->sourcePositions);
-
- while ($low < $high) {
- $mid = (int) (($high + $low) / 2);
-
- if ($pos < $this->sourcePositions[$mid]) {
- $high = $mid - 1;
- continue;
- }
-
- if ($pos >= $this->sourcePositions[$mid + 1]) {
- $low = $mid + 1;
- continue;
- }
-
- return [$mid + 1, $pos - $this->sourcePositions[$mid]];
- }
-
- return [$low + 1, $pos - $this->sourcePositions[$low]];
- }
-
- /**
- * Save internal encoding of mbstring
- *
- * When mbstring.func_overload is used to replace the standard PHP string functions,
- * this method configures the internal encoding to a single-byte one so that the
- * behavior matches the normal behavior of PHP string functions while using the parser.
- * The existing internal encoding is saved and will be restored when calling {@see restoreEncoding}.
- *
- * If mbstring.func_overload is not used (or does not override string functions), this method is a no-op.
- *
- * @return void
- */
- private function saveEncoding()
- {
- if (\PHP_VERSION_ID < 80000 && \extension_loaded('mbstring') && (2 & (int) ini_get('mbstring.func_overload')) > 0) {
- $this->encoding = mb_internal_encoding();
-
- mb_internal_encoding('iso-8859-1');
- }
- }
-
- /**
- * Restore internal encoding
- *
- * @return void
- */
- private function restoreEncoding()
- {
- if (\extension_loaded('mbstring') && $this->encoding) {
- mb_internal_encoding($this->encoding);
- }
- }
-}
diff --git a/vendor/scssphp/scssphp/src/Parser/AtRootQueryParser.php b/vendor/scssphp/scssphp/src/Parser/AtRootQueryParser.php
new file mode 100644
index 000000000..fec97c364
--- /dev/null
+++ b/vendor/scssphp/scssphp/src/Parser/AtRootQueryParser.php
@@ -0,0 +1,54 @@
+<?php
+
+/**
+ * SCSSPHP
+ *
+ * @copyright 2012-2020 Leaf Corcoran
+ *
+ * @license http://opensource.org/licenses/MIT MIT
+ *
+ * @link http://scssphp.github.io/scssphp
+ */
+
+namespace ScssPhp\ScssPhp\Parser;
+
+use ScssPhp\ScssPhp\Ast\Sass\AtRootQuery;
+use ScssPhp\ScssPhp\Exception\SassFormatException;
+
+/**
+ * A parser for `@at-root` queries.
+ *
+ * @internal
+ */
+final class AtRootQueryParser extends Parser
+{
+ /**
+ * @throws SassFormatException
+ */
+ public function parse(): AtRootQuery
+ {
+ return $this->wrapSpanFormatException(function () {
+ $this->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);
+ });
+ }
+}
diff --git a/vendor/scssphp/scssphp/src/Parser/CssParser.php b/vendor/scssphp/scssphp/src/Parser/CssParser.php
new file mode 100644
index 000000000..ae81615fe
--- /dev/null
+++ b/vendor/scssphp/scssphp/src/Parser/CssParser.php
@@ -0,0 +1,187 @@
+<?php
+
+/**
+ * SCSSPHP
+ *
+ * @copyright 2012-2020 Leaf Corcoran
+ *
+ * @license http://opensource.org/licenses/MIT MIT
+ *
+ * @link http://scssphp.github.io/scssphp
+ */
+
+namespace ScssPhp\ScssPhp\Parser;
+
+use ScssPhp\ScssPhp\Ast\Sass\ArgumentInvocation;
+use ScssPhp\ScssPhp\Ast\Sass\Expression;
+use ScssPhp\ScssPhp\Ast\Sass\Expression\FunctionExpression;
+use ScssPhp\ScssPhp\Ast\Sass\Expression\ParenthesizedExpression;
+use ScssPhp\ScssPhp\Ast\Sass\Expression\StringExpression;
+use ScssPhp\ScssPhp\Ast\Sass\Import\StaticImport;
+use ScssPhp\ScssPhp\Ast\Sass\Interpolation;
+use ScssPhp\ScssPhp\Ast\Sass\Statement;
+use ScssPhp\ScssPhp\Ast\Sass\Statement\ImportRule;
+use ScssPhp\ScssPhp\Function\FunctionRegistry;
+
+/**
+ * A parser for imported CSS files.
+ *
+ * @internal
+ */
+final class CssParser extends ScssParser
+{
+ /**
+ * Sass global functions which are shadowing a CSS function are allowed in CSS files.
+ */
+ private const CSS_ALLOWED_FUNCTIONS = [
+ 'rgb' => true, 'rgba' => true, 'hsl' => true, 'hsla' => true, 'grayscale' => true,
+ 'invert' => true, 'alpha' => true, 'opacity' => true, 'saturate' => true,
+ 'min' => true, 'max' => true, 'round' => true, 'abs' => true,
+ ];
+
+ protected function isPlainCss(): bool
+ {
+ return true;
+ }
+
+ protected function silentComment(): bool
+ {
+ if ($this->inExpression()) {
+ return false;
+ }
+
+ $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();
+
+ return match ($name->getAsPlain()) {
+ 'at-root',
+ 'content',
+ 'debug',
+ 'each',
+ 'error',
+ 'extend',
+ 'for',
+ 'function',
+ 'if',
+ 'include',
+ 'mixin',
+ 'return',
+ 'warn',
+ 'while' => $this->forbiddenAtRule($start),
+ 'import' => $this->cssImportRule($start),
+ 'media' => $this->mediaRule($start),
+ '-moz-document' => $this->mozDocumentRule($start, $name),
+ 'supports' => $this->supportsRule($start),
+ default => $this->unknownAtRule($start, $name),
+ };
+ }
+
+ private function forbiddenAtRule(int $start): never
+ {
+ $this->almostAnyValue();
+ $this->error("This at-rule isn't allowed in plain CSS.", $this->scanner->spanFrom($start));
+ }
+
+ 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 parentheses(): Expression
+ {
+ // Expressions are only allowed within calculations, but we verify this at
+ // evaluation time.
+ $start = $this->scanner->getPosition();
+ $this->scanner->expectChar('(');
+ $this->whitespace();
+ $expression = $this->expressionUntilComma();
+ $this->scanner->expectChar(')');
+
+ return new ParenthesizedExpression($expression, $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();
+ // `namespacedExpression()` is just here to throw a clearer error.
+ if ($this->scanner->scanChar('.')) {
+ return $this->namespacedExpression($plain, $start);
+ }
+ 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]) && FunctionRegistry::isBuiltinFunction($plain))) {
+ $this->error("This function isn't allowed in plain CSS.", $this->scanner->spanFrom($start));
+ }
+
+ return new FunctionExpression(
+ $plain,
+ 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/vendor/scssphp/scssphp/src/Parser/FormatException.php b/vendor/scssphp/scssphp/src/Parser/FormatException.php
new file mode 100644
index 000000000..e065feb86
--- /dev/null
+++ b/vendor/scssphp/scssphp/src/Parser/FormatException.php
@@ -0,0 +1,36 @@
+<?php
+
+/**
+ * SCSSPHP
+ *
+ * @copyright 2012-2020 Leaf Corcoran
+ *
+ * @license http://opensource.org/licenses/MIT MIT
+ *
+ * @link http://scssphp.github.io/scssphp
+ */
+
+namespace ScssPhp\ScssPhp\Parser;
+
+use JiriPudil\SealedClasses\Sealed;
+use SourceSpan\FileSpan;
+
+/**
+ * @internal
+ */
+#[Sealed([MultiSourceFormatException::class])]
+class FormatException extends \Exception
+{
+ private readonly FileSpan $span;
+
+ public function __construct(string $message, FileSpan $span, ?\Throwable $previous = null)
+ {
+ $this->span = $span;
+ parent::__construct($message, 0, $previous);
+ }
+
+ public function getSpan(): FileSpan
+ {
+ return $this->span;
+ }
+}
diff --git a/vendor/scssphp/scssphp/src/Parser/InterpolationBuffer.php b/vendor/scssphp/scssphp/src/Parser/InterpolationBuffer.php
new file mode 100644
index 000000000..a22d4892c
--- /dev/null
+++ b/vendor/scssphp/scssphp/src/Parser/InterpolationBuffer.php
@@ -0,0 +1,106 @@
+<?php
+
+/**
+ * SCSSPHP
+ *
+ * @copyright 2012-2020 Leaf Corcoran
+ *
+ * @license http://opensource.org/licenses/MIT MIT
+ *
+ * @link http://scssphp.github.io/scssphp
+ */
+
+namespace ScssPhp\ScssPhp\Parser;
+
+use ScssPhp\ScssPhp\Ast\Sass\Expression;
+use ScssPhp\ScssPhp\Ast\Sass\Interpolation;
+use SourceSpan\FileSpan;
+
+/**
+ * A buffer that iteratively builds up an {@see Interpolation}.
+ *
+ * @internal
+ */
+final class InterpolationBuffer
+{
+ private string $text = '';
+
+ /**
+ * @var list<string|Expression>
+ */
+ private array $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/vendor/scssphp/scssphp/src/Parser/InterpolationMap.php b/vendor/scssphp/scssphp/src/Parser/InterpolationMap.php
new file mode 100644
index 000000000..b38d8b1d2
--- /dev/null
+++ b/vendor/scssphp/scssphp/src/Parser/InterpolationMap.php
@@ -0,0 +1,220 @@
+<?php
+
+/**
+ * SCSSPHP
+ *
+ * @copyright 2012-2020 Leaf Corcoran
+ *
+ * @license http://opensource.org/licenses/MIT MIT
+ *
+ * @link http://scssphp.github.io/scssphp
+ */
+
+namespace ScssPhp\ScssPhp\Parser;
+
+use ScssPhp\ScssPhp\Ast\Sass\Expression;
+use ScssPhp\ScssPhp\Ast\Sass\Interpolation;
+use ScssPhp\ScssPhp\Util\Character;
+use ScssPhp\ScssPhp\Util\IterableUtil;
+use SourceSpan\FileLocation;
+use SourceSpan\FileSpan;
+use SourceSpan\SourceLocation;
+
+/**
+ * A class that can map locations in a string generated from an {@see Interpolation}
+ * to the original source code in the interpolation.
+ *
+ * @internal
+ */
+final class InterpolationMap
+{
+ private readonly Interpolation $interpolation;
+
+ /**
+ * Locations in the generated string.
+ *
+ * Each of these indicates the location in the generated string that
+ * corresponds to the end of the component at the same index of
+ * {@see $interpolation->getContents()}. Its length is always one less than
+ * {@see $interpolation->getContents()} because the last element always ends the string.
+ *
+ * @var list<SourceLocation>
+ */
+ private readonly array $targetLocations;
+
+ /**
+ * @param list<SourceLocation> $targetLocations
+ */
+ public function __construct(Interpolation $interpolation, array $targetLocations)
+ {
+ $this->interpolation = $interpolation;
+ $this->targetLocations = $targetLocations;
+
+ $expectedLocations = max(0, \count($interpolation->getContents()) - 1);
+ if (\count($targetLocations) !== $expectedLocations) {
+ $interpolationParts = \count($interpolation->getContents());
+ throw new \InvalidArgumentException("InterpolationMap must have $expectedLocations targetLocations if the interpolation has $interpolationParts components.");
+ }
+ }
+
+ public function mapException(FormatException $error): FormatException
+ {
+ $source = $this->mapSpan($error->getSpan());
+ $startIndex = $this->indexInContents($source->getStart());
+ $endIndex = $this->indexInContents($source->getEnd());
+
+ if (!IterableUtil::any(array_slice($this->interpolation->getContents(), $startIndex, $endIndex - $startIndex + 1), fn ($content) => $content instanceof Expression)) {
+ return new FormatException($error->getMessage(), $source, $error);
+ }
+
+ return new MultiSourceFormatException($error->getMessage(), $source, '', ['error in interpolated output' => $error->getSpan()], $error);
+ }
+
+ public function mapSpan(FileSpan $target): FileSpan
+ {
+ $start = $this->mapLocation($target->getStart());
+ $end = $this->mapLocation($target->getEnd());
+
+ if ($start instanceof FileSpan) {
+ if ($end instanceof FileSpan) {
+ return $start->expand($end);
+ }
+
+ return $this->interpolation->getSpan()->getFile()->span($this->expandInterpolationSpanLeft($start->getStart()), $end->getOffset());
+ }
+
+ if ($end instanceof FileSpan) {
+ return $this->interpolation->getSpan()->getFile()->span($start->getOffset(), $this->expandInterpolationSpanRight($end->getEnd()));
+ }
+
+ return $this->interpolation->getSpan()->getFile()->span($start->getOffset(), $end->getOffset());
+ }
+
+ /**
+ * @return FileSpan|FileLocation
+ */
+ private function mapLocation(SourceLocation $target): object
+ {
+ $index = $this->indexInContents($target);
+
+ $components = $this->interpolation->getContents();
+
+ if ($components[$index] instanceof Expression) {
+ return $components[$index]->getSpan();
+ }
+
+ if ($index === 0) {
+ $previousLocation = $this->interpolation->getSpan()->getStart();
+ } else {
+ $previousComponent = $components[$index - 1];
+ \assert($previousComponent instanceof Expression);
+ $previousLocation = $this->interpolation->getSpan()->getFile()->location($this->expandInterpolationSpanRight($previousComponent->getSpan()->getEnd()));
+ }
+
+ $offsetInString = $target->getOffset() - ($index === 0 ? 0 : $this->targetLocations[$index - 1]->getOffset());
+
+ return $previousLocation->getFile()->location($previousLocation->getOffset() + $offsetInString);
+ }
+
+ private function indexInContents(SourceLocation $target): int
+ {
+ foreach ($this->targetLocations as $i => $location) {
+ if ($target->getOffset() < $location->getOffset()) {
+ return $i;
+ }
+ }
+
+ return \count($this->interpolation->getContents()) - 1;
+ }
+
+ /**
+ * Given the start of a {@see FileSpan} covering an interpolated expression, returns
+ * the offset of the interpolation's opening `#`.
+ *
+ * Note that this can be tricked by a `#{` that appears within a single-line
+ * comment before the expression, but since it's only used for error
+ * reporting that's probably fine.
+ */
+ private function expandInterpolationSpanLeft(FileLocation $start): int
+ {
+ $source = $start->getFile()->getString();
+ $i = $start->getOffset() - 1;
+
+ while ($i >= 0) {
+ $prev = $source[$i--];
+
+ if ($prev === '{') {
+ if ($source[$i] === '#') {
+ break;
+ }
+ } elseif ($prev === '/') {
+ $second = $source[$i--];
+
+ if ($second === '*') {
+ while ($i >= 0) {
+ $char = $source[$i--];
+
+ if ($char !== '*') {
+ continue;
+ }
+
+ do {
+ $char = $source[$i--];
+ } while ($char === '*' && $i >= 0);
+
+ if ($char === '/') {
+ break;
+ }
+ }
+ }
+ }
+ }
+
+ return $i;
+ }
+
+ /**
+ * Given the end of a {@see FileSpan} covering an interpolated expression, returns
+ * the offset of the interpolation's closing `}`.
+ */
+ private function expandInterpolationSpanRight(FileLocation $end): int
+ {
+ $source = $end->getFile()->getString();
+ $i = $end->getOffset();
+
+ while ($i < \strlen($source)) {
+ $next = $source[$i++];
+
+ if ($next === '}') {
+ break;
+ }
+
+ if ($next === '/') {
+ $second = $source[$i++];
+ if ($second === '/') {
+ while (!Character::isNewline($source[$i++] ?? null)) {
+ // Move forward
+ }
+ } elseif ($second === '*') {
+ while (true) {
+ $char = $source[$i++] ?? null;
+
+ if ($char !== '*') {
+ continue;
+ }
+
+ do {
+ $char = $source[$i++] ?? null;
+ } while ($char === '*');
+
+ if ($char === '/') {
+ break;
+ }
+ }
+ }
+ }
+ }
+
+ return $i;
+ }
+}
diff --git a/vendor/scssphp/scssphp/src/Parser/KeyframeSelectorParser.php b/vendor/scssphp/scssphp/src/Parser/KeyframeSelectorParser.php
new file mode 100644
index 000000000..0a89bf0a1
--- /dev/null
+++ b/vendor/scssphp/scssphp/src/Parser/KeyframeSelectorParser.php
@@ -0,0 +1,103 @@
+<?php
+
+/**
+ * SCSSPHP
+ *
+ * @copyright 2012-2020 Leaf Corcoran
+ *
+ * @license http://opensource.org/licenses/MIT MIT
+ *
+ * @link http://scssphp.github.io/scssphp
+ */
+
+namespace ScssPhp\ScssPhp\Parser;
+
+use ScssPhp\ScssPhp\Exception\SassFormatException;
+use ScssPhp\ScssPhp\Util\Character;
+
+/**
+ * A parser for `@keyframes` block selectors.
+ *
+ * @internal
+ */
+final class KeyframeSelectorParser extends Parser
+{
+ /**
+ * @return list<string>
+ *
+ * @throws SassFormatException
+ */
+ public function parse(): array
+ {
+ return $this->wrapSpanFormatException(function () {
+ $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;
+ });
+ }
+
+ 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/vendor/scssphp/scssphp/src/Parser/LineScanner.php b/vendor/scssphp/scssphp/src/Parser/LineScanner.php
new file mode 100644
index 000000000..009ecdb96
--- /dev/null
+++ b/vendor/scssphp/scssphp/src/Parser/LineScanner.php
@@ -0,0 +1,181 @@
+<?php
+
+/**
+ * SCSSPHP
+ *
+ * @copyright 2012-2020 Leaf Corcoran
+ *
+ * @license http://opensource.org/licenses/MIT MIT
+ *
+ * @link http://scssphp.github.io/scssphp
+ */
+
+namespace ScssPhp\ScssPhp\Parser;
+
+/**
+ * A subclass of {@see StringScanner} that tracks line and column information.
+ *
+ * @internal
+ */
+final class LineScanner extends StringScanner
+{
+ /**
+ * @var int
+ */
+ private int $line = 0;
+
+ /**
+ * @var int
+ */
+ private int $column = 0;
+
+ public function getLine(): int
+ {
+ return $this->line;
+ }
+
+ public function getColumn(): int
+ {
+ return $this->column;
+ }
+
+ /**
+ * Whether the current position is between a CR character and an LF
+ * character.
+ */
+ private function betweenCRLF(): bool
+ {
+ return $this->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 {
+ $lastCrlfPosition = strrpos($this->getString(), "\r\n", $newPosition);
+ if ($lastCrlfPosition === false) {
+ $lastCrlfPosition = -1;
+ }
+ $lastLfPosition = strrpos($this->getString(), "\n", $newPosition);
+ if ($lastLfPosition === false) {
+ $lastLfPosition = -1;
+ }
+ $lastNewLinePosition = max($lastCrlfPosition, $lastLfPosition);
+ $this->column = $newPosition - $lastNewLinePosition - 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;
+ }
+
+ /**
+ * @return list<array{string, int}>
+ */
+ 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/vendor/scssphp/scssphp/src/Parser/MediaQueryParser.php b/vendor/scssphp/scssphp/src/Parser/MediaQueryParser.php
new file mode 100644
index 000000000..4abd13c8c
--- /dev/null
+++ b/vendor/scssphp/scssphp/src/Parser/MediaQueryParser.php
@@ -0,0 +1,153 @@
+<?php
+
+/**
+ * SCSSPHP
+ *
+ * @copyright 2012-2020 Leaf Corcoran
+ *
+ * @license http://opensource.org/licenses/MIT MIT
+ *
+ * @link http://scssphp.github.io/scssphp
+ */
+
+namespace ScssPhp\ScssPhp\Parser;
+
+use ScssPhp\ScssPhp\Ast\Css\CssMediaQuery;
+use ScssPhp\ScssPhp\Exception\SassFormatException;
+
+/**
+ * A parser for `@media` queries.
+ *
+ * @internal
+ */
+final class MediaQueryParser extends Parser
+{
+ /**
+ * @return list<CssMediaQuery>
+ *
+ * @throws SassFormatException when parsing fails
+ */
+ public function parse(): array
+ {
+ return $this->wrapSpanFormatException(function () {
+ $queries = [];
+
+ do {
+ $this->whitespace();
+ $queries[] = $this->mediaQuery();
+ $this->whitespace();
+ } while ($this->scanner->scanChar(','));
+ $this->scanner->expectDone();
+
+ return $queries;
+ });
+ }
+
+ /**
+ * 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')) {
+ $this->expectWhitespace();
+ // 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 `<media-in-parens>` expressions separated by
+ * $operator and returns them.
+ *
+ * @return list<string>
+ */
+ private function mediaLogicSequence(string $operator): array
+ {
+ $result = [];
+ while (true) {
+ $result[] = $this->mediaInParens();
+ $this->whitespace();
+
+ if (!$this->scanIdentifier($operator)) {
+ return $result;
+ }
+ $this->expectWhitespace();
+ }
+ }
+
+ /**
+ * Consumes a `<media-in-parens>` 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/vendor/scssphp/scssphp/src/Parser/MultiSourceFormatException.php b/vendor/scssphp/scssphp/src/Parser/MultiSourceFormatException.php
new file mode 100644
index 000000000..e226029af
--- /dev/null
+++ b/vendor/scssphp/scssphp/src/Parser/MultiSourceFormatException.php
@@ -0,0 +1,40 @@
+<?php
+
+/**
+ * SCSSPHP
+ *
+ * @copyright 2012-2020 Leaf Corcoran
+ *
+ * @license http://opensource.org/licenses/MIT MIT
+ *
+ * @link http://scssphp.github.io/scssphp
+ */
+
+namespace ScssPhp\ScssPhp\Parser;
+
+use SourceSpan\FileSpan;
+
+class MultiSourceFormatException extends FormatException
+{
+ /**
+ * {@see MultiSpanSassException::$primaryLabel}
+ */
+ public readonly string $primaryLabel;
+ /**
+ * {@see MultiSpanSassException::$secondarySpans}
+ *
+ * @var array<string, FileSpan>
+ */
+ public readonly array $secondarySpans;
+
+ /**
+ * @param array<string, FileSpan> $secondarySpans
+ */
+ public function __construct(string $message, FileSpan $span, string $primaryLabel, array $secondarySpans, ?\Throwable $previous = null)
+ {
+ $this->primaryLabel = $primaryLabel;
+ $this->secondarySpans = $secondarySpans;
+
+ parent::__construct($message, $span, $previous);
+ }
+}
diff --git a/vendor/scssphp/scssphp/src/Parser/Parser.php b/vendor/scssphp/scssphp/src/Parser/Parser.php
new file mode 100644
index 000000000..61c52100a
--- /dev/null
+++ b/vendor/scssphp/scssphp/src/Parser/Parser.php
@@ -0,0 +1,1021 @@
+<?php
+
+/**
+ * SCSSPHP
+ *
+ * @copyright 2012-2020 Leaf Corcoran
+ *
+ * @license http://opensource.org/licenses/MIT MIT
+ *
+ * @link http://scssphp.github.io/scssphp
+ */
+
+namespace ScssPhp\ScssPhp\Parser;
+
+use League\Uri\Contracts\UriInterface;
+use ScssPhp\ScssPhp\Exception\MultiSpanSassFormatException;
+use ScssPhp\ScssPhp\Exception\SassFormatException;
+use ScssPhp\ScssPhp\Exception\SimpleSassFormatException;
+use ScssPhp\ScssPhp\Logger\LoggerInterface;
+use ScssPhp\ScssPhp\Logger\QuietLogger;
+use ScssPhp\ScssPhp\SourceSpan\LazyFileSpan;
+use ScssPhp\ScssPhp\Util\Character;
+use ScssPhp\ScssPhp\Util\ParserUtil;
+use SourceSpan\FileLocation;
+use SourceSpan\FileSpan;
+
+/**
+ * @internal
+ */
+class Parser
+{
+ protected readonly StringScanner $scanner;
+
+ protected readonly LoggerInterface $logger;
+
+ /**
+ * A map used to map source spans in the text being parsed back to their
+ * original locations in the source file, if this isn't being parsed directly
+ * from source.
+ */
+ private readonly ?InterpolationMap $interpolationMap;
+
+ /**
+ * Parses $text as a CSS identifier and returns the result.
+ *
+ * @throws SassFormatException if parsing fails.
+ */
+ public static function parseIdentifier(string $text, ?LoggerInterface $logger = null): string
+ {
+ return (new Parser($text, $logger))->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) {
+ return false;
+ }
+ }
+
+ public function __construct(string $contents, ?LoggerInterface $logger = null, ?UriInterface $sourceUrl = null, ?InterpolationMap $interpolationMap = null)
+ {
+ $this->scanner = new StringScanner($contents, $sourceUrl);
+ $this->logger = $logger ?? new QuietLogger();
+ $this->interpolationMap = $interpolationMap;
+ }
+
+ /**
+ * @throws SassFormatException
+ */
+ private function doParseIdentifier(): string
+ {
+ return $this->wrapSpanFormatException(function () {
+ $result = $this->identifier();
+ $this->scanner->expectDone();
+
+ return $result;
+ });
+ }
+
+ /**
+ * 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 === '/') {
+ return $this->silentComment();
+ }
+
+ 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 single silent (Sass-style) comment, not including
+ * the trailing newline.
+ *
+ * Returns whether the comment was consumed.
+ */
+ protected function silentComment(): bool
+ {
+ $this->scanner->expect('//');
+
+ while (!$this->scanner->isDone() && !Character::isNewline($this->scanner->peekChar())) {
+ $this->scanner->readChar();
+ }
+
+ return true;
+ }
+
+ /**
+ * 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::isWhitespace(...));
+ $valueText = mb_chr($value, 'UTF-8');
+ } else {
+ $valueText = $this->scanner->readUtf8Char();
+ $value = mb_ord($valueText, 'UTF-8');
+ }
+
+ if ($valueText === false) {
+ $this->scanner->error('Invalid Unicode code point.', $start);
+ }
+
+ 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))) {
+ $hexValueText = $value === 0 ? '0' : ltrim(bin2hex($valueText), '0');
+ return '\\' . $hexValueText . ' ';
+ }
+
+ 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
+ *
+ * @param-immediately-invoked-callable $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 identifier, 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 ??= "\"$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(): (mixed|void) $consumer
+ *
+ * @param-immediately-invoked-callable $consumer
+ */
+ protected function rawText(callable $consumer): string
+ {
+ $start = $this->scanner->getPosition();
+ $consumer();
+
+ return $this->scanner->substring($start);
+ }
+
+ /**
+ * Like {@see StringScanner::spanFrom()} but passes the span through {@see $interpolationMap} if it's available.
+ */
+ protected function spanFrom(int $position): FileSpan
+ {
+ $span = $this->scanner->spanFrom($position);
+
+ if ($this->interpolationMap === null) {
+ return $span;
+ }
+
+ $interpolationMap = $this->interpolationMap;
+
+ return new LazyFileSpan(static fn() => $interpolationMap->mapSpan($span));
+ }
+
+ /**
+ * Prints a warning to standard error, associated with $span.
+ */
+ protected function warn(string $message, FileSpan $span): void
+ {
+ $this->logger->warn($message, null, $span);
+ }
+
+ /**
+ * Throws an error associated with $position.
+ *
+ * @throws FormatException
+ */
+ protected function error(string $message, FileSpan $span, ?\Throwable $previous = null): never
+ {
+ throw new FormatException($message, $span, $previous);
+ }
+
+ /**
+ * Runs $callback and wraps any {@see FormatException} it throws in a
+ * {@see SassFormatException}
+ *
+ * @template T
+ * @param callable(): T $callback
+ * @return T
+ *
+ * @param-immediately-invoked-callable $callback
+ *
+ * @throws SassFormatException
+ */
+ protected function wrapSpanFormatException(callable $callback)
+ {
+ try {
+ try {
+ return $callback();
+ } catch (FormatException $e) {
+ if ($this->interpolationMap === null) {
+ throw $e;
+ }
+
+ throw $this->interpolationMap->mapException($e);
+ }
+ } catch (MultiSourceFormatException $error) {
+ $span = $error->getSpan();
+ $secondarySpans = $error->secondarySpans;
+
+ if (0 === stripos($error->getMessage(), 'expected')) {
+ $span = $this->adjustExceptionSpan($span);
+ $secondarySpans = array_map(fn (FileSpan $span) => $this->adjustExceptionSpan($span), $secondarySpans);
+ }
+
+ throw new MultiSpanSassFormatException($error->getMessage(), $span, $error->primaryLabel, $secondarySpans, $error);
+ } catch (FormatException $error) {
+ $span = $error->getSpan();
+
+ if (0 === stripos($error->getMessage(), 'expected')) {
+ $span = $this->adjustExceptionSpan($span);
+ }
+
+ throw new SimpleSassFormatException($error->getMessage(), $span, $error);
+ }
+ }
+
+ /**
+ * Moves span to {@see firstNewlineBefore} if necessary.
+ */
+ private function adjustExceptionSpan(FileSpan $span): FileSpan
+ {
+ if ($span->getLength() > 0) {
+ return $span;
+ }
+
+ $start = $this->firstNewlineBefore($span->getStart());
+
+ if ($start === $span->getStart()) {
+ return $span;
+ }
+
+ return $start->pointSpan();
+ }
+
+ /**
+ * If $location is separated from the previous non-whitespace character in
+ * `$scanner->getString()` by one or more newlines, returns the location of the last
+ * separating newline.
+ *
+ * Otherwise returns $location.
+ *
+ * This helps avoid missing token errors pointing at the next closing bracket
+ * rather than the line where the problem actually occurred.
+ */
+ private function firstNewlineBefore(FileLocation $location): FileLocation
+ {
+ $text = $location->getFile()->getText(0, $location->getOffset());
+ $index = $location->getOffset() - 1;
+ $lastNewline = null;
+
+ while ($index >= 0) {
+ $char = $text[$index];
+
+ if (!Character::isWhitespace($char)) {
+ return $lastNewline === null ? $location : $location->getFile()->location($lastNewline);
+ }
+
+ if (Character::isNewline($char)) {
+ $lastNewline = $index;
+ }
+ $index--;
+ }
+
+ // If the document *only* contains whitespace before $location, always
+ // return $location.
+
+ return $location;
+ }
+}
diff --git a/vendor/scssphp/scssphp/src/Parser/SassParser.php b/vendor/scssphp/scssphp/src/Parser/SassParser.php
new file mode 100644
index 000000000..83a6a9905
--- /dev/null
+++ b/vendor/scssphp/scssphp/src/Parser/SassParser.php
@@ -0,0 +1,568 @@
+<?php
+
+/**
+ * SCSSPHP
+ *
+ * @copyright 2012-2020 Leaf Corcoran
+ *
+ * @license http://opensource.org/licenses/MIT MIT
+ *
+ * @link http://scssphp.github.io/scssphp
+ */
+
+namespace ScssPhp\ScssPhp\Parser;
+
+use League\Uri\Exceptions\SyntaxError;
+use ScssPhp\ScssPhp\Ast\Sass\Import;
+use ScssPhp\ScssPhp\Ast\Sass\Import\DynamicImport;
+use ScssPhp\ScssPhp\Ast\Sass\Import\StaticImport;
+use ScssPhp\ScssPhp\Ast\Sass\Interpolation;
+use ScssPhp\ScssPhp\Ast\Sass\Statement;
+use ScssPhp\ScssPhp\Ast\Sass\Statement\LoudComment;
+use ScssPhp\ScssPhp\Ast\Sass\Statement\SilentComment;
+use ScssPhp\ScssPhp\Util\Character;
+use ScssPhp\ScssPhp\Value\SassString;
+
+/**
+ * A parser for the indented syntax.
+ *
+ * @internal
+ */
+final class SassParser extends StylesheetParser
+{
+ private int $currentIndentation = 0;
+
+ /**
+ * The indentation level of the next source line after the scanner's
+ * position, or `null` if that hasn't been computed yet.
+ *
+ * A source line is any line that's not entirely whitespace.
+ */
+ private ?int $nextIndentation = null;
+
+ /**
+ * The beginning of the next source line after the scanner's position, or
+ * `null` if the next indentation hasn't been computed yet.
+ *
+ * A source line is any line that's not entirely whitespace.
+ */
+ private ?int $nextIndentationEnd = null;
+
+ /**
+ * Whether the document is indented using spaces or tabs.
+ *
+ * If this is `true`, the document is indented using spaces. If it's `false`,
+ * the document is indented using tabs. If it's `null`, we haven't yet seen
+ * the indentation character used by the document.
+ */
+ private ?bool $spaces = null;
+
+ public function getCurrentIndentation(): int
+ {
+ return $this->currentIndentation;
+ }
+
+ protected function isIndented(): bool
+ {
+ return true;
+ }
+
+ protected function styleRuleSelector(): Interpolation
+ {
+ $start = $this->scanner->getPosition();
+
+ $buffer = new InterpolationBuffer();
+
+ do {
+ $buffer->addInterpolation($this->almostAnyValue(omitComments: true));
+ $buffer->write("\n");
+ } while (str_ends_with(rtrim($buffer->getTrailingString()), ',') && $this->scanCharIf(fn ($char) => Character::isNewline($char)));
+
+ return $buffer->buildInterpolation($this->scanner->spanFrom($start));
+ }
+
+ protected function expectStatementSeparator(?string $name = null): void
+ {
+ if (!$this->atEndOfStatement()) {
+ $this->expectNewline();
+ }
+
+ if ($this->peekIndentation() <= $this->currentIndentation) {
+ return;
+ }
+
+ \assert($this->nextIndentationEnd !== null);
+
+ $this->scanner->error(\sprintf('Nothing may be indented %s.', $name === null ? 'here' : "beneath a $name"), $this->nextIndentationEnd);
+ }
+
+ protected function atEndOfStatement(): bool
+ {
+ $nextChar = $this->scanner->peekChar();
+
+ return $nextChar === null || Character::isNewline($nextChar);
+ }
+
+ protected function lookingAtChildren(): bool
+ {
+ return $this->atEndOfStatement() && $this->peekIndentation() > $this->currentIndentation;
+ }
+
+ protected function importArgument(): Import
+ {
+ switch ($this->scanner->peekChar()) {
+ case 'u':
+ case 'U':
+ $start = $this->scanner->getPosition();
+ if ($this->scanIdentifier('url')) {
+ if ($this->scanner->scanChar('(')) {
+ $this->scanner->setPosition($start);
+
+ return parent::importArgument();
+ } else {
+ $this->scanner->setPosition($start);
+ }
+ }
+ break;
+
+ case "'":
+ case '"':
+ return parent::importArgument();
+ }
+
+ $start = $this->scanner->getPosition();
+ $next = $this->scanner->peekChar();
+
+ while ($next !== null && $next !== ',' && $next !== ';' && !Character::isNewline($next)) {
+ $this->scanner->readUtf8Char();
+ $next = $this->scanner->peekChar();
+ }
+
+ $url = $this->scanner->substring($start);
+ $span = $this->scanner->spanFrom($start);
+
+ if ($this->isPlainImportUrl($url)) {
+ // Serialize $url as a Sass string because StaticImport expects it to
+ // include quotes.
+ return new StaticImport(new Interpolation([(string) new SassString($url)], $span), $span);
+ }
+
+ try {
+ return new DynamicImport($this->parseImportUrl($url), $span);
+ } catch (SyntaxError $e) {
+ $this->error('Invalid URL: ' . $e->getMessage(), $span, $e);
+ }
+ }
+
+ protected function scanElse(int $ifIndentation): bool
+ {
+ if ($this->peekIndentation() !== $ifIndentation) {
+ return false;
+ }
+
+ $start = $this->scanner->getPosition();
+ $startIndentation = $this->currentIndentation;
+ $startNextIndentation = $this->nextIndentation;
+ $startNextIndentationEnd = $this->nextIndentationEnd;
+ $this->readIndentation();
+
+ if ($this->scanner->scanChar('@') && $this->scanIdentifier('else')) {
+ return true;
+ }
+
+ $this->scanner->setPosition($start);
+ $this->currentIndentation = $startIndentation;
+ $this->nextIndentation = $startNextIndentation;
+ $this->nextIndentationEnd = $startNextIndentationEnd;
+
+ return false;
+ }
+
+ protected function children(callable $child): array
+ {
+ $children = [];
+
+ $this->whileIndentedLower(function () use ($child, &$children) {
+ $parsedChild = $this->child($child);
+
+ if ($parsedChild !== null) {
+ $children[] = $parsedChild;
+ }
+ });
+
+ return $children;
+ }
+
+ protected function statements(callable $statement): array
+ {
+ $next = $this->scanner->peekChar();
+ if ($next === "\t" || $next === ' ') {
+ $this->scanner->error('Indenting at the beginning of the document is illegal.', 0, $this->scanner->getPosition());
+ }
+
+ $statements = [];
+
+ while (!$this->scanner->isDone()) {
+ $child = $this->child($statement);
+
+ if ($child !== null) {
+ $statements[] = $child;
+ }
+
+ $indentation = $this->readIndentation();
+ \assert($indentation === 0);
+ }
+
+ return $statements;
+ }
+
+ /**
+ * Consumes a child of the current statement.
+ *
+ * This consumes children that are allowed at all levels of the document; the
+ * $child parameter is called to consume any children that are specifically
+ * allowed in the caller's context.
+ *
+ * @param callable(): (Statement|null) $child
+ */
+ private function child(callable $child): ?Statement
+ {
+ return match ($this->scanner->peekChar()) {
+ // Ignore empty lines.
+ "\r", "\n", "\f" => null,
+ '$' => $this->variableDeclarationWithoutNamespace(),
+ '/' => match ($this->scanner->peekChar(1)) {
+ '/' => $this->silentCommentStatement(),
+ '*' => $this->loudCommentStatement(),
+ default => $child(),
+ },
+ default => $child(),
+ };
+ }
+
+ /**
+ * Consumes an indented-style silent comment.
+ */
+ private function silentCommentStatement(): SilentComment
+ {
+ $start = $this->scanner->getPosition();
+ $this->scanner->expect('//');
+
+ $buffer = '';
+ $parentIndentation = $this->currentIndentation;
+
+ do {
+ $commentPrefix = $this->scanner->scanChar('/') ? '///' : '//';
+
+ while (true) {
+ $buffer .= $commentPrefix;
+
+ // Skip the initial characters because we're already writing the
+ // slashes.
+ for ($i = \strlen($commentPrefix); $i < $this->currentIndentation - $parentIndentation; $i++) {
+ $buffer .= ' ';
+ }
+
+ while (!$this->scanner->isDone() && !Character::isNewline($this->scanner->peekChar())) {
+ $buffer .= $this->scanner->readUtf8Char();
+ }
+
+ $buffer .= "\n";
+
+ if ($this->peekIndentation() < $parentIndentation) {
+ break 2;
+ }
+
+ if ($this->peekIndentation() === $parentIndentation) {
+ // Look ahead to the next line to see if it starts another comment.
+ if ($this->scanner->peekChar(1 + $parentIndentation) === '/' && $this->scanner->peekChar(2 + $parentIndentation) === '/') {
+ $this->readIndentation();
+ }
+ break;
+ }
+
+ $this->readIndentation();
+ }
+ } while ($this->scanner->scan('//'));
+
+ return $this->lastSilentComment = new SilentComment($buffer, $this->scanner->spanFrom($start));
+ }
+
+ /**
+ * Consumes an indented-style loud context.
+ */
+ private function loudCommentStatement(): LoudComment
+ {
+ $start = $this->scanner->getPosition();
+ $this->scanner->expect('/*');
+
+ $first = true;
+ $buffer = new InterpolationBuffer();
+ $buffer->write('/*');
+ $parentIndentation = $this->currentIndentation;
+
+ while (true) {
+ if ($first) {
+ // If the first line is empty, ignore it.
+ $beginningOfComment = $this->scanner->getPosition();
+ $this->spaces();
+ if (Character::isNewline($this->scanner->peekChar())) {
+ $this->readIndentation();
+ $buffer->write(' ');
+ } else {
+ $buffer->write($this->scanner->substring($beginningOfComment));
+ }
+ } else {
+ $buffer->write("\n * ");
+ }
+
+ $first = false;
+
+ for ($i = 3; $i < $this->currentIndentation - $parentIndentation; $i++) {
+ $buffer->write(' ');
+ }
+
+ while (!$this->scanner->isDone()) {
+ switch ($this->scanner->peekChar()) {
+ case "\n":
+ case "\r":
+ case "\f":
+ break 2;
+
+ case '#':
+ if ($this->scanner->peekChar(1) === '{') {
+ $buffer->add($this->singleInterpolation());
+ } else {
+ $buffer->write($this->scanner->readChar());
+ }
+ break;
+
+ default:
+ $buffer->write($this->scanner->readUtf8Char());
+ }
+ }
+
+ if ($this->peekIndentation() <= $parentIndentation) {
+ break;
+ }
+
+ // Preserve empty lines.
+ while ($this->lookingAtDoubleNewline()) {
+ $this->expectNewline();
+ $buffer->write("\n *");
+ }
+
+ $this->readIndentation();
+ }
+
+ return new LoudComment($buffer->buildInterpolation($this->scanner->spanFrom($start)));
+ }
+
+ protected function whitespaceWithoutComments(): void
+ {
+ // This overrides whitespace consumption so that it doesn't consume
+ // newlines.
+ while (!$this->scanner->isDone()) {
+ $next = $this->scanner->peekChar();
+ if ($next !== "\t" && $next !== ' ') {
+ break;
+ }
+ $this->scanner->readChar();
+ }
+ }
+
+ protected function loudComment(): void
+ {
+ // This overrides loud comment consumption so that it doesn't consume
+ // multi-line comments.
+ $this->scanner->expect('/*');
+ while (true) {
+ $next = $this->scanner->readUtf8Char();
+
+ if (Character::isNewline($next)) {
+ $this->scanner->error('expected */.');
+ }
+
+ if ($next !== '*') {
+ continue;
+ }
+
+ do {
+ $next = $this->scanner->readUtf8Char();
+ } while ($next === '*');
+
+ if ($next === '/') {
+ break;
+ }
+ }
+ }
+
+ /**
+ * Expect and consume a single newline character.
+ */
+ private function expectNewline(): void
+ {
+ switch ($this->scanner->peekChar()) {
+ case ';':
+ $this->scanner->error("semicolons aren't allowed in the indented syntax.");
+
+ case "\r":
+ $this->scanner->readChar();
+ if ($this->scanner->peekChar() === "\n") {
+ $this->scanner->readChar();
+ }
+ break;
+
+ case "\n":
+ case "\f":
+ $this->scanner->readChar();
+ break;
+
+ default:
+ $this->scanner->error('expected newline.');
+ }
+ }
+
+ /**
+ * Returns whether the scanner is immediately before *two* newlines.
+ */
+ private function lookingAtDoubleNewline(): bool
+ {
+ return match ($this->scanner->peekChar()) {
+ "\r" => match ($this->scanner->peekChar(1)) {
+ "\n" => Character::isNewline($this->scanner->peekChar(2)),
+ "\r", "\f" => true,
+ default => false,
+ },
+ "\n", "\f" => Character::isNewline($this->scanner->peekChar(1)),
+ default => false,
+ };
+ }
+
+ /**
+ * As long as the scanner's position is indented beneath the starting line,
+ * runs $body to consume the next statement.
+ *
+ * @param callable(): void $body
+ */
+ private function whileIndentedLower(callable $body): void
+ {
+ $parentIndentation = $this->currentIndentation;
+ $childIndentation = null;
+
+ while ($this->peekIndentation() > $parentIndentation) {
+ $indentation = $this->readIndentation();
+ $childIndentation ??= $indentation;
+
+ if ($childIndentation !== $indentation) {
+ $this->scanner->error(
+ "Inconsistent indentation, expected $childIndentation spaces.",
+ $this->scanner->getPosition() - $this->scanner->getColumn(),
+ $this->scanner->getColumn()
+ );
+ }
+
+ $body();
+ }
+ }
+
+ /**
+ * Consumes indentation whitespace and returns the indentation level of the
+ * next line.
+ *
+ * @phpstan-impure
+ */
+ private function readIndentation(): int
+ {
+ $currentIndentation = $this->currentIndentation = $this->nextIndentation ??= $this->peekIndentation();
+ \assert($this->nextIndentationEnd !== null);
+ $this->scanner->setPosition($this->nextIndentationEnd);
+ $this->nextIndentation = null;
+ $this->nextIndentationEnd = null;
+
+ return $currentIndentation;
+ }
+
+ /**
+ * Returns the indentation level of the next line.
+ */
+ private function peekIndentation(): int
+ {
+ if ($this->nextIndentation !== null) {
+ return $this->nextIndentation;
+ }
+
+ if ($this->scanner->isDone()) {
+ $this->nextIndentation = 0;
+ $this->nextIndentationEnd = $this->scanner->getPosition();
+
+ return 0;
+ }
+
+ $start = $this->scanner->getPosition();
+
+ do {
+ $containsTab = false;
+ $containsSpace = false;
+ $nextIndentation = 0;
+
+ while (true) {
+ switch ($this->scanner->peekChar()) {
+ case ' ':
+ $containsSpace = true;
+ break;
+
+ case "\t":
+ $containsTab = true;
+ break;
+
+ default:
+ break 2;
+ }
+
+ $nextIndentation++;
+ $this->scanner->readChar();
+ }
+
+ if ($this->scanner->isDone()) {
+ $this->nextIndentation = 0;
+ $this->nextIndentationEnd = $this->scanner->getPosition();
+ $this->scanner->setPosition($start);
+
+ return 0;
+ }
+ } while ($this->scanCharIf(fn ($char) => Character::isNewline($char)));
+
+ $this->checkIndentationConsistency($containsTab, $containsSpace);
+
+ $this->nextIndentation = $nextIndentation;
+ if ($nextIndentation > 0) {
+ $this->spaces ??= $containsSpace;
+ }
+ $this->nextIndentationEnd = $this->scanner->getPosition();
+ $this->scanner->setPosition($start);
+
+ return $nextIndentation;
+ }
+
+ /**
+ * Ensures that the document uses consistent characters for indentation.
+ *
+ * The $containsTab and $containsSpace parameters refer to a single line of
+ * indentation that has just been parsed.
+ */
+ private function checkIndentationConsistency(bool $containsTab, bool $containsSpace): void
+ {
+ if ($containsTab) {
+ if ($containsSpace) {
+ $this->scanner->error('Tabs and spaces may not be mixed.', $this->scanner->getPosition() - $this->scanner->getColumn(), $this->scanner->getColumn());
+ }
+
+ if ($this->spaces === true) {
+ $this->scanner->error('Expected spaces, was tabs.', $this->scanner->getPosition() - $this->scanner->getColumn(), $this->scanner->getColumn());
+ }
+ } elseif ($containsSpace && $this->spaces === false) {
+ $this->scanner->error('Expected tabs, was spaces.', $this->scanner->getPosition() - $this->scanner->getColumn(), $this->scanner->getColumn());
+ }
+ }
+}
diff --git a/vendor/scssphp/scssphp/src/Parser/ScssParser.php b/vendor/scssphp/scssphp/src/Parser/ScssParser.php
new file mode 100644
index 000000000..704c9c8ae
--- /dev/null
+++ b/vendor/scssphp/scssphp/src/Parser/ScssParser.php
@@ -0,0 +1,277 @@
+<?php
+
+/**
+ * SCSSPHP
+ *
+ * @copyright 2012-2020 Leaf Corcoran
+ *
+ * @license http://opensource.org/licenses/MIT MIT
+ *
+ * @link http://scssphp.github.io/scssphp
+ */
+
+namespace ScssPhp\ScssPhp\Parser;
+
+use ScssPhp\ScssPhp\Ast\Sass\Interpolation;
+use ScssPhp\ScssPhp\Ast\Sass\Statement\LoudComment;
+use ScssPhp\ScssPhp\Ast\Sass\Statement\SilentComment;
+use ScssPhp\ScssPhp\Deprecation;
+use ScssPhp\ScssPhp\Util\Character;
+use ScssPhp\ScssPhp\Util\LoggerUtil;
+
+/**
+ * A parser for the CSS-compatible syntax.
+ *
+ * @internal
+ */
+class ScssParser extends StylesheetParser
+{
+ protected function isIndented(): bool
+ {
+ return false;
+ }
+
+ protected function getCurrentIndentation(): int
+ {
+ return 0;
+ }
+
+ protected function styleRuleSelector(): Interpolation
+ {
+ return $this->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)) {
+ LoggerUtil::warnForDeprecation($this->logger, Deprecation::elseif, "@elseif is deprecated and will not be supported in future Sass versions.\n\nRecommendation: @else if", $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->spaces();
+ } 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/vendor/scssphp/scssphp/src/Parser/SelectorParser.php b/vendor/scssphp/scssphp/src/Parser/SelectorParser.php
new file mode 100644
index 000000000..ab1127198
--- /dev/null
+++ b/vendor/scssphp/scssphp/src/Parser/SelectorParser.php
@@ -0,0 +1,614 @@
+<?php
+
+/**
+ * SCSSPHP
+ *
+ * @copyright 2012-2020 Leaf Corcoran
+ *
+ * @license http://opensource.org/licenses/MIT MIT
+ *
+ * @link http://scssphp.github.io/scssphp
+ */
+
+namespace ScssPhp\ScssPhp\Parser;
+
+use League\Uri\Contracts\UriInterface;
+use ScssPhp\ScssPhp\Ast\Css\CssValue;
+use ScssPhp\ScssPhp\Ast\Selector\AttributeOperator;
+use ScssPhp\ScssPhp\Ast\Selector\AttributeSelector;
+use ScssPhp\ScssPhp\Ast\Selector\ClassSelector;
+use ScssPhp\ScssPhp\Ast\Selector\Combinator;
+use ScssPhp\ScssPhp\Ast\Selector\ComplexSelector;
+use ScssPhp\ScssPhp\Ast\Selector\ComplexSelectorComponent;
+use ScssPhp\ScssPhp\Ast\Selector\CompoundSelector;
+use ScssPhp\ScssPhp\Ast\Selector\IDSelector;
+use ScssPhp\ScssPhp\Ast\Selector\ParentSelector;
+use ScssPhp\ScssPhp\Ast\Selector\PlaceholderSelector;
+use ScssPhp\ScssPhp\Ast\Selector\PseudoSelector;
+use ScssPhp\ScssPhp\Ast\Selector\QualifiedName;
+use ScssPhp\ScssPhp\Ast\Selector\SelectorList;
+use ScssPhp\ScssPhp\Ast\Selector\SimpleSelector;
+use ScssPhp\ScssPhp\Ast\Selector\TypeSelector;
+use ScssPhp\ScssPhp\Ast\Selector\UniversalSelector;
+use ScssPhp\ScssPhp\Exception\SassFormatException;
+use ScssPhp\ScssPhp\Logger\LoggerInterface;
+use ScssPhp\ScssPhp\Util;
+use ScssPhp\ScssPhp\Util\Character;
+
+/**
+ * A parser for selectors.
+ *
+ * @internal
+ */
+final class SelectorParser extends Parser
+{
+ /**
+ * Pseudo-class selectors that take unadorned selectors as arguments.
+ */
+ private const SELECTOR_PSEUDO_CLASSES = ['not', 'is', 'matches', 'where', 'current', 'any', 'has', 'host', 'host-context'];
+
+ /**
+ * Pseudo-element selectors that take unadorned selectors as arguments.
+ */
+ private const SELECTOR_PSEUDO_ELEMENTS = ['slotted'];
+
+ private readonly bool $allowParent;
+
+ /**
+ * Whether to parse the selector as plain CSS.
+ */
+ private readonly bool $plainCss;
+
+ /**
+ * Creates a parser that parses CSS selectors.
+ *
+ * If $allowParent is `false`, this will throw a @see SassFormatException} if
+ * the selector includes the parent selector `&`.
+ *
+ * If $plainCss is `true`, this will parse the selector as a plain CSS
+ * selector rather than a Sass selector.
+ */
+ public function __construct(string $contents, ?LoggerInterface $logger = null, ?UriInterface $url = null, bool $allowParent = true, ?InterpolationMap $interpolationMap = null, bool $plainCss = false)
+ {
+ $this->allowParent = $allowParent;
+ $this->plainCss = $plainCss;
+ parent::__construct($contents, $logger, $url, $interpolationMap);
+ }
+
+ /**
+ * @throws SassFormatException
+ */
+ public function parse(): SelectorList
+ {
+ return $this->wrapSpanFormatException(function () {
+ $selector = $this->selectorList();
+
+ if (!$this->scanner->isDone()) {
+ $this->scanner->error('expected selector.');
+ }
+
+ return $selector;
+ });
+ }
+
+ public function parseComplexSelector(): ComplexSelector
+ {
+ return $this->wrapSpanFormatException(function () {
+ $complex = $this->complexSelector();
+
+ if (!$this->scanner->isDone()) {
+ $this->scanner->error('expected selector.');
+ }
+
+ return $complex;
+ });
+ }
+
+ public function parseCompoundSelector(): CompoundSelector
+ {
+ return $this->wrapSpanFormatException(function () {
+ $compound = $this->compoundSelector();
+
+ if (!$this->scanner->isDone()) {
+ $this->scanner->error('expected selector.');
+ }
+
+ return $compound;
+ });
+ }
+
+ public function parseSimpleSelector(): SimpleSelector
+ {
+ return $this->wrapSpanFormatException(function () {
+ $simple = $this->simpleSelector();
+
+ if (!$this->scanner->isDone()) {
+ $this->scanner->error('unexpected token.');
+ }
+
+ return $simple;
+ });
+ }
+
+ /**
+ * Consumes a selector list.
+ */
+ private function selectorList(): SelectorList
+ {
+ $start = $this->scanner->getPosition();
+ $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, $this->spanFrom($start));
+ }
+
+ /**
+ * 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
+ {
+ $start = $this->scanner->getPosition();
+
+ $componentStart = $this->scanner->getPosition();
+ $lastCompound = null;
+ /** @var list<CssValue<Combinator>> $combinators */
+ $combinators = [];
+
+ $initialCombinators = null;
+ $components = [];
+
+ while (true) {
+ $this->whitespace();
+
+ $next = $this->scanner->peekChar();
+
+ switch ($next) {
+ case '+':
+ $combinatorStart = $this->scanner->getPosition();
+ $this->scanner->readChar();
+ $combinators[] = new CssValue(Combinator::NEXT_SIBLING, $this->spanFrom($combinatorStart));
+ break;
+
+ case '>':
+ $combinatorStart = $this->scanner->getPosition();
+ $this->scanner->readChar();
+ $combinators[] = new CssValue(Combinator::CHILD, $this->spanFrom($combinatorStart));
+ break;
+
+ case '~':
+ $combinatorStart = $this->scanner->getPosition();
+ $this->scanner->readChar();
+ $combinators[] = new CssValue(Combinator::FOLLOWING_SIBLING, $this->spanFrom($combinatorStart));
+ break;
+
+ default:
+ if ($next === null || (!\in_array($next, ['[', '.', '#', '%', ':', '&', '*', '|'], true) && !$this->lookingAtIdentifier())) {
+ break 2;
+ }
+
+ if ($lastCompound !== null) {
+ $components[] = new ComplexSelectorComponent($lastCompound, $combinators, $this->spanFrom($componentStart));
+ } elseif (\count($combinators) !== 0) {
+ \assert($initialCombinators === null);
+ $initialCombinators = $combinators;
+ $componentStart = $this->scanner->getPosition();
+ }
+ $lastCompound = $this->compoundSelector();
+ $combinators = [];
+
+ if ($this->scanner->peekChar() === '&') {
+ $this->scanner->error('"&" may only used at the beginning of a compound selector.');
+ }
+ break;
+ }
+ }
+
+ if (\count($combinators) > 0 && $this->plainCss) {
+ $this->scanner->error('expected selector.');
+ }
+ if ($lastCompound !== null) {
+ $components[] = new ComplexSelectorComponent($lastCompound, $combinators, $this->spanFrom($componentStart));
+ } elseif (\count($combinators) !== 0) {
+ $initialCombinators = $combinators;
+ } else {
+ $this->scanner->error('expected selector.');
+ }
+
+ return new ComplexSelector($initialCombinators ?? [], $components, $this->spanFrom($start), $lineBreak);
+ }
+
+ /**
+ * Consumes a compound selector.
+ */
+ private function compoundSelector(): CompoundSelector
+ {
+ $start = $this->scanner->getPosition();
+ $components = [$this->simpleSelector()];
+
+ while ($this->isSimpleSelectorStart($this->scanner->peekChar())) {
+ $components[] = $this->simpleSelector(false);
+ }
+
+ return new CompoundSelector($components, $this->spanFrom($start));
+ }
+
+ /**
+ * 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 ??= $this->allowParent;
+
+ switch ($this->scanner->peekChar()) {
+ case '[':
+ return $this->attributeSelector();
+
+ case '.':
+ return $this->classSelector();
+
+ case '#':
+ return $this->idSelector();
+
+ case '%':
+ $selector = $this->placeholderSelector();
+ if ($this->plainCss) {
+ $this->error("Placeholder selectors aren't allowed in plain CSS.", $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
+ {
+ $start = $this->scanner->getPosition();
+ $this->scanner->expectChar('[');
+ $this->whitespace();
+
+ $name = $this->attributeName();
+ $this->whitespace();
+
+ if ($this->scanner->scanChar(']')) {
+ return AttributeSelector::create($name, $this->spanFrom($start));
+ }
+
+ $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, $this->spanFrom($start), $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(), '*');
+ }
+
+ if ($this->scanner->scanChar('|')) {
+ 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.
+ */
+ private function attributeOperator(): AttributeOperator
+ {
+ $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
+ {
+ $start = $this->scanner->getPosition();
+ $this->scanner->expectChar('.');
+ $name = $this->identifier();
+
+ return new ClassSelector($name, $this->spanFrom($start));
+ }
+
+ /**
+ * Consumes an ID selector.
+ */
+ private function idSelector(): IDSelector
+ {
+ $start = $this->scanner->getPosition();
+ $this->scanner->expectChar('#');
+ $name = $this->identifier();
+
+ return new IDSelector($name, $this->spanFrom($start));
+ }
+
+ /**
+ * Consumes a placeholder selector.
+ */
+ private function placeholderSelector(): PlaceholderSelector
+ {
+ $start = $this->scanner->getPosition();
+ $this->scanner->expectChar('%');
+ $name = $this->identifier();
+
+ return new PlaceholderSelector($name, $this->spanFrom($start));
+ }
+
+ /**
+ * Consumes a parent selector.
+ */
+ private function parentSelector(): ParentSelector
+ {
+ $start = $this->scanner->getPosition();
+ $this->scanner->expectChar('&');
+ $suffix = $this->lookingAtIdentifierBody() ? $this->identifierBody() : null;
+
+ if ($this->plainCss && $suffix !== null) {
+ $this->scanner->error("Parent selectors can't have suffixes in plain CSS.", $start, $this->scanner->getPosition() - $start);
+ }
+
+ return new ParentSelector($this->spanFrom($start), $suffix);
+ }
+
+ /**
+ * Consumes a pseudo selector.
+ */
+ private function pseudoSelector(): PseudoSelector
+ {
+ $start = $this->scanner->getPosition();
+ $this->scanner->expectChar(':');
+ $element = $this->scanner->scanChar(':');
+ $name = $this->identifier();
+
+ if (!$this->scanner->scanChar('(')) {
+ return new PseudoSelector($name, $this->spanFrom($start), $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, $this->spanFrom($start), $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
+ {
+ $start = $this->scanner->getPosition();
+ $first = $this->scanner->peekChar();
+
+ if ($first === '*') {
+ $this->scanner->readChar();
+
+ if (!$this->scanner->scanChar('|')) {
+ return new UniversalSelector($this->spanFrom($start));
+ }
+
+ if ($this->scanner->scanChar('*')) {
+ return new UniversalSelector($this->spanFrom($start), '*');
+ }
+
+ return new TypeSelector(new QualifiedName($this->identifier(), '*'), $this->spanFrom($start));
+ }
+
+ if ($first === '|') {
+ $this->scanner->readChar();
+
+ if ($this->scanner->scanChar('*')) {
+ return new UniversalSelector($this->spanFrom($start), '');
+ }
+
+ return new TypeSelector(new QualifiedName($this->identifier(), ''), $this->spanFrom($start));
+ }
+
+ $nameOrNamespace = $this->identifier();
+
+ if (!$this->scanner->scanChar('|')) {
+ return new TypeSelector(new QualifiedName($nameOrNamespace), $this->spanFrom($start));
+ }
+
+ if ($this->scanner->scanChar('*')) {
+ return new UniversalSelector($this->spanFrom($start), $nameOrNamespace);
+ }
+
+ return new TypeSelector(new QualifiedName($this->identifier(), $nameOrNamespace), $this->spanFrom($start));
+ }
+
+ /**
+ * Returns whether $character can start a simple selector in the middle of a compound selector.
+ */
+ private function isSimpleSelectorStart(?string $character): bool
+ {
+ return match ($character) {
+ '*', '[', '.', '#', '%', ':' => true,
+ '&' => $this->plainCss,
+ default => false,
+ };
+ }
+}
diff --git a/vendor/scssphp/scssphp/src/Parser/StringScanner.php b/vendor/scssphp/scssphp/src/Parser/StringScanner.php
new file mode 100644
index 000000000..9fabb48ff
--- /dev/null
+++ b/vendor/scssphp/scssphp/src/Parser/StringScanner.php
@@ -0,0 +1,309 @@
+<?php
+
+/**
+ * SCSSPHP
+ *
+ * @copyright 2012-2020 Leaf Corcoran
+ *
+ * @license http://opensource.org/licenses/MIT MIT
+ *
+ * @link http://scssphp.github.io/scssphp
+ */
+
+namespace ScssPhp\ScssPhp\Parser;
+
+use League\Uri\Contracts\UriInterface;
+use SourceSpan\FileLocation;
+use SourceSpan\FileSpan;
+use SourceSpan\SourceFile;
+
+/**
+ * A port of Dart's string_scanner package to be used by the parser.
+ *
+ * The scanner only supports UTF-8 strings.
+ *
+ * Differences with Dart:
+ * - reading a character is reading a byte, not a UTF-16 code unit (as PHP strings are not UTF-16). The
+ * {@see readUtf8Char} method can be used to consume a UTF-8 char.
+ * - characters are represented as a single-char string, not as an integer with their UTF-16 char code
+ * - offsets are based on bytes, not on UTF-16 code units. In practice, parsing Sass generally needs
+ * to peak following chars only when already knowing that the current char is an ASCII one, which
+ * makes this safe. When this assumption does not hold anymore, a different logic should be used
+ * - as strings and regexp cannot be used interchangeably in PHP (in Dart, regexps are a different
+ * object, and both String and Regexp are implementing a Pattern interface for matching), the scanner
+ * exposes supports only strings in scan() and expect(). Should we need support for regexps, a
+ * separate method will be added.
+ *
+ * @internal
+ */
+class StringScanner
+{
+ private readonly string $string;
+
+ private int $position = 0;
+
+ private readonly SourceFile $sourceFile;
+
+ public function __construct(string $content, ?UriInterface $sourceUrl = null)
+ {
+ $this->string = $content;
+ $this->sourceFile = SourceFile::fromString($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
+ {
+ return $this->sourceFile->span($start, $end ?? $this->position);
+ }
+
+ /**
+ * The current location of the scanner.
+ */
+ public function getLocation(): FileLocation
+ {
+ return $this->sourceFile->location($this->position);
+ }
+
+ /**
+ * 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 is the provided character.
+ *
+ * @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.
+ *
+ * @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.
+ *
+ * @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);
+ }
+
+ /**
+ * @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.
+ */
+ 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.
+ */
+ 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.
+ */
+ public function getLine(): int
+ {
+ return $this->sourceFile->getLine($this->position);
+ }
+
+ /**
+ * The scanner's current (zero-based) column number.
+ */
+ public function getColumn(): int
+ {
+ return $this->sourceFile->getColumn($this->position);
+ }
+
+ /**
+ * @throws FormatException
+ */
+ public function error(string $message, ?int $position = null, ?int $length = null): never
+ {
+ $position ??= $this->position;
+ $length ??= 0;
+
+ $span = $this->sourceFile->span($position, $position + $length);
+
+ throw new FormatException($message, $span);
+ }
+
+ /**
+ * @throws FormatException
+ */
+ private function fail(string $message): never
+ {
+ $this->error("expected $message.");
+ }
+}
diff --git a/vendor/scssphp/scssphp/src/Parser/StylesheetParser.php b/vendor/scssphp/scssphp/src/Parser/StylesheetParser.php
new file mode 100644
index 000000000..ea2918a6f
--- /dev/null
+++ b/vendor/scssphp/scssphp/src/Parser/StylesheetParser.php
@@ -0,0 +1,4356 @@
+<?php
+
+/**
+ * SCSSPHP
+ *
+ * @copyright 2012-2020 Leaf Corcoran
+ *
+ * @license http://opensource.org/licenses/MIT MIT
+ *
+ * @link http://scssphp.github.io/scssphp
+ */
+
+namespace ScssPhp\ScssPhp\Parser;
+
+use League\Uri\Contracts\UriInterface;
+use League\Uri\Exceptions\SyntaxError;
+use League\Uri\Uri;
+use ScssPhp\ScssPhp\Ast\Sass\Argument;
+use ScssPhp\ScssPhp\Ast\Sass\ArgumentDeclaration;
+use ScssPhp\ScssPhp\Ast\Sass\ArgumentInvocation;
+use ScssPhp\ScssPhp\Ast\Sass\Expression;
+use ScssPhp\ScssPhp\Ast\Sass\Expression\BinaryOperationExpression;
+use ScssPhp\ScssPhp\Ast\Sass\Expression\BinaryOperator;
+use ScssPhp\ScssPhp\Ast\Sass\Expression\BooleanExpression;
+use ScssPhp\ScssPhp\Ast\Sass\Expression\ColorExpression;
+use ScssPhp\ScssPhp\Ast\Sass\Expression\FunctionExpression;
+use ScssPhp\ScssPhp\Ast\Sass\Expression\IfExpression;
+use ScssPhp\ScssPhp\Ast\Sass\Expression\InterpolatedFunctionExpression;
+use ScssPhp\ScssPhp\Ast\Sass\Expression\ListExpression;
+use ScssPhp\ScssPhp\Ast\Sass\Expression\MapExpression;
+use ScssPhp\ScssPhp\Ast\Sass\Expression\NullExpression;
+use ScssPhp\ScssPhp\Ast\Sass\Expression\NumberExpression;
+use ScssPhp\ScssPhp\Ast\Sass\Expression\ParenthesizedExpression;
+use ScssPhp\ScssPhp\Ast\Sass\Expression\SelectorExpression;
+use ScssPhp\ScssPhp\Ast\Sass\Expression\StringExpression;
+use ScssPhp\ScssPhp\Ast\Sass\Expression\SupportsExpression;
+use ScssPhp\ScssPhp\Ast\Sass\Expression\UnaryOperationExpression;
+use ScssPhp\ScssPhp\Ast\Sass\Expression\UnaryOperator;
+use ScssPhp\ScssPhp\Ast\Sass\Expression\VariableExpression;
+use ScssPhp\ScssPhp\Ast\Sass\Import;
+use ScssPhp\ScssPhp\Ast\Sass\Import\DynamicImport;
+use ScssPhp\ScssPhp\Ast\Sass\Import\StaticImport;
+use ScssPhp\ScssPhp\Ast\Sass\Interpolation;
+use ScssPhp\ScssPhp\Ast\Sass\Statement;
+use ScssPhp\ScssPhp\Ast\Sass\Statement\AtRootRule;
+use ScssPhp\ScssPhp\Ast\Sass\Statement\AtRule;
+use ScssPhp\ScssPhp\Ast\Sass\Statement\ContentBlock;
+use ScssPhp\ScssPhp\Ast\Sass\Statement\ContentRule;
+use ScssPhp\ScssPhp\Ast\Sass\Statement\DebugRule;
+use ScssPhp\ScssPhp\Ast\Sass\Statement\Declaration;
+use ScssPhp\ScssPhp\Ast\Sass\Statement\EachRule;
+use ScssPhp\ScssPhp\Ast\Sass\Statement\ElseClause;
+use ScssPhp\ScssPhp\Ast\Sass\Statement\ErrorRule;
+use ScssPhp\ScssPhp\Ast\Sass\Statement\ExtendRule;
+use ScssPhp\ScssPhp\Ast\Sass\Statement\ForRule;
+use ScssPhp\ScssPhp\Ast\Sass\Statement\FunctionRule;
+use ScssPhp\ScssPhp\Ast\Sass\Statement\IfClause;
+use ScssPhp\ScssPhp\Ast\Sass\Statement\IfRule;
+use ScssPhp\ScssPhp\Ast\Sass\Statement\ImportRule;
+use ScssPhp\ScssPhp\Ast\Sass\Statement\IncludeRule;
+use ScssPhp\ScssPhp\Ast\Sass\Statement\MediaRule;
+use ScssPhp\ScssPhp\Ast\Sass\Statement\MixinRule;
+use ScssPhp\ScssPhp\Ast\Sass\Statement\ReturnRule;
+use ScssPhp\ScssPhp\Ast\Sass\Statement\SilentComment;
+use ScssPhp\ScssPhp\Ast\Sass\Statement\StyleRule;
+use ScssPhp\ScssPhp\Ast\Sass\Statement\Stylesheet;
+use ScssPhp\ScssPhp\Ast\Sass\Statement\SupportsRule;
+use ScssPhp\ScssPhp\Ast\Sass\Statement\VariableDeclaration;
+use ScssPhp\ScssPhp\Ast\Sass\Statement\WarnRule;
+use ScssPhp\ScssPhp\Ast\Sass\Statement\WhileRule;
+use ScssPhp\ScssPhp\Ast\Sass\SupportsCondition;
+use ScssPhp\ScssPhp\Ast\Sass\SupportsCondition\SupportsAnything;
+use ScssPhp\ScssPhp\Ast\Sass\SupportsCondition\SupportsDeclaration;
+use ScssPhp\ScssPhp\Ast\Sass\SupportsCondition\SupportsFunction;
+use ScssPhp\ScssPhp\Ast\Sass\SupportsCondition\SupportsInterpolation;
+use ScssPhp\ScssPhp\Ast\Sass\SupportsCondition\SupportsNegation;
+use ScssPhp\ScssPhp\Ast\Sass\SupportsCondition\SupportsOperation;
+use ScssPhp\ScssPhp\Colors;
+use ScssPhp\ScssPhp\Deprecation;
+use ScssPhp\ScssPhp\Exception\SassFormatException;
+use ScssPhp\ScssPhp\Logger\LoggerInterface;
+use ScssPhp\ScssPhp\Util;
+use ScssPhp\ScssPhp\Util\Character;
+use ScssPhp\ScssPhp\Util\LoggerUtil;
+use ScssPhp\ScssPhp\Util\Path;
+use ScssPhp\ScssPhp\Util\StringUtil;
+use ScssPhp\ScssPhp\Value\ListSeparator;
+use ScssPhp\ScssPhp\Value\SassColor;
+use ScssPhp\ScssPhp\Value\SpanColorFormat;
+use SourceSpan\FileSpan;
+
+/**
+ * @internal
+ */
+abstract class StylesheetParser extends Parser
+{
+ /**
+ * The silent comment this parser encountered previously.
+ */
+ protected ?SilentComment $lastSilentComment = null;
+
+ /**
+ * Whether we've consumed a rule other than `@charset`, `@forward`, or `@use`.
+ */
+ private bool $isUseAllowed = true;
+
+ /**
+ * Whether the parser is currently parsing the contents of a mixin declaration.
+ */
+ private bool $inMixin = false;
+
+ /**
+ * Whether the parser is currently parsing a content block passed to a mixin.
+ */
+ private bool $inContentBlock = false;
+
+ /**
+ * Whether the parser is currently parsing a control directive such as `@if`
+ * or `@each`.
+ */
+ private bool $inControlDirective = false;
+
+ /**
+ * Whether the parser is currently parsing an unknown rule.
+ */
+ private bool $inUnknownAtRule = false;
+
+ /**
+ * Whether the parser is currently parsing a style rule.
+ */
+ private bool $inStyleRule = false;
+
+ /**
+ * Whether the parser is currently within a parenthesized expression.
+ */
+ private bool $inParentheses = false;
+
+ /**
+ * Whether the parser is currently within an expression.
+ */
+ private bool $inExpression = false;
+
+ /**
+ * A map from all variable names that are assigned with `!global` in the
+ * current stylesheet to the nodes where they're defined.
+ *
+ * These are collected at parse time because they affect the variables
+ * exposed by the module generated for this stylesheet, *even if they aren't
+ * evaluated*. This allows us to ensure that the stylesheet always exposes
+ * the same set of variable names no matter how it's evaluated.
+ *
+ * @var array<string, VariableDeclaration>
+ */
+ private array $globalVariables = [];
+
+ public function __construct(string $contents, ?LoggerInterface $logger = null, ?UriInterface $sourceUrl = null)
+ {
+ parent::__construct($contents, $logger, $sourceUrl);
+ }
+
+ protected function inExpression(): bool
+ {
+ return $this->inExpression;
+ }
+
+ /**
+ * @throws SassFormatException when parsing fails
+ */
+ public function parse(): Stylesheet
+ {
+ return $this->wrapSpanFormatException(function () {
+ $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());
+ });
+ }
+
+ public function parseArgumentDeclaration(): ArgumentDeclaration
+ {
+ return $this->wrapSpanFormatException(function () {
+ $this->scanner->expectChar('@', '@-rule');
+ $this->identifier();
+ $this->whitespace();
+ $this->identifier();
+ $arguments = $this->argumentDeclaration();
+ $this->whitespace();
+ $this->scanner->expectChar('{');
+
+ $this->scanner->expectDone();
+
+ return $arguments;
+ });
+ }
+
+ /**
+ * 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->statement(...), $root);
+
+ case '+':
+ if (!$this->isIndented() || !$this->lookingAtIdentifier(1)) {
+ return $this->styleRule();
+ }
+
+ $this->isUseAllowed = false;
+ $start = $this->scanner->getPosition();
+ $this->scanner->readChar();
+
+ return $this->includeRule($start);
+
+ case '=':
+ if (!$this->isIndented()) {
+ return $this->styleRule();
+ }
+
+ $this->isUseAllowed = false;
+ $start = $this->scanner->getPosition();
+ $this->scanner->readChar();
+ $this->whitespace();
+
+ return $this->mixinRule($start);
+
+ 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, fn() => $this->scanner->spanFrom($start));
+ }
+
+ if ($this->isPlainCss()) {
+ $this->error('Sass variables aren\'t allowed in plain CSS.', $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') {
+ if ($guarded) {
+ LoggerUtil::warnForDeprecation($this->logger, Deprecation::duplicateVarFlags, "!default should only be written once for each variable.\nThis will be an error in Dart Sass 2.0.0.", $this->scanner->spanFrom($flagStart));
+ }
+
+ $guarded = true;
+ } elseif ($flag === 'global') {
+ if ($namespace !== null) {
+ $this->error("!global isn't allowed for variables in other modules.", $this->scanner->spanFrom($flagStart));
+ } elseif ($global) {
+ LoggerUtil::warnForDeprecation($this->logger, Deprecation::duplicateVarFlags, "!global should only be written once for each variable.\nThis will be an error in Dart Sass 2.0.0.", $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();
+ }
+
+ // The indented syntax allows a single backslash to distinguish a style rule
+ // from old-style property syntax. We don't support old property syntax, but
+ // we do support the backslash because it's easy to do.
+ if ($this->isIndented() && $this->scanner->scanChar('\\')) {
+ 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
+ {
+ // The indented syntax allows a single backslash to distinguish a style rule
+ // from old-style property syntax. We don't support old property syntax, but
+ // we do support the backslash because it's easy to do.
+ if ($this->isIndented() && $this->scanner->scanChar('\\')) {
+ return $this->styleRule();
+ }
+
+ $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.
+ */
+ private function declarationOrBuffer(): Statement|InterpolationBuffer
+ {
+ $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 (str_starts_with($name->getInitialPlain(), '--')) {
+ $value = new StringExpression($this->interpolatedDeclarationValue(silentComments: false));
+ $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()) {
+ // In the indented syntax, `foo:bar` is always considered a selector
+ // rather than a property.
+ $nameBuffer->write($midBuffer);
+
+ return $nameBuffer;
+ }
+
+ $postColonWhitespace = $this->rawText($this->whitespace(...));
+
+ $nested = $this->tryDeclarationChildren($name, $start);
+ if ($nested !== null) {
+ return $nested;
+ }
+
+ $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;
+ }
+
+ $nested = $this->tryDeclarationChildren($name, $start, $value);
+ if ($nested !== null) {
+ return $nested;
+ }
+
+ $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.
+ */
+ private function variableDeclarationOrInterpolation(): Interpolation|VariableDeclaration
+ {
+ 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->statement(...), $start, function (array $children) use ($wasInStyleRule, $start, $interpolation) {
+ if ($this->isIndented() && $children === []) {
+ $this->warn("This selector doesn't have any properties and won't be rendered.", $interpolation->getSpan());
+ }
+
+ $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 && str_starts_with($name->getInitialPlain(), '--')) {
+ $value = new StringExpression($this->interpolatedDeclarationValue(silentComments: false));
+ $this->expectStatementSeparator('custom property');
+
+ return Declaration::create($name, $value, $this->scanner->spanFrom($start));
+ }
+
+ $this->whitespace();
+
+ $nested = $this->tryDeclarationChildren($name, $start);
+ if ($nested !== null) {
+ return $nested;
+ }
+
+ $value = $this->expression();
+
+ $nested = $this->tryDeclarationChildren($name, $start, $value);
+ if ($nested !== null) {
+ return $nested;
+ }
+
+ $this->expectStatementSeparator();
+
+ return Declaration::create($name, $value, $this->scanner->spanFrom($start));
+ }
+
+ /**
+ * Tries parsing nested children of a declaration whose $name has already
+ * been parsed, and returns `null` if it doesn't have any.
+ *
+ * If $value is passed, it's used as the value of the property without
+ * nesting.
+ */
+ private function tryDeclarationChildren(Interpolation $name, int $start, ?Expression $value = null): ?Declaration
+ {
+ if (!$this->lookingAtChildren()) {
+ return null;
+ }
+
+ if ($this->isPlainCss()) {
+ $this->scanner->error("Nested declarations aren't allowed in plain CSS.");
+ }
+
+ return $this->withChildren($this->declarationChild(...), $start, fn(array $children, FileSpan $span) => Declaration::nested($name, $children, $span, $value));
+ }
+
+ /**
+ * 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
+ *
+ * @param-immediately-invoked-callable $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->declarationChild(...));
+ case 'else':
+ $this->disallowedAtRule($start);
+ case 'error':
+ return $this->errorRule($start);
+ case 'for':
+ return $this->forRule($start, $this->declarationChild(...));
+ case 'if':
+ return $this->ifRule($start, $this->declarationChild(...));
+ case 'include':
+ return $this->includeRule($start);
+ case 'warn':
+ return $this->warnRule($start);
+ case 'while':
+ return $this->whileRule($start, $this->declarationChild(...));
+ 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) {
+ 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->functionChild(...));
+ case 'else':
+ $this->disallowedAtRule($start);
+ case 'error':
+ return $this->errorRule($start);
+ case 'for':
+ return $this->forRule($start, $this->functionChild(...));
+ case 'if':
+ return $this->ifRule($start, $this->functionChild(...));
+ case 'return':
+ return $this->returnRule($start);
+ case 'warn':
+ return $this->warnRule($start);
+ case 'while':
+ return $this->whileRule($start, $this->functionChild(...));
+ 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->statement(...), $start, fn(array $children, FileSpan $span) => new AtRootRule($children, $span, $query));
+ }
+
+ if ($this->lookingAtChildren() || ($this->isIndented() && $this->atEndOfStatement())) {
+ return $this->withChildren($this->statement(...), $start, fn(array $children, FileSpan $span) => 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
+ {
+ $start = $this->scanner->getPosition();
+ $buffer = new InterpolationBuffer();
+ $this->scanner->expectChar('(');
+ $buffer->write('(');
+ $this->whitespace();
+
+ $this->addOrInject($buffer, $this->expression());
+
+ if ($this->scanner->scanChar(':')) {
+ $this->whitespace();
+ $buffer->write(': ');
+ $this->addOrInject($buffer, $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));
+ }
+
+ $beforeWhitespace = $this->scanner->getLocation();
+ $this->whitespace();
+
+ if ($this->scanner->peekChar() === '(') {
+ $arguments = $this->argumentInvocation(true);
+ $this->whitespace();
+ } else {
+ $arguments = ArgumentInvocation::createEmpty($beforeWhitespace->pointSpan());
+ }
+
+ $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
+ *
+ * @param-immediately-invoked-callable $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->whitespace();
+ }
+
+ $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;
+
+ $beforeName = $this->scanner->getPosition();
+ $name = $this->identifier();
+
+ if (str_starts_with($name, '--')) {
+ LoggerUtil::warnForDeprecation(
+ $this->logger,
+ Deprecation::cssFunctionMixin,
+ "Sass @function names beginning with -- are deprecated for forward-compatibility with plain CSS mixins.\n\nFor details, see https://sass-lang.com/d/css-function-mixin",
+ $this->scanner->spanFrom($beforeName)
+ );
+ }
+
+ $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->functionChild(...),
+ $start,
+ fn(array $children, FileSpan $span) => 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
+ *
+ * @param-immediately-invoked-callable $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
+ *
+ * @param-immediately-invoked-callable $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::fromWindowsPath($url);
+ }
+
+ Uri::new($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 (str_ends_with($url, '.css')) {
+ return true;
+ }
+
+ if ($url[0] === '/') {
+ return $url[1] === '/';
+ }
+
+ if ($url[0] !== 'h') {
+ return false;
+ }
+
+ return str_starts_with($url, 'http://') || str_starts_with($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();
+ }
+
+ $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->statement(...), $start, fn(array $children, FileSpan $span) => 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->statement(...), $start, fn(array $children, FileSpan $span) => 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;
+
+ $beforeName = $this->scanner->getPosition();
+ $name = $this->identifier();
+
+ if (str_starts_with($name, '--')) {
+ LoggerUtil::warnForDeprecation(
+ $this->logger,
+ Deprecation::cssFunctionMixin,
+ "Sass @mixin names beginning with -- are deprecated for forward-compatibility with plain CSS mixins.\n\nFor details, see https://sass-lang.com/d/css-function-mixin",
+ $this->scanner->spanFrom($beforeName)
+ );
+ }
+
+ $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->statement(...), $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 (!str_ends_with($trailing, 'url-prefix()') && !str_ends_with($trailing, "url-prefix('')") && !str_ends_with($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->statement(...), $start, function (array $children, FileSpan $span) use ($name, $value, $needsDeprecationWarning) {
+ if ($needsDeprecationWarning) {
+ LoggerUtil::warnForDeprecation($this->logger, Deprecation::mozDocument, "@-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.", $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->statement(...), $start, fn(array $children, FileSpan $span) => 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
+ *
+ * @param-immediately-invoked-callable $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->interpolatedDeclarationValue(allowOpenBrace: false);
+ }
+
+ if ($this->lookingAtChildren()) {
+ $rule = $this->withChildren($this->statement(...), $start, fn(array $children, FileSpan $span) => 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.
+ */
+ private function disallowedAtRule(int $start): never
+ {
+ $this->interpolatedDeclarationValue(allowEmpty: true, allowOpenBrace: false);
+ $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();
+ $wasInExpression = $this->inExpression;
+ $wasInParentheses = $this->inParentheses;
+ $this->inExpression = true;
+
+ /**
+ * @var list<Expression>|null $commaExpressions
+ */
+ $commaExpressions = null;
+ /**
+ * @var list<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.
+ *
+ * @var list<BinaryOperator>|null $operators
+ */
+ $operators = null;
+ /**
+ * The left-hand sides of $operators. `$operands[n]` is the left-hand side
+ * of `$operators[n]`.
+ *
+ * @var list<Expression>|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->getOperator()), \strlen($operator->getOperator()));
+ }
+
+ 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->getOperator()
+ && Character::isWhitespace($this->scanner->getString()[$left->getSpan()->getEnd()->getOffset()])
+ ) {
+ $operatorText = $operator->getOperator();
+ $message = <<<WARNING
+This operation is parsed as:
+
+ $left $operatorText $right
+
+but you may have intended it to mean:
+
+ $left ($operatorText$right)
+
+Add a space after $operatorText to clarify that it's meant to be a binary operation, or wrap
+it in parentheses to make it a unary operation. This will be an error in future
+versions of Sass.
+
+More info and automated migrator: https://sass-lang.com/d/strict-unary
+WARNING;
+
+ LoggerUtil::warnForDeprecation($this->logger, Deprecation::strictUnary, $message, $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 = function (BinaryOperator $operator) use (&$allowSlash, &$operators, &$operands, &$singleExpression, $resolveOneOperation): void {
+ if (
+ $this->isPlainCss()
+ && $operator !== BinaryOperator::SINGLE_EQUALS
+ // These are allowed in calculations, so we have to check them at
+ // evaluation time.
+ && $operator !== BinaryOperator::PLUS
+ && $operator !== BinaryOperator::MINUS
+ && $operator !== BinaryOperator::TIMES
+ && $operator !== BinaryOperator::DIVIDED_BY
+ ) {
+ $this->scanner->error("Operators aren't allowed in plain CSS.", $this->scanner->getPosition() - \strlen($operator->getOperator()), \strlen($operator->getOperator()));
+ }
+
+ $allowSlash = $allowSlash && $operator === BinaryOperator::DIVIDED_BY;
+
+ $operators = $operators ?? [];
+ $operands = $operands ?? [];
+
+ $precedence = $operator->getPrecedence();
+
+ while ($operators && $operators[\count($operators) - 1]->getPrecedence() >= $precedence) {
+ $resolveOneOperation();
+ }
+
+ $operators[] = $operator;
+
+ if ($singleExpression === null) {
+ $this->scanner->error('Expected expression.', $this->scanner->getPosition() - \strlen($operator->getOperator()), \strlen($operator->getOperator()));
+ }
+
+ $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;
+ }
+
+ $this->inExpression = $wasInExpression;
+
+ return new ListExpression($commaExpressions, ListSeparator::COMMA, $this->scanner->spanFrom($beforeBracket ?? $start), $bracketList);
+ }
+
+ if ($bracketList && $spaceExpressions !== null) {
+ $resolveOperations();
+ $this->inExpression = $wasInExpression;
+ 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);
+ }
+ $this->inExpression = $wasInExpression;
+
+ 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(fn() => $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 FunctionExpression || ($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.
+ */
+ protected 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 ?? 1.0, $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.
+ */
+ private function unaryOperatorFor(string $character): ?UnaryOperator
+ {
+ return match ($character) {
+ '+' => UnaryOperator::PLUS,
+ '-' => UnaryOperator::MINUS,
+ '/' => UnaryOperator::DIVIDE,
+ default => 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 && $first !== '+' && $first !== '-');
+ $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::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::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();
+
+ $expression = $this->singleExpression();
+
+ return new UnaryOperationExpression(UnaryOperator::NOT, $expression, $identifier->getSpan()->expand($expression->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, fn() => $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
+ {
+ $normalized = Util::unvendor($name);
+
+ switch ($normalized) {
+ case 'calc':
+ if ($normalized === $name) {
+ return null;
+ }
+
+ // fall through
+ 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)));
+ }
+
+ 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 always stops at curly braces.
+ * - This does not interpret backslashes, since the text is expected to be
+ * re-parsed.
+ * - 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 '/':
+ switch ($this->scanner->peekChar(1)) {
+ case '*':
+ if (!$omitComments) {
+ $buffer->write($this->rawText($this->loudComment(...)));
+ } else {
+ $this->loudComment();
+ }
+ break;
+
+ case '/':
+ if (!$omitComments) {
+ $buffer->write($this->rawText($this->silentComment(...)));
+ } else {
+ $this->silentComment();
+ }
+ break;
+
+ default:
+ $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();
+ $identifier = $this->identifier();
+
+ if (
+ $identifier !== 'url'
+ // This isn't actually a standard CSS feature, but it was
+ // supported by the old `@document` rule, so we continue to support
+ // it for backwards-compatibility.
+ && $identifier !== 'url-prefix'
+ ) {
+ $buffer->write($identifier);
+ continue 2;
+ }
+
+ $contents = $this->tryUrlContents($beforeUrl, $identifier);
+
+ 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.
+ *
+ * If $allowOpenBrace is `false`, this stops at opening curly braces.
+ *
+ * If $silentComments is `true`, this will parse silent comments as
+ * comments. Otherwise, it will preserve two adjacent slashes and emit them
+ * to CSS.
+ *
+ * Unlike {@see declarationValue}, this allows interpolation.
+ */
+ private function interpolatedDeclarationValue(bool $allowEmpty = false, bool $allowSemicolon = false, bool $allowColon = true, bool $allowOpenBrace = true, bool $silentComments = 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 '/':
+ $peekedChar = $this->scanner->peekChar(1);
+
+ if ($peekedChar === '*') {
+ $buffer->write($this->rawText($this->loudComment(...)));
+ } elseif ($peekedChar === '/' && $silentComments) {
+ $this->silentComment();
+ } 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 '{':
+ if (!$allowOpenBrace) {
+ break 2;
+ }
+
+ // Fallthrough
+ case '(':
+ case '[':
+ $bracket = $this->scanner->readChar();
+ $buffer->write($bracket);
+ $brackets[] = Character::opposite($bracket);
+ $wroteNewline = false;
+ break;
+
+ case ')':
+ case '}':
+ case ']':
+ if (empty($brackets)) {
+ break 2;
+ }
+
+ $bracket = array_pop($brackets);
+ $this->scanner->expectChar($bracket);
+ $buffer->write($bracket);
+ $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();
+ $identifier = $this->identifier();
+
+ if (
+ $identifier !== 'url'
+ // This isn't actually a standard CSS feature, but it was
+ // supported by the old `@document` rule, so we continue to support
+ // it for backwards-compatibility.
+ && $identifier !== 'url-prefix'
+ ) {
+ $buffer->write($identifier);
+ $wroteNewline = false;
+ continue 2;
+ }
+
+ $contents = $this->tryUrlContents($beforeUrl, $identifier);
+
+ 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();
+
+ 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');
+ }
+ } elseif ($this->scanIdentifier('not')) {
+ $buffer->write('not ');
+ $this->expectWhitespace();
+ $this->mediaOrInterp($buffer);
+ } else {
+ $buffer->add($this->expressionUntilComparison());
+
+ 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() && str_starts_with($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
+ *
+ * @param-immediately-invoked-callable $child
+ * @param-immediately-invoked-callable $create
+ */
+ 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();
+ $this->assertPublic($result, fn() => $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
+ *
+ * @param-immediately-invoked-callable $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());
+ }
+
+ /**
+ * Adds $expression to $buffer, or if it's an unquoted string adds the
+ * interpolation it contains instead.
+ */
+ private function addOrInject(InterpolationBuffer $buffer, Expression $expression): void
+ {
+ if ($expression instanceof StringExpression && !$expression->hasQuotes()) {
+ $buffer->addInterpolation($expression->getText());
+ } else {
+ $buffer->add($expression);
+ }
+ }
+
+ /**
+ * 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[]
+ *
+ * @param-immediately-invoked-callable $child
+ */
+ 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[]
+ *
+ * @param-immediately-invoked-callable $statement
+ */
+ abstract protected function statements(callable $statement): array;
+}
diff --git a/vendor/scssphp/scssphp/src/SassCallable/BuiltInCallable.php b/vendor/scssphp/scssphp/src/SassCallable/BuiltInCallable.php
new file mode 100644
index 000000000..4c2e79f7f
--- /dev/null
+++ b/vendor/scssphp/scssphp/src/SassCallable/BuiltInCallable.php
@@ -0,0 +1,199 @@
+<?php
+
+/**
+ * SCSSPHP
+ *
+ * @copyright 2012-2020 Leaf Corcoran
+ *
+ * @license http://opensource.org/licenses/MIT MIT
+ *
+ * @link http://scssphp.github.io/scssphp
+ */
+
+namespace ScssPhp\ScssPhp\SassCallable;
+
+use League\Uri\Contracts\UriInterface;
+use ScssPhp\ScssPhp\Ast\Sass\ArgumentDeclaration;
+use ScssPhp\ScssPhp\Exception\SassFormatException;
+use ScssPhp\ScssPhp\Value\SassNull;
+use ScssPhp\ScssPhp\Value\Value;
+
+/**
+ * A callable defined in PHP code.
+ *
+ * Unlike user-defined callables, built-in callables support overloads. They
+ * may declare multiple different callbacks with multiple different sets of
+ * arguments. When the callable is invoked, the first callback with matching
+ * arguments is invoked.
+ *
+ * @internal
+ */
+class BuiltInCallable implements SassCallable
+{
+ private readonly string $name;
+
+ /**
+ * @var list<array{ArgumentDeclaration, callable(list<Value>): Value}>
+ */
+ private readonly array $overloads;
+
+ private readonly bool $acceptsContent;
+
+ /**
+ * Creates a function with a single $arguments declaration and a single
+ * $callback.
+ *
+ * The argument declaration is parsed from $arguments, which should not
+ * include parentheses. Throws a {@see SassFormatException} if parsing fails.
+ *
+ * If passed, $url is the URL of the module in which the function is
+ * defined.
+ *
+ * @param callable(list<Value>): Value $callback
+ *
+ * @throws SassFormatException
+ */
+ public static function function(string $name, string $arguments, callable $callback, ?UriInterface $url = null): BuiltInCallable
+ {
+ return self::parsed(
+ $name,
+ ArgumentDeclaration::parse("@function $name($arguments) {", url: $url),
+ $callback
+ );
+ }
+
+ /**
+ * Creates a mixin with a single $arguments declaration and a single
+ * $callback.
+ *
+ * The argument declaration is parsed from $arguments, which should not
+ * include parentheses. Throws a {@see SassFormatException} if parsing fails.
+ *
+ * If passed, $url is the URL of the module in which the mixin is
+ * defined.
+ *
+ * @param callable(list<Value>): void $callback
+ *
+ * @throws SassFormatException
+ */
+ public static function mixin(string $name, string $arguments, callable $callback, ?UriInterface $url = null, bool $acceptsContent = false): BuiltInCallable
+ {
+ return self::parsed(
+ $name,
+ ArgumentDeclaration::parse("@mixin $name($arguments) {", url: $url),
+ function ($arguments) use ($callback) {
+ $callback($arguments);
+
+ return SassNull::create();
+ },
+ $acceptsContent
+ );
+ }
+
+ /**
+ * Creates a function with multiple implementations.
+ *
+ * Each key/value pair in $overloads defines the argument declaration for
+ * the overload (which should not include parentheses), and the callback to
+ * execute if that argument declaration matches. Throws a
+ * {@see SassFormatException} if parsing fails.
+ *
+ * If passed, $url is the URL of the module in which the function is
+ * defined.
+ *
+ * @param array<string, callable(list<Value>): Value> $overloads
+ *
+ * @throws SassFormatException
+ */
+ public static function overloadedFunction(string $name, array $overloads, ?UriInterface $url = null): BuiltInCallable
+ {
+ $processedOverloads = [];
+
+ foreach ($overloads as $args => $callback) {
+ $processedOverloads[] = [
+ ArgumentDeclaration::parse("@function $name($args) {", url: $url),
+ $callback
+ ];
+ }
+
+ return new BuiltInCallable($name, $processedOverloads, false);
+ }
+
+ /**
+ * Creates a callable with a single $arguments declaration and a single $callback.
+ *
+ * @param callable(list<Value>): Value $callback
+ */
+ private static function parsed(string $name, ArgumentDeclaration $arguments, callable $callback, bool $acceptsContent = false): BuiltInCallable
+ {
+ return new BuiltInCallable($name, [[$arguments, $callback]], $acceptsContent);
+ }
+
+ /**
+ * @param list<array{ArgumentDeclaration, callable(list<Value>): Value}> $overloads
+ */
+ private function __construct(string $name, array $overloads, bool $acceptsContent)
+ {
+ $this->name = $name;
+ $this->overloads = $overloads;
+ $this->acceptsContent = $acceptsContent;
+ }
+
+ public function getName(): string
+ {
+ return $this->name;
+ }
+
+ public function acceptsContent(): bool
+ {
+ return $this->acceptsContent;
+ }
+
+ /**
+ * Returns the argument declaration and PHP callback for the given
+ * positional and named arguments.
+ *
+ * If no exact match is found, finds the closest approximation. Note that this
+ * doesn't guarantee that $positional and $names are valid for the returned
+ * {@see ArgumentDeclaration}.
+ *
+ * @param array<string, mixed> $names Only the keys are relevant
+ *
+ * @return array{ArgumentDeclaration, callable(list<Value>): Value}
+ */
+ public function callbackFor(int $positional, array $names): array
+ {
+ $fuzzyMatch = null;
+ $minMismatchDistance = null;
+
+ foreach ($this->overloads as $overload) {
+ // Ideally, find an exact match.
+ if ($overload[0]->matches($positional, $names)) {
+ return $overload;
+ }
+
+ $mismatchDistance = \count($overload[0]->getArguments()) - $positional;
+
+ if ($minMismatchDistance !== null) {
+ if (abs($mismatchDistance) > $minMismatchDistance) {
+ continue;
+ }
+
+ // If two overloads have the same mismatch distance, favor the overload
+ // that has more arguments.
+ if (abs($mismatchDistance) === abs($minMismatchDistance) && $mismatchDistance < 0) {
+ continue;
+ }
+ }
+
+ $minMismatchDistance = $mismatchDistance;
+ $fuzzyMatch = $overload;
+ }
+
+ if ($fuzzyMatch !== null) {
+ return $fuzzyMatch;
+ }
+
+ throw new \LogicException("BuiltInCallable {$this->name} may not have empty overloads.");
+ }
+}
diff --git a/vendor/scssphp/scssphp/src/SassCallable/PlainCssCallable.php b/vendor/scssphp/scssphp/src/SassCallable/PlainCssCallable.php
new file mode 100644
index 000000000..964fa9301
--- /dev/null
+++ b/vendor/scssphp/scssphp/src/SassCallable/PlainCssCallable.php
@@ -0,0 +1,42 @@
+<?php
+
+/**
+ * SCSSPHP
+ *
+ * @copyright 2012-2020 Leaf Corcoran
+ *
+ * @license http://opensource.org/licenses/MIT MIT
+ *
+ * @link http://scssphp.github.io/scssphp
+ */
+
+namespace ScssPhp\ScssPhp\SassCallable;
+
+use ScssPhp\ScssPhp\Util\Equatable;
+
+/**
+ * A callable that emits a plain CSS function.
+ *
+ * This can't be used for mixins.
+ *
+ * @internal
+ */
+final class PlainCssCallable implements SassCallable, Equatable
+{
+ private readonly string $name;
+
+ public function __construct(string $name)
+ {
+ $this->name = $name;
+ }
+
+ public function getName(): string
+ {
+ return $this->name;
+ }
+
+ public function equals(object $other): bool
+ {
+ return $other instanceof PlainCssCallable && $this->name === $other->name;
+ }
+}
diff --git a/vendor/scssphp/scssphp/src/SassCallable/SassCallable.php b/vendor/scssphp/scssphp/src/SassCallable/SassCallable.php
new file mode 100644
index 000000000..1482b494c
--- /dev/null
+++ b/vendor/scssphp/scssphp/src/SassCallable/SassCallable.php
@@ -0,0 +1,71 @@
+<?php
+
+/**
+ * SCSSPHP
+ *
+ * @copyright 2012-2020 Leaf Corcoran
+ *
+ * @license http://opensource.org/licenses/MIT MIT
+ *
+ * @link http://scssphp.github.io/scssphp
+ */
+
+namespace ScssPhp\ScssPhp\SassCallable;
+
+use ScssPhp\ScssPhp\Value\SassNumber;
+use ScssPhp\ScssPhp\Value\Value;
+
+/**
+ * An interface for functions and mixins that can be invoked from Sass by
+ * passing in arguments.
+ *
+ * When writing custom functions, it's important to make them as user-friendly
+ * and as close to the standards set by Sass's core functions as possible. Some
+ * good guidelines to follow include:
+ *
+ * * Use `Value.assert*` methods, like {@see Value::assertString}, to cast untyped
+ * {@see Value} objects to more specific types. For values from the argument list,
+ * pass in the argument name as well. This ensures that the user gets good
+ * error messages when they pass in the wrong type to your function.
+ *
+ * * Individual classes may have more specific `assert*` methods, like
+ * {@see SassNumber::assertInt}, which should be used when possible.
+ *
+ * * In Sass, every value counts as a list. Functions should avoid casting
+ * values to the `SassList` type, and should use the {@see Value::asList} method
+ * instead.
+ *
+ * * When manipulating values like lists, strings, and numbers that have
+ * metadata (comma versus space separated, bracketed versus unbracketed,
+ * quoted versus unquoted, units), the output metadata should match the input
+ * metadata. For lists, the {@see Value::withListContents} method can be used to do
+ * this automatically.
+ *
+ * * When in doubt, lists should default to comma-separated, strings should
+ * default to quoted, and number should default to unitless.
+ *
+ * * In Sass, lists and strings use one-based indexing and use negative indices
+ * to index from the end of value. Functions should follow these conventions.
+ * The {@see Value::sassIndexToListIndex} and {@see SassString::sassIndexToStringIndex}
+ * methods can be used to do this automatically.
+ *
+ * * String indexes in Sass 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 `"😊"`. Functions should follow this
+ * convention. The {@see SassString::sassIndexToStringIndex} and
+ * {@see SassString::sassIndexToCodePointIndex} methods can be used to do this
+ * automatically, and the {@see SassString::getSassLength} getter can be used to
+ * access a string's length in code points.
+ *
+ * @internal
+ */
+interface SassCallable
+{
+ /**
+ * The callable's name
+ */
+ public function getName(): string;
+}
diff --git a/vendor/scssphp/scssphp/src/SassCallable/UserDefinedCallable.php b/vendor/scssphp/scssphp/src/SassCallable/UserDefinedCallable.php
new file mode 100644
index 000000000..b8c2d71bd
--- /dev/null
+++ b/vendor/scssphp/scssphp/src/SassCallable/UserDefinedCallable.php
@@ -0,0 +1,57 @@
+<?php
+
+/**
+ * SCSSPHP
+ *
+ * @copyright 2012-2020 Leaf Corcoran
+ *
+ * @license http://opensource.org/licenses/MIT MIT
+ *
+ * @link http://scssphp.github.io/scssphp
+ */
+
+namespace ScssPhp\ScssPhp\SassCallable;
+
+use ScssPhp\ScssPhp\Ast\Sass\Statement\CallableDeclaration;
+use ScssPhp\ScssPhp\Evaluation\Environment;
+
+/**
+ * A callback defined in the user's Sass stylesheet.
+ *
+ * @internal
+ */
+final class UserDefinedCallable implements SassCallable
+{
+ private readonly CallableDeclaration $declaration;
+
+ private readonly Environment $environment;
+
+ private readonly bool $inDependency;
+
+ public function __construct(CallableDeclaration $declaration, Environment $environment, bool $inDependency)
+ {
+ $this->declaration = $declaration;
+ $this->environment = $environment;
+ $this->inDependency = $inDependency;
+ }
+
+ public function getDeclaration(): CallableDeclaration
+ {
+ return $this->declaration;
+ }
+
+ public function getEnvironment(): Environment
+ {
+ return $this->environment;
+ }
+
+ public function isInDependency(): bool
+ {
+ return $this->inDependency;
+ }
+
+ public function getName(): string
+ {
+ return $this->declaration->getName();
+ }
+}
diff --git a/vendor/scssphp/scssphp/src/Serializer/SerializeResult.php b/vendor/scssphp/scssphp/src/Serializer/SerializeResult.php
new file mode 100644
index 000000000..1b1327109
--- /dev/null
+++ b/vendor/scssphp/scssphp/src/Serializer/SerializeResult.php
@@ -0,0 +1,29 @@
+<?php
+
+/**
+ * SCSSPHP
+ *
+ * @copyright 2012-2020 Leaf Corcoran
+ *
+ * @license http://opensource.org/licenses/MIT MIT
+ *
+ * @link http://scssphp.github.io/scssphp
+ */
+
+namespace ScssPhp\ScssPhp\Serializer;
+
+use ScssPhp\ScssPhp\SourceMap\SingleMapping;
+
+/**
+ * The result of converting a CSS AST to CSS text.
+ *
+ * @internal
+ */
+final class SerializeResult
+{
+ public function __construct(
+ public readonly string $css,
+ public readonly ?SingleMapping $mapping,
+ ) {
+ }
+}
diff --git a/vendor/scssphp/scssphp/src/Serializer/SerializeVisitor.php b/vendor/scssphp/scssphp/src/Serializer/SerializeVisitor.php
new file mode 100644
index 000000000..4d46e9112
--- /dev/null
+++ b/vendor/scssphp/scssphp/src/Serializer/SerializeVisitor.php
@@ -0,0 +1,1904 @@
+<?php
+
+/**
+ * SCSSPHP
+ *
+ * @copyright 2018-2020 Anthon Pang
+ *
+ * @license http://opensource.org/licenses/MIT MIT
+ *
+ * @link http://scssphp.github.io/scssphp
+ */
+
+namespace ScssPhp\ScssPhp\Serializer;
+
+use ScssPhp\ScssPhp\Ast\AstNode;
+use ScssPhp\ScssPhp\Ast\Css\CssAtRule;
+use ScssPhp\ScssPhp\Ast\Css\CssComment;
+use ScssPhp\ScssPhp\Ast\Css\CssDeclaration;
+use ScssPhp\ScssPhp\Ast\Css\CssImport;
+use ScssPhp\ScssPhp\Ast\Css\CssKeyframeBlock;
+use ScssPhp\ScssPhp\Ast\Css\CssMediaQuery;
+use ScssPhp\ScssPhp\Ast\Css\CssMediaRule;
+use ScssPhp\ScssPhp\Ast\Css\CssNode;
+use ScssPhp\ScssPhp\Ast\Css\CssParentNode;
+use ScssPhp\ScssPhp\Ast\Css\CssStyleRule;
+use ScssPhp\ScssPhp\Ast\Css\CssStylesheet;
+use ScssPhp\ScssPhp\Ast\Css\CssSupportsRule;
+use ScssPhp\ScssPhp\Ast\Css\CssValue;
+use ScssPhp\ScssPhp\Ast\Selector\AttributeSelector;
+use ScssPhp\ScssPhp\Ast\Selector\ClassSelector;
+use ScssPhp\ScssPhp\Ast\Selector\Combinator;
+use ScssPhp\ScssPhp\Ast\Selector\ComplexSelector;
+use ScssPhp\ScssPhp\Ast\Selector\CompoundSelector;
+use ScssPhp\ScssPhp\Ast\Selector\IDSelector;
+use ScssPhp\ScssPhp\Ast\Selector\ParentSelector;
+use ScssPhp\ScssPhp\Ast\Selector\PlaceholderSelector;
+use ScssPhp\ScssPhp\Ast\Selector\PseudoSelector;
+use ScssPhp\ScssPhp\Ast\Selector\SelectorList;
+use ScssPhp\ScssPhp\Ast\Selector\TypeSelector;
+use ScssPhp\ScssPhp\Ast\Selector\UniversalSelector;
+use ScssPhp\ScssPhp\Colors;
+use ScssPhp\ScssPhp\Deprecation;
+use ScssPhp\ScssPhp\Exception\SassScriptException;
+use ScssPhp\ScssPhp\Logger\LoggerInterface;
+use ScssPhp\ScssPhp\Logger\QuietLogger;
+use ScssPhp\ScssPhp\OutputStyle;
+use ScssPhp\ScssPhp\Parser\LineScanner;
+use ScssPhp\ScssPhp\Parser\Parser;
+use ScssPhp\ScssPhp\Parser\StringScanner;
+use ScssPhp\ScssPhp\SourceSpan\MultiSpan;
+use ScssPhp\ScssPhp\Util\Character;
+use ScssPhp\ScssPhp\Util\IterableUtil;
+use ScssPhp\ScssPhp\Util\LoggerUtil;
+use ScssPhp\ScssPhp\Util\NumberUtil;
+use ScssPhp\ScssPhp\Util\SpanUtil;
+use ScssPhp\ScssPhp\Util\StringUtil;
+use ScssPhp\ScssPhp\Value\CalculationOperation;
+use ScssPhp\ScssPhp\Value\CalculationOperator;
+use ScssPhp\ScssPhp\Value\ColorFormatEnum;
+use ScssPhp\ScssPhp\Value\ListSeparator;
+use ScssPhp\ScssPhp\Value\SassBoolean;
+use ScssPhp\ScssPhp\Value\SassCalculation;
+use ScssPhp\ScssPhp\Value\SassColor;
+use ScssPhp\ScssPhp\Value\SassFunction;
+use ScssPhp\ScssPhp\Value\SassList;
+use ScssPhp\ScssPhp\Value\SassMap;
+use ScssPhp\ScssPhp\Value\SassMixin;
+use ScssPhp\ScssPhp\Value\SassNumber;
+use ScssPhp\ScssPhp\Value\SassString;
+use ScssPhp\ScssPhp\Value\SpanColorFormat;
+use ScssPhp\ScssPhp\Value\Value;
+use ScssPhp\ScssPhp\Visitor\CssVisitor;
+use ScssPhp\ScssPhp\Visitor\SelectorVisitor;
+use ScssPhp\ScssPhp\Visitor\ValueVisitor;
+
+/**
+ * @internal
+ *
+ * @template-implements CssVisitor<void>
+ * @template-implements ValueVisitor<void>
+ * @template-implements SelectorVisitor<void>
+ */
+final class SerializeVisitor implements CssVisitor, ValueVisitor, SelectorVisitor
+{
+ private readonly SourceMapBuffer $buffer;
+
+ /**
+ * The current indentation of the CSS output.
+ *
+ * @var int
+ */
+ private int $indentation = 0;
+
+ /**
+ * Whether we're emitting an unambiguous representation of the source
+ * structure, as opposed to valid CSS.
+ */
+ private readonly bool $inspect;
+
+ /**
+ * Whether quoted strings should be emitted with quotes.
+ */
+ private readonly bool $quote;
+
+ private readonly LoggerInterface $logger;
+
+ private readonly bool $compressed;
+
+ public function __construct(bool $inspect = false, bool $quote = true, OutputStyle $style = OutputStyle::EXPANDED, bool $sourceMap = false, ?LoggerInterface $logger = null)
+ {
+ $this->buffer = $sourceMap ? new TrackingSourceMapBuffer() : new SimpleStringBuffer();
+ $this->inspect = $inspect;
+ $this->quote = $quote;
+ $this->logger = $logger ?? new QuietLogger();
+ $this->compressed = $style === OutputStyle::COMPRESSED;
+ }
+
+ public function getBuffer(): SourceMapBuffer
+ {
+ return $this->buffer;
+ }
+
+ public function visitCssStylesheet(CssStylesheet $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(CssComment $node): void
+ {
+ $this->for($node, function () use ($node) {
+ // Preserve comments that start with `/*!`.
+ if ($this->compressed && !$node->isPreserved()) {
+ return;
+ }
+
+ // Ignore sourceMappingURL and sourceURL comments.
+ if (preg_match('{^/\*# source(Mapping)?URL=}', $node->getText())) {
+ 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(CssAtRule $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(CssMediaRule $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) && str_starts_with($firstQuery->getConditions()[0], '(not ')) {
+ $this->buffer->writeChar(' ');
+ }
+
+ $this->writeBetween($node->getQueries(), $this->getCommaSeparator(), $this->visitMediaQuery(...));
+ });
+
+ $this->writeOptionalSpace();
+ $this->visitChildren($node);
+ }
+
+ public function visitCssImport(CssImport $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(CssKeyframeBlock $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 && str_starts_with($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(CssStyleRule $node): void
+ {
+ $this->writeIndentation();
+
+ $this->for($node->getSelector(), function () use ($node) {
+ $node->getSelector()->accept($this);
+ });
+ $this->writeOptionalSpace();
+ $this->visitChildren($node);
+ }
+
+ public function visitCssSupportsRule(CssSupportsRule $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(CssDeclaration $node): void
+ {
+ if ($node->getInterleavedRules() !== []) {
+ \assert($node->getParent() !== null);
+ $declSpecificities = $this->specificities($node->getParent());
+
+ foreach ($node->getInterleavedRules() as $rule) {
+ $ruleSpecificities = $this->specificities($rule);
+
+ // If the declaration can never match with the same specificity as one
+ // of its sibling rules, then ordering will never matter and there's no
+ // need to warn about the declaration being re-ordered.
+ if (!IterableUtil::any($declSpecificities, fn ($s) => \in_array($s, $ruleSpecificities, true))) {
+ continue;
+ }
+
+ LoggerUtil::warnForDeprecation(
+ $this->logger,
+ Deprecation::mixedDecls,
+ <<<'MESSAGE'
+ Sass's behavior for declarations that appear after nested
+ rules will be changing to match the behavior specified by CSS in an upcoming
+ version. To keep the existing behavior, move the declaration above the nested
+ rule. To opt into the new behavior, wrap the declaration in `& {}`.
+
+ More info: https://sass-lang.com/d/mixed-decls
+ MESSAGE,
+ new MultiSpan($node->getSpan(), 'declaration', [
+ 'nested rule' => $rule->getSpan(),
+ ]),
+ $node->getTrace()
+ );
+ }
+ }
+
+ $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 {
+ $this->buffer->forSpan($node->getValueSpanForMap(), fn () => $node->getValue()->getValue()->accept($this));
+ } catch (SassScriptException $error) {
+ throw $error->withSpan($node->getValue()->getSpan());
+ }
+ }
+ }
+
+ /**
+ * Returns the set of possible specificities with which $node might match.
+ *
+ * @return non-empty-array<int>
+ */
+ private function specificities(CssParentNode $node): array
+ {
+ if ($node instanceof CssStyleRule) {
+ // Plain CSS style rule nesting implicitly wraps parent selectors in
+ // `:is()`, so they all match with the highest specificity among any of
+ // them.
+ if ($node->getParent() !== null) {
+ $parent = max($this->specificities($node->getParent()));
+ } else {
+ $parent = 0;
+ }
+
+ return array_map(fn (ComplexSelector $selector) => $parent + $selector->getSpecificity(), $node->getSelector()->getComponents());
+ }
+
+ if ($node->getParent() !== null) {
+ return $this->specificities($node->getParent());
+ }
+
+ return [0];
+ }
+
+ /**
+ * 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): void
+ {
+ $this->buffer->write($value->getValue() ? 'true' : 'false');
+ }
+
+ public function visitCalculation(SassCalculation $value): void
+ {
+ $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 SassNumber && $value->hasComplexUnits() && !$this->inspect) {
+ throw new SassScriptException("$value is not a valid CSS value.");
+ }
+ if ($value instanceof SassNumber && !is_finite($value->getValue())) {
+ if (is_nan($value->getValue())) {
+ $this->buffer->write('NaN');
+ } elseif ($value->getValue() > 0) {
+ $this->buffer->write('infinity');
+ } else {
+ $this->buffer->write('-infinity');
+ }
+
+ $this->writeCalculationUnits($value->getNumeratorUnits(), $value->getDenominatorUnits());
+ } elseif ($value instanceof SassNumber && $value->hasComplexUnits()) {
+ $this->writeNumber($value->getValue());
+
+ $firstUnit = $value->getNumeratorUnits()[0] ?? null;
+
+ if ($firstUnit !== null) {
+ $this->buffer->write($firstUnit);
+ $this->writeCalculationUnits(array_slice($value->getNumeratorUnits(), 1), $value->getDenominatorUnits());
+ } else {
+ $this->writeCalculationUnits([], $value->getDenominatorUnits());
+ }
+ } elseif ($value instanceof Value) {
+ $value->accept($this);
+ } elseif ($value instanceof CalculationOperation) {
+ $left = $value->getLeft();
+ $parenthesizeLeft = $left instanceof CalculationOperation && $left->getOperator()->getPrecedence() < $value->getOperator()->getPrecedence();
+
+ if ($parenthesizeLeft) {
+ $this->buffer->writeChar('(');
+ }
+ $this->writeCalculationValue($left);
+ if ($parenthesizeLeft) {
+ $this->buffer->writeChar(')');
+ }
+
+ $operatorWhitespace = !$this->compressed || $value->getOperator()->getPrecedence() === 1;
+ if ($operatorWhitespace) {
+ $this->buffer->writeChar(' ');
+ }
+ $this->buffer->write($value->getOperator()->getOperator());
+ if ($operatorWhitespace) {
+ $this->buffer->writeChar(' ');
+ }
+
+ $right = $value->getRight();
+ $parenthesizeRight = ($right instanceof CalculationOperation && $this->parenthesizeCalculationRhs($value->getOperator(), $right->getOperator()))
+ || ($value->getOperator() === CalculationOperator::DIVIDED_BY && $right instanceof SassNumber && (is_finite($right->getValue()) ? $right->hasComplexUnits() : $right->hasUnits()));
+
+ if ($parenthesizeRight) {
+ $this->buffer->writeChar('(');
+ }
+ $this->writeCalculationValue($right);
+ if ($parenthesizeRight) {
+ $this->buffer->writeChar(')');
+ }
+ }
+ }
+
+ /**
+ * Writes the complex numerator and denominator units beyond the first
+ * numerator unit for a number as they appear in a calculation.
+ *
+ * @param list<string> $numeratorUnits
+ * @param list<string> $denominatorUnits
+ */
+ private function writeCalculationUnits(array $numeratorUnits, array $denominatorUnits): void
+ {
+ foreach ($numeratorUnits as $unit) {
+ $this->writeOptionalSpace();
+ $this->buffer->writeChar('*');
+ $this->writeOptionalSpace();
+ $this->buffer->writeChar('1');
+ $this->buffer->write($unit);
+ }
+
+ foreach ($denominatorUnits as $unit) {
+ $this->writeOptionalSpace();
+ $this->buffer->writeChar('/');
+ $this->writeOptionalSpace();
+ $this->buffer->writeChar('1');
+ $this->buffer->write($unit);
+ }
+ }
+
+ /**
+ * Returns whether the right-hand operation of a calculation should be
+ * parenthesized.
+ *
+ * In `a ? (b # c)`, `outer` is `?` and `right` is `#`.
+ */
+ private function parenthesizeCalculationRhs(CalculationOperator $outer, CalculationOperator $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): void
+ {
+ $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 === ColorFormatEnum::rgbFunction) {
+ $this->writeRgb($value);
+ } elseif ($format === ColorFormatEnum::hslFunction) {
+ $this->writeHsl($value);
+ } elseif ($format instanceof SpanColorFormat) {
+ $this->buffer->write($format->getOriginal());
+ } else {
+ // should not happen as our interface is sealed.
+ \assert(false, 'unknown format');
+ }
+ } 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($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): void
+ {
+ if (!$this->inspect) {
+ throw new SassScriptException("$value is not a valid CSS value.");
+ }
+
+ $this->buffer->write('get-function(');
+ $this->visitQuotedString($value->getCallable()->getName());
+ $this->buffer->writeChar(')');
+ }
+
+ public function visitMixin(SassMixin $value): void
+ {
+ if (!$this->inspect) {
+ throw new SassScriptException("$value is not a valid CSS value.");
+ }
+
+ $this->buffer->write('get-mixin(');
+ $this->visitQuotedString($value->getCallable()->getName());
+ $this->buffer->writeChar(')');
+ }
+
+ public function visitList(SassList $value): void
+ {
+ 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) {
+ \assert($value->getSeparator()->getSeparator() !== null, 'The list separator is not undecided at that point.');
+ $this->buffer->write($value->getSeparator()->getSeparator());
+
+ if (!$value->hasBrackets()) {
+ $this->buffer->writeChar(')');
+ }
+ }
+
+ if ($value->hasBrackets()) {
+ $this->buffer->writeChar(']');
+ }
+ }
+
+ private function separatorString(ListSeparator $separator): string
+ {
+ return match ($separator) {
+ ListSeparator::COMMA => $this->getCommaSeparator(),
+ ListSeparator::SLASH => $this->compressed ? '/' : ' / ',
+ ListSeparator::SPACE => ' ',
+ /**
+ * 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.
+ */
+ default => '',
+ };
+ }
+
+ /**
+ * Returns whether the value needs parentheses as an element in a list with the given separator.
+ */
+ private static function elementNeedsParens(ListSeparator $separator, Value $value): bool
+ {
+ if (!$value instanceof SassList) {
+ return false;
+ }
+
+ if (count($value->asList()) < 2) {
+ return false;
+ }
+
+ if ($value->hasBrackets()) {
+ return false;
+ }
+
+ return match ($separator) {
+ ListSeparator::COMMA => $value->getSeparator() === ListSeparator::COMMA,
+ ListSeparator::SLASH => $value->getSeparator() === ListSeparator::COMMA || $value->getSeparator() === ListSeparator::SLASH,
+ default => $value->getSeparator() !== ListSeparator::UNDECIDED,
+ };
+ }
+
+ public function visitMap(SassMap $value): void
+ {
+ 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(): void
+ {
+ if ($this->inspect) {
+ $this->buffer->write('null');
+ }
+ }
+
+ public function visitNumber(SassNumber $value): void
+ {
+ $asSlash = $value->getAsSlash();
+
+ if ($asSlash !== null) {
+ $this->visitNumber($asSlash[0]);
+ $this->buffer->writeChar('/');
+ $this->visitNumber($asSlash[1]);
+
+ return;
+ }
+
+ if (!is_finite($value->getValue())) {
+ $this->visitCalculation(SassCalculation::unsimplified('calc', [$value]));
+ return;
+ }
+
+ if ($value->hasComplexUnits()) {
+ if (!$this->inspect) {
+ throw new SassScriptException("$value is not a valid CSS value.");
+ }
+
+ $this->visitCalculation(SassCalculation::unsimplified('calc', [$value]));
+ } else {
+ $this->writeNumber($value->getValue());
+
+ if (\count($value->getNumeratorUnits()) > 0) {
+ $this->buffer->write($value->getNumeratorUnits()[0]);
+ }
+ }
+ }
+
+ /**
+ * Writes $number without exponent notation and with at most
+ * {@see SassNumber::PRECISION} digits after the decimal point.
+ */
+ 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;
+ }
+
+
+ $text = $this->removeExponent((string) $number);
+
+ // Any double that's less than `SassNumber.precision + 2` digits long is
+ // guaranteed to be safe to emit directly, since it'll contain at most `0.`
+ // followed by [SassNumber.precision] digits.
+ $canWriteDirectly = \strlen($text) < SassNumber::PRECISION + 2;
+
+ if ($canWriteDirectly) {
+ if ($this->compressed && $text[0] === '0') {
+ $text = substr($text, 1);
+ }
+
+ $this->buffer->write($text);
+ return;
+ }
+
+ $this->writeRounded($text);
+ }
+
+ /**
+ * If $text is written in exponent notation, returns a string representation
+ * of it without exponent notation.
+ *
+ * Otherwise, returns $text as-is.
+ */
+ private function removeExponent(string $text): string
+ {
+ $exponentDelimiterPosition = strpos($text, 'E');
+
+ if ($exponentDelimiterPosition === false) {
+ return $text;
+ }
+
+ $negative = $text[0] === '-';
+
+ $buffer = $text[0];
+
+ // If the number has more than one significant digit, the second
+ // character will be a decimal point that we don't want to include in
+ // the generated number.
+ if ($negative) {
+ $buffer .= $text[1];
+
+ if ($exponentDelimiterPosition > 3) {
+ $buffer .= substr($text, 3, $exponentDelimiterPosition - 3);
+ }
+ } elseif ($exponentDelimiterPosition > 2) {
+ $buffer .= substr($text, 2, $exponentDelimiterPosition - 2);
+ }
+
+ $exponent = intval(substr($text, $exponentDelimiterPosition + 1));
+
+ if ($exponent > 0) {
+ // Write an additional zero for each exponent digits other than those
+ // already written to the buffer. We subtract 1 from `buffer.length`
+ // because the first digit doesn't count towards the exponent. Subtract 1
+ // more for negative numbers because of the `-` written to the buffer.
+ $additionalZeroes = $exponent - (\strlen($buffer) - 1 - ($negative ? 1 : 0));
+ $buffer .= str_repeat('0', $additionalZeroes);
+
+ return $buffer;
+ }
+
+ $result = '';
+ if ($negative) {
+ $result .= '-';
+ }
+ $result .= '0.';
+ for ($i = -1; $i > $exponent; --$i) {
+ $result .= '0';
+ }
+
+ $result .= $negative ? substr($buffer, 1) : $buffer;
+
+ return $result;
+ }
+
+ /**
+ * Assuming $text is a number written without exponent notation, rounds it
+ * to {@see SassNumber::PRECISION} digits after the decimal and writes the result
+ * to {@see $buffer}.
+ */
+ private function writeRounded(string $text): void
+ {
+ \assert(preg_match('/^-?\d+(\.\d+)?$/D', $text) === 1, "\"$text\" should be a number written without exponent notation.");
+
+ // We need to ensure that we write at most [SassNumber.precision] digits
+ // after the decimal point, and that we round appropriately if necessary. To
+ // do this, we maintain an intermediate buffer of digits (both before and
+ // after the decimal point), which we then write to [_buffer] as text. We
+ // start writing after the first digit to give us room to round up to a
+ // higher decimal place than was represented in the original number.
+ $digits = array_fill(0, \strlen($text) + 1, 0);
+ $digitsIndex = 1;
+
+ // Write the digits before the decimal to $digits.
+ $textIndex = 0;
+ $negative = $text[0] === '-';
+ if ($negative) {
+ $textIndex++;
+ }
+
+ while (true) {
+ if ($textIndex === \strlen($text)) {
+ // If we get here, $text has no decimal point. It definitely doesn't
+ // need to be rounded; we can write it as-is.
+ $this->buffer->write($text);
+ return;
+ }
+
+ $codeUnit = $text[$textIndex++];
+ if ($codeUnit === '.') {
+ break;
+ }
+
+ $digits[$digitsIndex++] = intval($codeUnit);
+ }
+
+ $firstFractionalDigit = $digitsIndex;
+
+ // Only write at most PRECISION digits after the decimal. If there aren't
+ // that many digits left in the number, write it as-is since no rounding or
+ // truncation is needed.
+ $indexAfterPrecision = $textIndex + SassNumber::PRECISION;
+ if ($indexAfterPrecision >= \strlen($text)) {
+ $this->buffer->write($text);
+ return;
+ }
+
+ // Write the digits after the decimal to $digits.
+ while ($textIndex < $indexAfterPrecision) {
+ $digits[$digitsIndex++] = intval($text[$textIndex++]);
+ }
+
+ // Round the trailing digits in $digits up if necessary.
+ if (intval($text[$textIndex]) >= 5) {
+ while (true) {
+ // $digitsIndex is guaranteed to be >0 here because we added a leading
+ // 0 to $digits when we constructed it, so even if we round everything
+ // up $newDigit will always be 1 when $digitsIndex is 1.
+ $newDigit = ++$digits[$digitsIndex - 1];
+
+ if ($newDigit !== 10) {
+ break;
+ }
+ $digitsIndex--;
+ }
+ }
+
+ // At most one of the following loops will actually execute. If we rounded
+ // digits up before the decimal point, the first loop will set those digits
+ // to 0 (rather than 10, which is not a valid decimal digit). On the other
+ // hand, if we have trailing zeros left after the decimal point, the second
+ // loop will move $digitsIndex before them and cause them not to be
+ // written. Either way, $digitsIndex will end up >= $firstFractionalDigit.
+ for (; $digitsIndex < $firstFractionalDigit; $digitsIndex++) {
+ $digits[$digitsIndex] = 0;
+ }
+ while ($digitsIndex > $firstFractionalDigit && $digits[$digitsIndex - 1] === 0) {
+ $digitsIndex--;
+ }
+
+ // Omit the minus sign if the number ended up being rounded to exactly zero,
+ // write "0" explicit to avoid adding a minus sign or omitting the number
+ // entirely in compressed mode.
+ if ($digitsIndex === 2 && $digits[0] === 0 && $digits[1] == 0) {
+ $this->buffer->writeChar('0');
+ return;
+ }
+
+ if ($negative) {
+ $this->buffer->writeChar('-');
+ }
+
+ // Write the digits before the decimal point to $buffer. Omit the leading
+ // 0 that's added to $digits to accommodate rounding, and in compressed
+ // mode omit the 0 before the decimal point as well.
+ $writtenIndex = 0;
+
+ if ($digits[0] === 0) {
+ $writtenIndex++;
+ if ($this->compressed && $digits[1] === 0) {
+ $writtenIndex++;
+ }
+ }
+
+ for (; $writtenIndex < $firstFractionalDigit; $writtenIndex++) {
+ $this->buffer->writeChar((string) $digits[$writtenIndex]);
+ }
+
+ if ($digitsIndex > $firstFractionalDigit) {
+ $this->buffer->writeChar('.');
+
+ for (; $writtenIndex < $digitsIndex; $writtenIndex++) {
+ $this->buffer->writeChar((string) $digits[$writtenIndex]);
+ }
+ }
+ }
+
+ public function visitString(SassString $value): void
+ {
+ if ($this->quote && $value->hasQuotes()) {
+ $this->visitQuotedString($value->getText());
+ } else {
+ $this->visitUnquotedString($value->getText());
+ }
+ }
+
+ private function visitQuotedString(string $string): void
+ {
+ $includesDoubleQuote = str_contains($string, '"');
+ $includesSingleQuote = str_contains($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 "\x10":
+ 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":
+ case "\x7F":
+ $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(SourceMapBuffer $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 = mb_ord($fullChar, 'UTF-8');
+ } 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(SourceMapBuffer $buffer, string $character, string $string, int $i): void
+ {
+ $buffer->writeChar('\\');
+ $buffer->write(dechex(mb_ord($character, 'UTF-8')));
+
+ 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): void
+ {
+ $this->buffer->writeChar('[');
+ $this->buffer->write($attribute->getName());
+
+ $value = $attribute->getValue();
+
+ if ($value !== null) {
+ assert($attribute->getOp() !== null);
+ $this->buffer->write($attribute->getOp()->getText());
+
+ // Emit identifiers that start with `--` with quotes, because IE11
+ // doesn't consider them to be valid identifiers.
+ if (Parser::isIdentifier($value) && !str_starts_with($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): void
+ {
+ $this->buffer->writeChar('.');
+ $this->buffer->write($klass->getName());
+ }
+
+ public function visitComplexSelector(ComplexSelector $complex): void
+ {
+ $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 list<CssValue<Combinator>> $combinators
+ */
+ private function writeCombinators(array $combinators): void
+ {
+ $this->writeBetween($combinators, $this->compressed ? '' : ' ', function ($text) {
+ $this->buffer->write($text);
+ });
+ }
+
+ public function visitCompoundSelector(CompoundSelector $compound): void
+ {
+ $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): void
+ {
+ $this->buffer->writeChar('#');
+ $this->buffer->write($id->getName());
+ }
+
+ public function visitSelectorList(SelectorList $list): void
+ {
+ $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();
+ $this->writeIndentation();
+ } else {
+ $this->writeOptionalSpace();
+ }
+ }
+
+ $this->visitComplexSelector($complex);
+ }
+ }
+
+ public function visitParentSelector(ParentSelector $parent): void
+ {
+ $this->buffer->writeChar('&');
+
+ if ($parent->getSuffix() !== null) {
+ $this->buffer->write($parent->getSuffix());
+ }
+ }
+
+ public function visitPlaceholderSelector(PlaceholderSelector $placeholder): void
+ {
+ $this->buffer->writeChar('%');
+ $this->buffer->write($placeholder->getName());
+ }
+
+ public function visitPseudoSelector(PseudoSelector $pseudo): void
+ {
+ $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): void
+ {
+ $this->buffer->write($type->getName());
+ }
+
+ public function visitUniversalSelector(UniversalSelector $universal): void
+ {
+ 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 callable(): T $callback
+ *
+ * @return T
+ *
+ * @param-immediately-invoked-callable $callback
+ */
+ private function for(AstNode $node, callable $callback)
+ {
+ return $this->buffer->forSpan($node->getSpan(), $callback);
+ }
+
+ /**
+ * @param CssValue<string> $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->withoutIndentation(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 ($node->getSpan()->getSourceUrl() !== $previous->getSpan()->getSourceUrl()) {
+ 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;
+ }
+
+ $previousSpanText = $previous->getSpan()->getText();
+ $endOffset = strrpos($previousSpanText, '{', $searchFrom - \strlen($previousSpanText));
+ 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<T> $iterable
+ * @param callable(T): void $callback
+ *
+ * @param-immediately-invoked-callable $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
+ *
+ * @param-immediately-invoked-callable $callback
+ */
+ private function indent(callable $callback): void
+ {
+ $this->indentation++;
+ $callback();
+ $this->indentation--;
+ }
+
+ /**
+ * Runs $callback without any indentation.
+ *
+ * @param callable(): void $callback
+ *
+ * @param-immediately-invoked-callable $callback
+ */
+ private function withoutIndentation(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/vendor/scssphp/scssphp/src/Serializer/Serializer.php b/vendor/scssphp/scssphp/src/Serializer/Serializer.php
new file mode 100644
index 000000000..385020f88
--- /dev/null
+++ b/vendor/scssphp/scssphp/src/Serializer/Serializer.php
@@ -0,0 +1,82 @@
+<?php
+
+/**
+ * SCSSPHP
+ *
+ * @copyright 2018-2020 Anthon Pang
+ *
+ * @license http://opensource.org/licenses/MIT MIT
+ *
+ * @link http://scssphp.github.io/scssphp
+ */
+
+namespace ScssPhp\ScssPhp\Serializer;
+
+use ScssPhp\ScssPhp\Ast\Css\CssNode;
+use ScssPhp\ScssPhp\Ast\Selector\Selector;
+use ScssPhp\ScssPhp\Exception\SassScriptException;
+use ScssPhp\ScssPhp\Logger\LoggerInterface;
+use ScssPhp\ScssPhp\OutputStyle;
+use ScssPhp\ScssPhp\Value\Value;
+
+/**
+ * @internal
+ */
+final class Serializer
+{
+ public static function serialize(CssNode $node, bool $inspect = false, OutputStyle $style = OutputStyle::EXPANDED, bool $sourceMap = false, bool $charset = true, ?LoggerInterface $logger = null): SerializeResult
+ {
+ $visitor = new SerializeVisitor($inspect, true, $style, $sourceMap, $logger);
+ $node->accept($visitor);
+ $css = (string) $visitor->getBuffer();
+
+ $prefix = '';
+
+ if ($charset && strlen($css) !== mb_strlen($css, 'UTF-8')) {
+ if ($style === OutputStyle::COMPRESSED) {
+ $prefix = "\u{FEFF}";
+ } else {
+ $prefix = '@charset "UTF-8";' . "\n";
+ }
+ }
+
+ return new SerializeResult(
+ $prefix . $css,
+ $sourceMap ? $visitor->getBuffer()->buildSourceMap($prefix) : null,
+ );
+ }
+
+ /**
+ * 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/vendor/scssphp/scssphp/src/Serializer/SimpleStringBuffer.php b/vendor/scssphp/scssphp/src/Serializer/SimpleStringBuffer.php
new file mode 100644
index 000000000..38b976c05
--- /dev/null
+++ b/vendor/scssphp/scssphp/src/Serializer/SimpleStringBuffer.php
@@ -0,0 +1,60 @@
+<?php
+
+/**
+ * SCSSPHP
+ *
+ * @copyright 2018-2020 Anthon Pang
+ *
+ * @license http://opensource.org/licenses/MIT MIT
+ *
+ * @link http://scssphp.github.io/scssphp
+ */
+
+namespace ScssPhp\ScssPhp\Serializer;
+
+use ScssPhp\ScssPhp\SourceMap\SingleMapping;
+use SourceSpan\FileSpan;
+
+/**
+ * A buffer that doesn't actually build a source map.
+ *
+ * We implement {@see SourceMapBuffer} directly on SimpleStringBuffer to avoid
+ * an unnecessary wrapper for NoSourceMapBuffer (dart-sass has to make a wrapper
+ * because StringBuffer comes from dart core).
+ *
+ * @internal
+ */
+final class SimpleStringBuffer implements SourceMapBuffer
+{
+ private string $text = '';
+
+ public function getLength(): int
+ {
+ return \strlen($this->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;
+ }
+
+ public function forSpan(FileSpan $span, callable $callback)
+ {
+ return $callback();
+ }
+
+ public function buildSourceMap(?string $prefix): SingleMapping
+ {
+ throw new \BadMethodCallException(__METHOD__ . ' is not supported.');
+ }
+}
diff --git a/vendor/scssphp/scssphp/src/Serializer/SourceMapBuffer.php b/vendor/scssphp/scssphp/src/Serializer/SourceMapBuffer.php
new file mode 100644
index 000000000..08b17fac3
--- /dev/null
+++ b/vendor/scssphp/scssphp/src/Serializer/SourceMapBuffer.php
@@ -0,0 +1,43 @@
+<?php
+
+/**
+ * SCSSPHP
+ *
+ * @copyright 2018-2020 Anthon Pang
+ *
+ * @license http://opensource.org/licenses/MIT MIT
+ *
+ * @link http://scssphp.github.io/scssphp
+ */
+
+namespace ScssPhp\ScssPhp\Serializer;
+
+use ScssPhp\ScssPhp\SourceMap\SingleMapping;
+use SourceSpan\FileSpan;
+
+/**
+ * @internal
+ */
+interface SourceMapBuffer extends StringBuffer
+{
+ /**
+ * Runs $callback and associates all text written within it with $span.
+ *
+ * Specifically, this associates the point at the beginning of the written
+ * text with {@see FileSpan::getStart()} and the point at the end of the
+ * written text with {@see FileSpan::getEnd()}.
+ *
+ * @template T
+ * @param callable(): T $callback
+ * @return T
+ */
+ public function forSpan(FileSpan $span, callable $callback);
+
+ /**
+ * Returns the source map for the file being written.
+ *
+ * If $prefix is passed, all the entries in the source map will be moved
+ * forward by the number of characters and lines in $prefix.
+ */
+ public function buildSourceMap(?string $prefix): SingleMapping;
+}
diff --git a/vendor/scssphp/scssphp/src/Serializer/StringBuffer.php b/vendor/scssphp/scssphp/src/Serializer/StringBuffer.php
new file mode 100644
index 000000000..a4c9a881e
--- /dev/null
+++ b/vendor/scssphp/scssphp/src/Serializer/StringBuffer.php
@@ -0,0 +1,31 @@
+<?php
+
+/**
+ * SCSSPHP
+ *
+ * @copyright 2018-2020 Anthon Pang
+ *
+ * @license http://opensource.org/licenses/MIT MIT
+ *
+ * @link http://scssphp.github.io/scssphp
+ */
+
+namespace ScssPhp\ScssPhp\Serializer;
+
+/**
+ * @internal
+ */
+interface StringBuffer extends \Stringable
+{
+ /**
+ * Returns the length of the content that has been accumulated so far.
+ */
+ public function getLength(): int;
+
+ public function write(string $string): void;
+
+ /**
+ * Writes a single char to the buffer.
+ */
+ public function writeChar(string $char): void;
+}
diff --git a/vendor/scssphp/scssphp/src/Serializer/TrackingSourceMapBuffer.php b/vendor/scssphp/scssphp/src/Serializer/TrackingSourceMapBuffer.php
new file mode 100644
index 000000000..f8fdf22e8
--- /dev/null
+++ b/vendor/scssphp/scssphp/src/Serializer/TrackingSourceMapBuffer.php
@@ -0,0 +1,194 @@
+<?php
+
+/**
+ * SCSSPHP
+ *
+ * @copyright 2018-2020 Anthon Pang
+ *
+ * @license http://opensource.org/licenses/MIT MIT
+ *
+ * @link http://scssphp.github.io/scssphp
+ */
+
+namespace ScssPhp\ScssPhp\Serializer;
+
+use ScssPhp\ScssPhp\SourceMap\Builder\Entry;
+use ScssPhp\ScssPhp\SourceMap\SingleMapping;
+use ScssPhp\ScssPhp\Util\ListUtil;
+use SourceSpan\FileLocation;
+use SourceSpan\FileSpan;
+use SourceSpan\SimpleSourceLocation;
+use SourceSpan\SourceLocation;
+use SourceSpan\SourceSpan;
+
+/**
+ * A {@see SourceMapBuffer} that builds a source map.
+ *
+ * @internal
+ */
+final class TrackingSourceMapBuffer implements SourceMapBuffer
+{
+ private readonly StringBuffer $buffer;
+ /**
+ * @var list<Entry>
+ */
+ private array $entries = [];
+ /**
+ * The index of the current line in {@see $buffer}.
+ */
+ private int $line = 0;
+ /**
+ * The index of the current column in {@see $buffer}.
+ */
+ private int $column = 0;
+ /**
+ * Whether the text currently being written should be encompassed by a
+ * {@see SourceSpan}.
+ */
+ private bool $inSpan = false;
+
+ public function __construct()
+ {
+ $this->buffer = new SimpleStringBuffer();
+ }
+
+ public function getLength(): int
+ {
+ return $this->buffer->getLength();
+ }
+
+ /**
+ * The current location in {@see $buffer}.
+ */
+ private function getTargetLocation(): SourceLocation
+ {
+ return new SimpleSourceLocation($this->buffer->getLength(), line: $this->line, column: $this->column);
+ }
+
+ public function forSpan(FileSpan $span, callable $callback)
+ {
+ $wasInSpan = $this->inSpan;
+ $this->inSpan = true;
+ $this->addEntry($span->getStart(), $this->getTargetLocation());
+
+ try {
+ return $callback();
+ } finally {
+ // We could map $span->getEnd() to $this->getTargetLocation() here, but in practice
+ // browsers don't care about where a span ends as long as it covers at
+ // least the entity that they're looking up. Avoiding end mappings halves
+ // the size of the source maps we generate.
+
+ $this->inSpan = $wasInSpan;
+ }
+ }
+
+ /**
+ * Adds an entry to {@see $entries} unless it's redundant with the last entry.
+ */
+ private function addEntry(FileLocation $source, SourceLocation $target): void
+ {
+ if ($this->entries !== []) {
+ $entry = ListUtil::last($this->entries);
+
+ // Browsers don't care about the position of a value within a line, so
+ // it's redundant to have two entries on the same target line that both
+ // point to the same source line, even if they point to different
+ // columns in that line.
+ if ($entry->source->getLine() === $source->getLine() && $entry->target->getLine() === $target->getLine()) {
+ return;
+ }
+
+ // Since source maps are only used to look up the source from the target
+ // and not vice versa, we don't need multiple mappings to the same target.
+ if ($entry->target->getOffset() === $target->getOffset()) {
+ return;
+ }
+ }
+
+ $this->entries[] = new Entry($source, $target);
+ }
+
+ public function write(string $string): void
+ {
+ $this->buffer->write($string);
+
+ for ($i = 0; $i < \strlen($string); ++$i) {
+ if ($string[$i] === "\n") {
+ $this->writeLine();
+ } else {
+ $this->column++;
+ }
+ }
+ }
+
+ public function writeChar(string $char): void
+ {
+ $this->buffer->writeChar($char);
+
+ if ($char === "\n") {
+ $this->writeLine();
+ } else {
+ $this->column++;
+ }
+ }
+
+ /**
+ * Records that a line has been passed.
+ *
+ * If we're in the middle of a source span, indicate that at the beginning of
+ * the new line. This is necessary because source maps consider each line
+ * separately.
+ */
+ private function writeLine(): void
+ {
+ $lastEntry = ListUtil::last($this->entries);
+
+ // Trim useless entries.
+ if ($lastEntry->target->getLine() === $this->line && $lastEntry->target->getColumn() === $this->column) {
+ array_pop($this->entries);
+ }
+
+ $this->line++;
+ $this->column = 0;
+
+ if ($this->inSpan) {
+ $this->entries[] = new Entry($lastEntry->source, $this->getTargetLocation());
+ }
+ }
+
+ public function __toString(): string
+ {
+ return (string) $this->buffer;
+ }
+
+ public function buildSourceMap(?string $prefix): SingleMapping
+ {
+ if ($prefix === null || $prefix === '') {
+ return SingleMapping::fromEntries($this->entries);
+ }
+
+ $prefixLength = \strlen($prefix);
+ $prefixLines = 0;
+ $prefixColumn = 0;
+ for ($i = 0; $i < \strlen($prefix); ++$i) {
+ if ($prefix[$i] === "\n") {
+ $prefixLines++;
+ $prefixColumn = 0;
+ } else {
+ $prefixColumn++;
+ }
+ }
+
+ return SingleMapping::fromEntries(array_map(fn (Entry $entry) => new Entry(
+ $entry->source,
+ new SimpleSourceLocation(
+ $entry->target->getOffset() + $prefixLength,
+ line: $entry->target->getLine() + $prefixLines,
+ // Only adjust the column for entries that are on the same line as
+ // the last chunk of the prefix.
+ column: $entry->target->getColumn() + ($entry->target->getLine() === 0 ? $prefixColumn : 0)
+ )
+ ), $this->entries));
+ }
+}
diff --git a/vendor/scssphp/scssphp/src/SourceMap/Base64.php b/vendor/scssphp/scssphp/src/SourceMap/Base64.php
index 00b6b4545..3dd4663ab 100644
--- a/vendor/scssphp/scssphp/src/SourceMap/Base64.php
+++ b/vendor/scssphp/scssphp/src/SourceMap/Base64.php
@@ -19,12 +19,12 @@ namespace ScssPhp\ScssPhp\SourceMap;
*
* @internal
*/
-class Base64
+final class Base64
{
/**
* @var array<int, string>
*/
- private static $encodingMap = [
+ private const ENCODING_MAP = [
0 => 'A',
1 => 'B',
2 => 'C',
@@ -92,96 +92,10 @@ class Base64
];
/**
- * @var array<string|int, int>
- */
- private static $decodingMap = [
- 'A' => 0,
- 'B' => 1,
- 'C' => 2,
- 'D' => 3,
- 'E' => 4,
- 'F' => 5,
- 'G' => 6,
- 'H' => 7,
- 'I' => 8,
- 'J' => 9,
- 'K' => 10,
- 'L' => 11,
- 'M' => 12,
- 'N' => 13,
- 'O' => 14,
- 'P' => 15,
- 'Q' => 16,
- 'R' => 17,
- 'S' => 18,
- 'T' => 19,
- 'U' => 20,
- 'V' => 21,
- 'W' => 22,
- 'X' => 23,
- 'Y' => 24,
- 'Z' => 25,
- 'a' => 26,
- 'b' => 27,
- 'c' => 28,
- 'd' => 29,
- 'e' => 30,
- 'f' => 31,
- 'g' => 32,
- 'h' => 33,
- 'i' => 34,
- 'j' => 35,
- 'k' => 36,
- 'l' => 37,
- 'm' => 38,
- 'n' => 39,
- 'o' => 40,
- 'p' => 41,
- 'q' => 42,
- 'r' => 43,
- 's' => 44,
- 't' => 45,
- 'u' => 46,
- 'v' => 47,
- 'w' => 48,
- 'x' => 49,
- 'y' => 50,
- 'z' => 51,
- 0 => 52,
- 1 => 53,
- 2 => 54,
- 3 => 55,
- 4 => 56,
- 5 => 57,
- 6 => 58,
- 7 => 59,
- 8 => 60,
- 9 => 61,
- '+' => 62,
- '/' => 63,
- ];
-
- /**
* Convert to base64
- *
- * @param int $value
- *
- * @return string
- */
- public static function encode($value)
- {
- return self::$encodingMap[$value];
- }
-
- /**
- * Convert from base64
- *
- * @param string $value
- *
- * @return int
*/
- public static function decode($value)
+ public static function encode(int $value): string
{
- return self::$decodingMap[$value];
+ return self::ENCODING_MAP[$value];
}
}
diff --git a/vendor/scssphp/scssphp/src/SourceMap/Base64VLQ.php b/vendor/scssphp/scssphp/src/SourceMap/Base64VLQ.php
index 2a5210c68..166466777 100644
--- a/vendor/scssphp/scssphp/src/SourceMap/Base64VLQ.php
+++ b/vendor/scssphp/scssphp/src/SourceMap/Base64VLQ.php
@@ -37,7 +37,7 @@ namespace ScssPhp\ScssPhp\SourceMap;
*
* @internal
*/
-class Base64VLQ
+final class Base64VLQ
{
// A Base64 VLQ digit can represent 5 bits, so it is base-32.
const VLQ_BASE_SHIFT = 5;
@@ -50,12 +50,8 @@ class Base64VLQ
/**
* Returns the VLQ encoded value.
- *
- * @param int $value
- *
- * @return string
*/
- public static function encode($value)
+ public static function encode(int $value): string
{
$encoded = '';
$vlq = self::toVLQSigned($value);
@@ -77,75 +73,17 @@ class Base64VLQ
}
/**
- * Decodes VLQValue.
- *
- * @param string $str
- * @param int $index
- *
- * @return int
- */
- public static function decode($str, &$index)
- {
- $result = 0;
- $shift = 0;
-
- do {
- $c = $str[$index++];
- $digit = Base64::decode($c);
- $continuation = ($digit & self::VLQ_CONTINUATION_BIT) != 0;
- $digit &= self::VLQ_BASE_MASK;
- $result = $result + ($digit << $shift);
- $shift = $shift + self::VLQ_BASE_SHIFT;
- } while ($continuation);
-
- return self::fromVLQSigned($result);
- }
-
- /**
* Converts from a two-complement value to a value where the sign bit is
* is placed in the least significant bit. For example, as decimals:
* 1 becomes 2 (10 binary), -1 becomes 3 (11 binary)
* 2 becomes 4 (100 binary), -2 becomes 5 (101 binary)
- *
- * @param int $value
- *
- * @return int
*/
- private static function toVLQSigned($value)
+ private static function toVLQSigned(int $value): int
{
if ($value < 0) {
return ((-$value) << 1) + 1;
}
- return ($value << 1) + 0;
- }
-
- /**
- * Converts to a two-complement value from a value where the sign bit is
- * is placed in the least significant bit. For example, as decimals:
- * 2 (10 binary) becomes 1, 3 (11 binary) becomes -1
- * 4 (100 binary) becomes 2, 5 (101 binary) becomes -2
- *
- * @param int $value
- *
- * @return int
- */
- private static function fromVLQSigned($value)
- {
- $negate = ($value & 1) === 1;
-
- //$value >>>= 1; // unsigned right shift
- $value = ($value >> 1) & PHP_INT_MAX;
-
- if (! $negate) {
- return $value;
- }
-
- // We need to OR 0x80000000 here to ensure the 32nd bit (the sign bit) is
- // always set for negative numbers. If `value` were 1, (meaning `negate` is
- // true and all other bits were zeros), `value` would now be 0. -0 is just
- // 0, and doesn't flip the 32nd bit as intended. All positive numbers will
- // successfully flip the 32nd bit without issue, so it's a noop for them.
- return -$value | 0x80000000;
+ return $value << 1;
}
}
diff --git a/vendor/scssphp/scssphp/src/SourceMap/Builder/Entry.php b/vendor/scssphp/scssphp/src/SourceMap/Builder/Entry.php
new file mode 100644
index 000000000..ad8c9d91d
--- /dev/null
+++ b/vendor/scssphp/scssphp/src/SourceMap/Builder/Entry.php
@@ -0,0 +1,62 @@
+<?php
+
+/**
+ * SCSSPHP
+ *
+ * @copyright 2018-2020 Anthon Pang
+ *
+ * @license http://opensource.org/licenses/MIT MIT
+ *
+ * @link http://scssphp.github.io/scssphp
+ */
+
+namespace ScssPhp\ScssPhp\SourceMap\Builder;
+
+use SourceSpan\SourceLocation;
+
+/**
+ * An entry in the source map builder.
+ *
+ * @internal
+ */
+final class Entry
+{
+ /**
+ * Span denoting the original location in the input source file
+ */
+ public readonly SourceLocation $source;
+
+ /**
+ * Span indicating the corresponding location in the target file.
+ */
+ public readonly SourceLocation $target;
+
+ public function __construct(SourceLocation $source, SourceLocation $target)
+ {
+ $this->source = $source;
+ $this->target = $target;
+ }
+
+ /**
+ * Implements comparison to ensure that entries are ordered by their
+ * location in the target file. We sort primarily by the target offset
+ * because source map files are encoded by printing each mapping in order as
+ * they appear in the target file.
+ */
+ public function compareTo(Entry $other): int
+ {
+ $res = $this->target->compareTo($other->target);
+
+ if ($res !== 0) {
+ return $res;
+ }
+
+ $res = (string) $this->source->getSourceUrl() <=> (string) $other->source->getSourceUrl();
+
+ if ($res !== 0) {
+ return $res;
+ }
+
+ return $this->source->compareTo($other->source);
+ }
+}
diff --git a/vendor/scssphp/scssphp/src/SourceMap/SingleMapping.php b/vendor/scssphp/scssphp/src/SourceMap/SingleMapping.php
new file mode 100644
index 000000000..cee9e5fb5
--- /dev/null
+++ b/vendor/scssphp/scssphp/src/SourceMap/SingleMapping.php
@@ -0,0 +1,197 @@
+<?php
+
+/**
+ * SCSSPHP
+ *
+ * @copyright 2018-2020 Anthon Pang
+ *
+ * @license http://opensource.org/licenses/MIT MIT
+ *
+ * @link http://scssphp.github.io/scssphp
+ */
+
+namespace ScssPhp\ScssPhp\SourceMap;
+
+use ScssPhp\ScssPhp\SourceMap\Builder\Entry;
+use SourceSpan\FileLocation;
+use SourceSpan\SourceFile;
+
+/**
+ * @internal
+ */
+final class SingleMapping
+{
+ /**
+ * @var list<string>
+ */
+ public readonly array $urls;
+
+ /**
+ * The {@see SourceFile}s to which the entries in {@see $lines} refer.
+ *
+ * This is in the same order as {@see $urls}. If this was constructed using
+ * {@see SingleMapping::fromEntries()}, this contains files from any {@see FileLocation}s
+ * used to build the mapping.
+ *
+ * Files whose contents aren't available are `null`.
+ *
+ * @var list<SourceFile|null>
+ */
+ public readonly array $files;
+
+ /**
+ * Entries indicating the beginning of each span.
+ *
+ * @var list<TargetLineEntry>
+ */
+ public readonly array $lines;
+
+ /**
+ * Url of the target file.
+ */
+ public ?string $targetUrl = null;
+
+ /**
+ * Source root prepended to all entries in {@see $urls}.
+ */
+ public ?string $sourceRoot = null;
+
+ /**
+ * @param list<SourceFile|null> $files
+ * @param list<string> $urls
+ * @param list<TargetLineEntry> $lines
+ */
+ private function __construct(array $files, array $urls, array $lines)
+ {
+ $this->urls = $urls;
+ $this->files = $files;
+ $this->lines = $lines;
+ }
+
+ /**
+ * @param Entry[] $sourceEntries
+ */
+ public static function fromEntries(array $sourceEntries): self
+ {
+ usort($sourceEntries, fn (Entry $a, Entry $b) => $a->compareTo($b));
+
+ $lines = [];
+ // Indices associated with file urls that will be part of the source map. We
+ // rely on map order so that `array_keys($url)[$urls[$u]] === $u`
+ $urls = [];
+ // The file for each URL, indexed by $urls' values.
+ $files = [];
+ $lineNum = null;
+ $targetEntries = null;
+
+ foreach ($sourceEntries as $sourceEntry) {
+ if ($lineNum === null || $sourceEntry->target->getLine() > $lineNum) {
+ $lineNum = $sourceEntry->target->getLine();
+ $targetEntries = new \ArrayObject();
+ $lines[] = new TargetLineEntry($lineNum, $targetEntries);
+ }
+
+ $sourceUrl = $sourceEntry->source->getSourceUrl();
+ $urlId = $urls[$sourceUrl?->toString() ?? ''] ??= \count($urls);
+
+ if ($sourceEntry->source instanceof FileLocation) {
+ $files[$urlId] ??= $sourceEntry->source->getFile();
+ }
+
+ $targetEntries[] = new TargetEntry($sourceEntry->target->getColumn(), $urlId, $sourceEntry->source->getLine(), $sourceEntry->source->getColumn());
+ }
+
+ return new self(array_values(array_map(fn (int $i) => $files[$i] ?? null, $urls)), array_keys($urls), $lines);
+ }
+
+ /**
+ * Encodes the Mapping mappings as a json map.
+ *
+ * If $includeSourceContents is `true`, this includes the source file
+ * contents from {@see $files} in the map if possible.
+ *
+ * @return array<string, mixed>
+ */
+ public function toJson(bool $includeSourceContents = false): array
+ {
+ $buff = '';
+ $line = 0;
+ $column = 0;
+ $srcLine = 0;
+ $srcColumn = 0;
+ $srcUrlId = 0;
+ $first = true;
+
+ foreach ($this->lines as $entry) {
+ $nextLine = $entry->line;
+
+ if ($nextLine > $line) {
+ for ($i = $line; $i < $nextLine; $i++) {
+ $buff .= ';';
+ }
+ $line = $nextLine;
+ $column = 0;
+ $first = true;
+ }
+
+ foreach ($entry->entries as $segment) {
+ if (!$first) {
+ $buff .= ',';
+ }
+ $first = false;
+ $buff .= Base64VLQ::encode($segment->column - $column);
+ $column = $segment->column;
+
+ // Encoding can be just the column offset if there is no source
+ // information.
+ $newUrlId = $segment->sourceUrlId;
+ if ($newUrlId === null) {
+ continue;
+ }
+ \assert($segment->sourceLine !== null);
+ \assert($segment->sourceColumn !== null);
+
+ $buff .= Base64VLQ::encode($newUrlId - $srcUrlId);
+ $srcUrlId = $newUrlId;
+ $buff .= Base64VLQ::encode($segment->sourceLine - $srcLine);
+ $srcLine = $segment->sourceLine;
+ $buff .= Base64VLQ::encode($segment->sourceColumn - $srcColumn);
+ $srcColumn = $segment->sourceColumn;
+ }
+ }
+
+ $result = [
+ 'version' => 3,
+ 'sourceRoot' => $this->sourceRoot ?? '',
+ 'sources' => $this->urls,
+ 'names' => [],
+ 'mappings' => $buff,
+ ];
+
+ if ($this->targetUrl !== null) {
+ $result['file'] = $this->targetUrl;
+ }
+
+ if ($includeSourceContents) {
+ $result['sourcesContent'] = array_map(fn (?SourceFile $file) => $file?->getText(0), $this->files);
+ }
+
+ return $result;
+ }
+
+ /**
+ * Returns a new mapping with {@see $urls} transformed by $callback.
+ *
+ * @param callable(string): string $callback
+ */
+ public function mapUrls(callable $callback): self
+ {
+ $newUrls = array_map($callback, $this->urls);
+
+ $new = new self($this->files, $newUrls, $this->lines);
+ $new->targetUrl = $this->targetUrl;
+ $new->sourceRoot = $this->sourceRoot;
+
+ return $new;
+ }
+}
diff --git a/vendor/scssphp/scssphp/src/SourceMap/SourceMapGenerator.php b/vendor/scssphp/scssphp/src/SourceMap/SourceMapGenerator.php
deleted file mode 100644
index ccd4f0261..000000000
--- a/vendor/scssphp/scssphp/src/SourceMap/SourceMapGenerator.php
+++ /dev/null
@@ -1,390 +0,0 @@
-<?php
-
-/**
- * SCSSPHP
- *
- * @copyright 2012-2020 Leaf Corcoran
- *
- * @license http://opensource.org/licenses/MIT MIT
- *
- * @link http://scssphp.github.io/scssphp
- */
-
-namespace ScssPhp\ScssPhp\SourceMap;
-
-use ScssPhp\ScssPhp\Exception\CompilerException;
-
-/**
- * Source Map Generator
- *
- * {@internal Derivative of oyejorge/less.php's lib/SourceMap/Generator.php, relicensed with permission. }}
- *
- * @author Josh Schmidt <oyejorge@gmail.com>
- * @author Nicolas FRANÇOIS <nicolas.francois@frog-labs.com>
- *
- * @internal
- */
-class SourceMapGenerator
-{
- /**
- * What version of source map does the generator generate?
- */
- const VERSION = 3;
-
- /**
- * Array of default options
- *
- * @var array
- * @phpstan-var array{sourceRoot: string, sourceMapFilename: string|null, sourceMapURL: string|null, sourceMapWriteTo: string|null, outputSourceFiles: bool, sourceMapRootpath: string, sourceMapBasepath: string}
- */
- protected $defaultOptions = [
- // an optional source root, useful for relocating source files
- // on a server or removing repeated values in the 'sources' entry.
- // This value is prepended to the individual entries in the 'source' field.
- 'sourceRoot' => '',
-
- // an optional name of the generated code that this source map is associated with.
- 'sourceMapFilename' => null,
-
- // url of the map
- 'sourceMapURL' => null,
-
- // absolute path to a file to write the map to
- 'sourceMapWriteTo' => null,
-
- // output source contents?
- 'outputSourceFiles' => false,
-
- // base path for filename normalization
- 'sourceMapRootpath' => '',
-
- // base path for filename normalization
- 'sourceMapBasepath' => ''
- ];
-
- /**
- * The base64 VLQ encoder
- *
- * @var \ScssPhp\ScssPhp\SourceMap\Base64VLQ
- */
- protected $encoder;
-
- /**
- * Array of mappings
- *
- * @var array
- * @phpstan-var list<array{generated_line: int, generated_column: int, original_line: int, original_column: int, source_file: string}>
- */
- protected $mappings = [];
-
- /**
- * Array of contents map
- *
- * @var array
- */
- protected $contentsMap = [];
-
- /**
- * File to content map
- *
- * @var array<string, string>
- */
- protected $sources = [];
-
- /**
- * @var array<string, int>
- */
- protected $sourceKeys = [];
-
- /**
- * @var array
- * @phpstan-var array{sourceRoot: string, sourceMapFilename: string|null, sourceMapURL: string|null, sourceMapWriteTo: string|null, outputSourceFiles: bool, sourceMapRootpath: string, sourceMapBasepath: string}
- */
- private $options;
-
- /**
- * @phpstan-param array{sourceRoot?: string, sourceMapFilename?: string|null, sourceMapURL?: string|null, sourceMapWriteTo?: string|null, outputSourceFiles?: bool, sourceMapRootpath?: string, sourceMapBasepath?: string} $options
- */
- public function __construct(array $options = [])
- {
- $this->options = array_replace($this->defaultOptions, $options);
- $this->encoder = new Base64VLQ();
- }
-
- /**
- * Adds a mapping
- *
- * @param int $generatedLine The line number in generated file
- * @param int $generatedColumn The column number in generated file
- * @param int $originalLine The line number in original file
- * @param int $originalColumn The column number in original file
- * @param string $sourceFile The original source file
- *
- * @return void
- */
- public function addMapping($generatedLine, $generatedColumn, $originalLine, $originalColumn, $sourceFile)
- {
- $this->mappings[] = [
- 'generated_line' => $generatedLine,
- 'generated_column' => $generatedColumn,
- 'original_line' => $originalLine,
- 'original_column' => $originalColumn,
- 'source_file' => $sourceFile
- ];
-
- $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
- *
- * @param string $prefix A prefix added in the output file, which needs to shift mappings
- *
- * @return string
- *
- * @see https://docs.google.com/document/d/1U1RGAehQwRypUTovF1KRlpiOFze0b-_2gc6fAH0KY0k/edit#
- */
- public function generateJson($prefix = '')
- {
- $sourceMap = [];
- $mappings = $this->generateMappings($prefix);
-
- // File version (always the first entry in the object) and must be a positive integer.
- $sourceMap['version'] = self::VERSION;
-
- // An optional name of the generated code that this source map is associated with.
- $file = $this->options['sourceMapFilename'];
-
- if ($file) {
- $sourceMap['file'] = $file;
- }
-
- // 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.
- $root = $this->options['sourceRoot'];
-
- if ($root) {
- $sourceMap['sourceRoot'] = $root;
- }
-
- // A list of original sources used by the 'mappings' entry.
- $sourceMap['sources'] = [];
-
- foreach ($this->sources as $sourceFilename) {
- $sourceMap['sources'][] = $this->normalizeFilename($sourceFilename);
- }
-
- // A list of symbol names used by the 'mappings' entry.
- $sourceMap['names'] = [];
-
- // A string with the encoded mapping data.
- $sourceMap['mappings'] = $mappings;
-
- if ($this->options['outputSourceFiles']) {
- // An optional list of source content, useful when the 'source' can't be hosted.
- // The contents are listed in the same order as the sources above.
- // 'null' may be used if some original sources should be retrieved by name.
- $sourceMap['sourcesContent'] = $this->getSourcesContent();
- }
-
- // less.js compat fixes
- if (\count($sourceMap['sources']) && empty($sourceMap['sourceRoot'])) {
- unset($sourceMap['sourceRoot']);
- }
-
- $jsonSourceMap = json_encode($sourceMap, JSON_UNESCAPED_SLASHES);
-
- if (json_last_error() !== JSON_ERROR_NONE) {
- throw new \RuntimeException(json_last_error_msg());
- }
-
- assert($jsonSourceMap !== false);
-
- return $jsonSourceMap;
- }
-
- /**
- * Returns the sources contents
- *
- * @return string[]|null
- */
- protected function getSourcesContent()
- {
- if (empty($this->sources)) {
- return null;
- }
-
- $content = [];
-
- foreach ($this->sources as $sourceFile) {
- $content[] = file_get_contents($sourceFile);
- }
-
- return $content;
- }
-
- /**
- * Generates the mappings string
- *
- * @param string $prefix A prefix added in the output file, which needs to shift mappings
- *
- * @return string
- */
- public function generateMappings($prefix = '')
- {
- if (! \count($this->mappings)) {
- return '';
- }
-
- $prefixLines = substr_count($prefix, "\n");
- $lastPrefixNewLine = strrpos($prefix, "\n");
- $lastPrefixLineStart = false === $lastPrefixNewLine ? 0 : $lastPrefixNewLine + 1;
- $prefixColumn = strlen($prefix) - $lastPrefixLineStart;
-
- $this->sourceKeys = array_flip(array_keys($this->sources));
-
- // group mappings by generated line number.
- $groupedMap = $groupedMapEncoded = [];
-
- foreach ($this->mappings as $m) {
- $groupedMap[$m['generated_line']][] = $m;
- }
-
- ksort($groupedMap);
-
- $lastGeneratedLine = $lastOriginalIndex = $lastOriginalLine = $lastOriginalColumn = 0;
-
- foreach ($groupedMap as $lineNumber => $lineMap) {
- if ($lineNumber > 1) {
- // The prefix only impacts the column for the first line of the original output
- $prefixColumn = 0;
- }
- $lineNumber += $prefixLines;
-
- while (++$lastGeneratedLine < $lineNumber) {
- $groupedMapEncoded[] = ';';
- }
-
- $lineMapEncoded = [];
- $lastGeneratedColumn = 0;
-
- foreach ($lineMap as $m) {
- $generatedColumn = $m['generated_column'] + $prefixColumn;
-
- $mapEncoded = $this->encoder->encode($generatedColumn - $lastGeneratedColumn);
- $lastGeneratedColumn = $generatedColumn;
-
- // find the index
- if ($m['source_file']) {
- $index = $this->findFileIndex($m['source_file']);
-
- if ($index !== false) {
- $mapEncoded .= $this->encoder->encode($index - $lastOriginalIndex);
- $lastOriginalIndex = $index;
- // lines are stored 0-based in SourceMap spec version 3
- $mapEncoded .= $this->encoder->encode($m['original_line'] - 1 - $lastOriginalLine);
- $lastOriginalLine = $m['original_line'] - 1;
- $mapEncoded .= $this->encoder->encode($m['original_column'] - $lastOriginalColumn);
- $lastOriginalColumn = $m['original_column'];
- }
- }
-
- $lineMapEncoded[] = $mapEncoded;
- }
-
- $groupedMapEncoded[] = implode(',', $lineMapEncoded) . ';';
- }
-
- return rtrim(implode($groupedMapEncoded), ';');
- }
-
- /**
- * Finds the index for the filename
- *
- * @param string $filename
- *
- * @return int|false
- */
- protected function findFileIndex($filename)
- {
- return $this->sourceKeys[$filename];
- }
-
- /**
- * Normalize filename
- *
- * @param string $filename
- *
- * @return string
- */
- protected function normalizeFilename($filename)
- {
- $filename = $this->fixWindowsPath($filename);
- $rootpath = $this->options['sourceMapRootpath'];
- $basePath = $this->options['sourceMapBasepath'];
-
- // "Trim" the 'sourceMapBasepath' from the output filename.
- if (\strlen($basePath) && strpos($filename, $basePath) === 0) {
- $filename = substr($filename, \strlen($basePath));
- }
-
- // Remove extra leading path separators.
- if (strpos($filename, '\\') === 0 || strpos($filename, '/') === 0) {
- $filename = substr($filename, 1);
- }
-
- return $rootpath . $filename;
- }
-
- /**
- * Fix windows paths
- *
- * @param string $path
- * @param bool $addEndSlash
- *
- * @return string
- */
- public function fixWindowsPath($path, $addEndSlash = false)
- {
- $slash = ($addEndSlash) ? '/' : '';
-
- if (! empty($path)) {
- $path = str_replace('\\', '/', $path);
- $path = rtrim($path, '/') . $slash;
- }
-
- return $path;
- }
-}
diff --git a/vendor/scssphp/scssphp/src/SourceMap/TargetEntry.php b/vendor/scssphp/scssphp/src/SourceMap/TargetEntry.php
new file mode 100644
index 000000000..e2c253614
--- /dev/null
+++ b/vendor/scssphp/scssphp/src/SourceMap/TargetEntry.php
@@ -0,0 +1,29 @@
+<?php
+
+/**
+ * SCSSPHP
+ *
+ * @copyright 2018-2020 Anthon Pang
+ *
+ * @license http://opensource.org/licenses/MIT MIT
+ *
+ * @link http://scssphp.github.io/scssphp
+ */
+
+namespace ScssPhp\ScssPhp\SourceMap;
+
+/**
+ * A target segment entry read from a source map
+ *
+ * @internal
+ */
+final class TargetEntry
+{
+ public function __construct(
+ public readonly int $column,
+ public readonly ?int $sourceUrlId = null,
+ public readonly ?int $sourceLine = null,
+ public readonly ?int $sourceColumn = null,
+ ) {
+ }
+}
diff --git a/vendor/scssphp/scssphp/src/SourceMap/TargetLineEntry.php b/vendor/scssphp/scssphp/src/SourceMap/TargetLineEntry.php
new file mode 100644
index 000000000..92612840b
--- /dev/null
+++ b/vendor/scssphp/scssphp/src/SourceMap/TargetLineEntry.php
@@ -0,0 +1,28 @@
+<?php
+
+/**
+ * SCSSPHP
+ *
+ * @copyright 2018-2020 Anthon Pang
+ *
+ * @license http://opensource.org/licenses/MIT MIT
+ *
+ * @link http://scssphp.github.io/scssphp
+ */
+
+namespace ScssPhp\ScssPhp\SourceMap;
+
+/**
+ * @internal
+ */
+final class TargetLineEntry
+{
+ /**
+ * @param \ArrayObject<int, TargetEntry> $entries
+ */
+ public function __construct(
+ public readonly int $line,
+ public readonly \ArrayObject $entries,
+ ) {
+ }
+}
diff --git a/vendor/scssphp/scssphp/src/SourceSpan/LazyFileSpan.php b/vendor/scssphp/scssphp/src/SourceSpan/LazyFileSpan.php
new file mode 100644
index 000000000..54aa22fbc
--- /dev/null
+++ b/vendor/scssphp/scssphp/src/SourceSpan/LazyFileSpan.php
@@ -0,0 +1,131 @@
+<?php
+
+/**
+ * SCSSPHP
+ *
+ * @copyright 2012-2020 Leaf Corcoran
+ *
+ * @license http://opensource.org/licenses/MIT MIT
+ *
+ * @link http://scssphp.github.io/scssphp
+ */
+
+namespace ScssPhp\ScssPhp\SourceSpan;
+
+use League\Uri\Contracts\UriInterface;
+use SourceSpan\FileLocation;
+use SourceSpan\FileSpan;
+use SourceSpan\SourceFile;
+use SourceSpan\SourceSpan;
+
+/**
+ * A wrapper for {@see FileSpan} that allows an expensive creation process to be
+ * deferred until the span is actually needed.
+ *
+ * @internal
+ */
+class LazyFileSpan implements FileSpan
+{
+ /**
+ * @var \Closure(): FileSpan
+ * @readonly
+ */
+ private readonly \Closure $builder;
+
+ /**
+ * @var FileSpan|null
+ */
+ private ?FileSpan $span = null;
+
+ /**
+ * @param \Closure(): FileSpan $builder
+ */
+ public function __construct(\Closure $builder)
+ {
+ $this->builder = $builder;
+ }
+
+ public function getSpan(): FileSpan
+ {
+ if ($this->span === null) {
+ $this->span = ($this->builder)();
+ }
+
+ return $this->span;
+ }
+
+ public function getFile(): SourceFile
+ {
+ return $this->getSpan()->getFile();
+ }
+
+ public function getSourceUrl(): ?UriInterface
+ {
+ return $this->getSpan()->getSourceUrl();
+ }
+
+ public function getLength(): int
+ {
+ return $this->getSpan()->getLength();
+ }
+
+ public function getStart(): FileLocation
+ {
+ return $this->getSpan()->getStart();
+ }
+
+ public function getEnd(): FileLocation
+ {
+ return $this->getSpan()->getEnd();
+ }
+
+ public function getText(): string
+ {
+ return $this->getSpan()->getText();
+ }
+
+ public function union(SourceSpan $other): SourceSpan
+ {
+ return $this->getSpan()->union($other);
+ }
+
+ public function compareTo(SourceSpan $other): int
+ {
+ return $this->getSpan()->compareTo($other);
+ }
+
+ public function expand(FileSpan $other): FileSpan
+ {
+ return $this->getSpan()->expand($other);
+ }
+
+ public function message(string $message): string
+ {
+ return $this->getSpan()->message($message);
+ }
+
+ public function messageMultiple(string $message, string $label, array $secondarySpans): string
+ {
+ return $this->getSpan()->messageMultiple($message, $label, $secondarySpans);
+ }
+
+ public function highlight(): string
+ {
+ return $this->getSpan()->highlight();
+ }
+
+ public function highlightMultiple(string $label, array $secondarySpans): string
+ {
+ return $this->getSpan()->highlightMultiple($label, $secondarySpans);
+ }
+
+ public function subspan(int $start, ?int $end = null): FileSpan
+ {
+ return $this->getSpan()->subspan($start, $end);
+ }
+
+ public function getContext(): string
+ {
+ return $this->getSpan()->getContext();
+ }
+}
diff --git a/vendor/scssphp/scssphp/src/SourceSpan/MultiSpan.php b/vendor/scssphp/scssphp/src/SourceSpan/MultiSpan.php
new file mode 100644
index 000000000..23fa8b05d
--- /dev/null
+++ b/vendor/scssphp/scssphp/src/SourceSpan/MultiSpan.php
@@ -0,0 +1,127 @@
+<?php
+
+/**
+ * SCSSPHP
+ *
+ * @copyright 2012-2020 Leaf Corcoran
+ *
+ * @license http://opensource.org/licenses/MIT MIT
+ *
+ * @link http://scssphp.github.io/scssphp
+ */
+
+namespace ScssPhp\ScssPhp\SourceSpan;
+
+use League\Uri\Contracts\UriInterface;
+use SourceSpan\FileLocation;
+use SourceSpan\FileSpan;
+use SourceSpan\SourceFile;
+use SourceSpan\SourceSpan;
+
+/**
+ * A FileSpan wrapper that with secondary spans attached, so that
+ * {@see MultiSpan::message} can forward to {@see SourceSpan::messageMultiple}.
+ *
+ * This is used to transparently support multi-span messages in situations that
+ * need to be backwards-compatible with single spans, such as logger
+ * invocations. To match the `source_span` package, separate APIs should
+ * generally be preferred over this class wherever backwards compatibility
+ * isn't a concern.
+ *
+ * @internal
+ */
+final class MultiSpan implements FileSpan
+{
+ /**
+ * @param array<string, SourceSpan> $secondarySpans
+ */
+ public function __construct(
+ private readonly FileSpan $primary,
+ private readonly string $primaryLabel,
+ private readonly array $secondarySpans,
+ ) {
+ }
+
+ public function getStart(): FileLocation
+ {
+ return $this->primary->getStart();
+ }
+
+ public function getEnd(): FileLocation
+ {
+ return $this->primary->getEnd();
+ }
+
+ public function getText(): string
+ {
+ return $this->primary->getText();
+ }
+
+ public function getContext(): string
+ {
+ return $this->primary->getContext();
+ }
+
+ public function getFile(): SourceFile
+ {
+ return $this->primary->getFile();
+ }
+
+ public function getLength(): int
+ {
+ return $this->primary->getLength();
+ }
+
+ public function getSourceUrl(): ?UriInterface
+ {
+ return $this->primary->getSourceUrl();
+ }
+
+ public function compareTo(SourceSpan $other): int
+ {
+ return $this->primary->compareTo($other);
+ }
+
+ public function expand(FileSpan $other): FileSpan
+ {
+ return $this->withPrimary($this->primary->expand($other));
+ }
+
+ public function union(SourceSpan $other): SourceSpan
+ {
+ return $this->primary->union($other);
+ }
+
+ public function subspan(int $start, ?int $end = null): FileSpan
+ {
+ return $this->withPrimary($this->primary->subspan($start, $end));
+ }
+
+ public function highlight(): string
+ {
+ return $this->primary->highlightMultiple($this->primaryLabel, $this->secondarySpans);
+ }
+
+ public function message(string $message): string
+ {
+ return $this->primary->messageMultiple($message, $this->primaryLabel, $this->secondarySpans);
+ }
+
+ public function highlightMultiple(string $label, array $secondarySpans): string
+ {
+ return $this->primary->highlightMultiple($label, array_merge($this->secondarySpans, $secondarySpans));
+ }
+
+ public function messageMultiple(string $message, string $label, array $secondarySpans): string
+ {
+ return $this->primary->messageMultiple($message, $label, array_merge($this->secondarySpans, $secondarySpans));
+ }
+
+ /**
+ * Returns a copy of $this with $newPrimary as its primary span.
+ */
+ private function withPrimary(FileSpan $newPrimary): MultiSpan
+ {
+ return new MultiSpan($newPrimary, $this->primaryLabel, $this->secondarySpans);
+ }
+}
diff --git a/vendor/scssphp/scssphp/src/StackTrace/Frame.php b/vendor/scssphp/scssphp/src/StackTrace/Frame.php
new file mode 100644
index 000000000..0d83fdd7a
--- /dev/null
+++ b/vendor/scssphp/scssphp/src/StackTrace/Frame.php
@@ -0,0 +1,112 @@
+<?php
+
+/**
+ * SCSSPHP
+ *
+ * @copyright 2012-2020 Leaf Corcoran
+ *
+ * @license http://opensource.org/licenses/MIT MIT
+ *
+ * @link http://scssphp.github.io/scssphp
+ */
+
+namespace ScssPhp\ScssPhp\StackTrace;
+
+use League\Uri\Contracts\UriInterface;
+use ScssPhp\ScssPhp\Util\Path;
+
+/**
+ * A single stack frame. Each frame points to a precise location in Sass code.
+ */
+final class Frame
+{
+ /**
+ * The URI of the file in which the code is located.
+ */
+ private readonly UriInterface $url;
+
+ /**
+ * The line number on which the code location is located.
+ *
+ * This can be null, indicating that the line number is unknown or
+ * unimportant.
+ */
+ private readonly ?int $line;
+
+ /**
+ * The column number of the code location.
+ *
+ * This can be null, indicating that the column number is unknown or
+ * unimportant.
+ */
+ private readonly ?int $column;
+
+ /**
+ * The name of the member in which the code location occurs.
+ */
+ private readonly ?string $member;
+
+ public function __construct(UriInterface $url, ?int $line, ?int $column, ?string $member)
+ {
+ $this->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(): UriInterface
+ {
+ 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/vendor/scssphp/scssphp/src/StackTrace/Trace.php b/vendor/scssphp/scssphp/src/StackTrace/Trace.php
new file mode 100644
index 000000000..ebd148353
--- /dev/null
+++ b/vendor/scssphp/scssphp/src/StackTrace/Trace.php
@@ -0,0 +1,53 @@
+<?php
+
+/**
+ * SCSSPHP
+ *
+ * @copyright 2012-2020 Leaf Corcoran
+ *
+ * @license http://opensource.org/licenses/MIT MIT
+ *
+ * @link http://scssphp.github.io/scssphp
+ */
+
+namespace ScssPhp\ScssPhp\StackTrace;
+
+/**
+ * A stack trace, comprised of a list of stack frames.
+ */
+final class Trace
+{
+ /**
+ * @var list<Frame>
+ * @readonly
+ */
+ private readonly array $frames;
+
+ /**
+ * @param list<Frame> $frames
+ */
+ public function __construct(array $frames)
+ {
+ $this->frames = $frames;
+ }
+
+ /**
+ * @return list<Frame>
+ */
+ 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(fn(Frame $frame) => str_pad($frame->getLocation(), $longest) . ' ' . $frame->getMember() . "\n", $this->frames));
+ }
+}
diff --git a/vendor/scssphp/scssphp/src/Syntax.php b/vendor/scssphp/scssphp/src/Syntax.php
new file mode 100644
index 000000000..f8409694e
--- /dev/null
+++ b/vendor/scssphp/scssphp/src/Syntax.php
@@ -0,0 +1,44 @@
+<?php
+
+/**
+ * SCSSPHP
+ *
+ * @copyright 2012-2020 Leaf Corcoran
+ *
+ * @license http://opensource.org/licenses/MIT MIT
+ *
+ * @link http://scssphp.github.io/scssphp
+ */
+
+namespace ScssPhp\ScssPhp;
+
+enum Syntax
+{
+ /**
+ * The CSS-superset SCSS syntax.
+ */
+ case SCSS;
+
+ /**
+ * The whitespace-sensitive indented syntax.
+ */
+ case SASS;
+
+ /**
+ * The plain CSS syntax, which disallows special Sass features.
+ */
+ case CSS;
+
+ public static function forPath(string $path): self
+ {
+ if (str_ends_with($path, '.sass')) {
+ return self::SASS;
+ }
+
+ if (str_ends_with($path, '.css')) {
+ return self::CSS;
+ }
+
+ return self::SCSS;
+ }
+}
diff --git a/vendor/scssphp/scssphp/src/Type.php b/vendor/scssphp/scssphp/src/Type.php
index d43088785..4163c9c5c 100644
--- a/vendor/scssphp/scssphp/src/Type.php
+++ b/vendor/scssphp/scssphp/src/Type.php
@@ -17,192 +17,16 @@ namespace ScssPhp\ScssPhp;
*
* @author Anthon Pang <anthon.pang@gmail.com>
*/
-class Type
+final class Type
{
- /**
- * @internal
- */
- const T_ASSIGN = 'assign';
- /**
- * @internal
- */
- const T_AT_ROOT = 'at-root';
- /**
- * @internal
- */
- const T_BLOCK = 'block';
- /**
- * @deprecated
- * @internal
- */
- const T_BREAK = 'break';
- /**
- * @internal
- */
- const T_CHARSET = 'charset';
const T_COLOR = 'color';
/**
* @internal
*/
- const T_COMMENT = 'comment';
- /**
- * @deprecated
- * @internal
- */
- const T_CONTINUE = 'continue';
- /**
- * @deprecated
- * @internal
- */
- const T_CONTROL = 'control';
- /**
- * @internal
- */
- const T_CUSTOM_PROPERTY = 'custom';
- /**
- * @internal
- */
- const T_DEBUG = 'debug';
- /**
- * @internal
- */
- const T_DIRECTIVE = 'directive';
- /**
- * @internal
- */
- const T_EACH = 'each';
- /**
- * @internal
- */
- const T_ELSE = 'else';
- /**
- * @internal
- */
- const T_ELSEIF = 'elseif';
- /**
- * @internal
- */
- const T_ERROR = 'error';
- /**
- * @internal
- */
- const T_EXPRESSION = 'exp';
- /**
- * @internal
- */
- const T_EXTEND = 'extend';
- /**
- * @internal
- */
- const T_FOR = 'for';
- const T_FUNCTION = 'function';
- /**
- * @internal
- */
- const T_FUNCTION_REFERENCE = 'function-reference';
- /**
- * @internal
- */
- const T_FUNCTION_CALL = 'fncall';
- /**
- * @internal
- */
- const T_HSL = 'hsl';
- /**
- * @internal
- */
- const T_HWB = 'hwb';
- /**
- * @internal
- */
- const T_IF = 'if';
- /**
- * @internal
- */
- const T_IMPORT = 'import';
- /**
- * @internal
- */
- const T_INCLUDE = 'include';
- /**
- * @internal
- */
- const T_INTERPOLATE = 'interpolate';
- /**
- * @internal
- */
- const T_INTERPOLATED = 'interpolated';
- /**
- * @internal
- */
const T_KEYWORD = 'keyword';
const T_LIST = 'list';
const T_MAP = 'map';
- /**
- * @internal
- */
- const T_MEDIA = 'media';
- /**
- * @internal
- */
- const T_MEDIA_EXPRESSION = 'mediaExp';
- /**
- * @internal
- */
- const T_MEDIA_TYPE = 'mediaType';
- /**
- * @internal
- */
- const T_MEDIA_VALUE = 'mediaValue';
- /**
- * @internal
- */
- const T_MIXIN = 'mixin';
- /**
- * @internal
- */
- const T_MIXIN_CONTENT = 'mixin_content';
- /**
- * @internal
- */
- const T_NESTED_PROPERTY = 'nestedprop';
- /**
- * @internal
- */
- const T_NOT = 'not';
const T_NULL = 'null';
const T_NUMBER = 'number';
- /**
- * @internal
- */
- const T_RETURN = 'return';
- /**
- * @internal
- */
- const T_ROOT = 'root';
- /**
- * @internal
- */
- const T_SCSSPHP_IMPORT_ONCE = 'scssphp-import-once';
- /**
- * @internal
- */
- const T_SELF = 'self';
const T_STRING = 'string';
- /**
- * @internal
- */
- const T_UNARY = 'unary';
- /**
- * @internal
- */
- const T_VARIABLE = 'var';
- /**
- * @internal
- */
- const T_WARN = 'warn';
- /**
- * @internal
- */
- const T_WHILE = 'while';
}
diff --git a/vendor/scssphp/scssphp/src/Util.php b/vendor/scssphp/scssphp/src/Util.php
index ad608ceb3..546f8ed80 100644
--- a/vendor/scssphp/scssphp/src/Util.php
+++ b/vendor/scssphp/scssphp/src/Util.php
@@ -12,9 +12,11 @@
namespace ScssPhp\ScssPhp;
-use ScssPhp\ScssPhp\Base\Range;
-use ScssPhp\ScssPhp\Exception\RangeException;
-use ScssPhp\ScssPhp\Node\Number;
+use League\Uri\Contracts\UriInterface;
+use League\Uri\Uri;
+use ScssPhp\ScssPhp\StackTrace\Frame;
+use ScssPhp\ScssPhp\Util\StringUtil;
+use SourceSpan\FileSpan;
/**
* Utility functions
@@ -23,162 +25,109 @@ use ScssPhp\ScssPhp\Node\Number;
*
* @internal
*/
-class Util
+final class Util
{
/**
- * Asserts that `value` falls within `range` (inclusive), leaving
- * room for slight floating-point errors.
- *
- * @param string $name The name of the value. Used in the error message.
- * @param Range $range Range of values.
- * @param array|Number $value The value to check.
- * @param string $unit The unit of the value. Used in error reporting.
- *
- * @return mixed `value` adjusted to fall within range, if it was outside by a floating-point margin.
- *
- * @throws \ScssPhp\ScssPhp\Exception\RangeException
+ * Returns $string with every line indented $indentation spaces.
*/
- public static function checkRange($name, Range $range, $value, $unit = '')
+ public static function indent(string $string, int $indentation): string
{
- $val = $value[1];
- $grace = new Range(-0.00001, 0.00001);
-
- if (! \is_numeric($val)) {
- throw new RangeException("$name {$val} is not a number.");
- }
-
- if ($range->includes($val)) {
- return $val;
- }
-
- if ($grace->includes($val - $range->first)) {
- return $range->first;
- }
-
- if ($grace->includes($val - $range->last)) {
- return $range->last;
- }
-
- throw new RangeException("$name {$val} must be between {$range->first} and {$range->last}$unit");
+ return implode("\n", array_map(function ($line) use ($indentation) {
+ return str_repeat(' ', $indentation) . $line;
+ }, explode("\n", $string)));
}
/**
* Encode URI component
- *
- * @param string $string
- *
- * @return string
*/
- public static function encodeURIComponent($string)
+ public static function encodeURIComponent(string $string): string
{
$revert = ['%21' => '!', '%2A' => '*', '%27' => "'", '%28' => '(', '%29' => ')'];
return strtr(rawurlencode($string), $revert);
}
+ public static function frameForSpan(FileSpan $span, string $member, ?UriInterface $url = null): Frame
+ {
+ return new Frame(
+ $url ?? $span->getSourceUrl() ?? Uri::new('-'),
+ $span->getStart()->getLine() + 1,
+ $span->getStart()->getColumn() + 1,
+ $member
+ );
+ }
+
/**
- * mb_chr() wrapper
- *
- * @param int $code
+ * 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.
*
- * @return string
+ * This isn't particularly efficient, and should only be used for error
+ * messages.
*/
- public static function mbChr($code)
+ public static function declarationName(FileSpan $span): string
{
- // 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')) {
- return mb_chr($code, 'UTF-8');
- }
-
- if (0x80 > $code %= 0x200000) {
- $s = \chr($code);
- } elseif (0x800 > $code) {
- $s = \chr(0xC0 | $code >> 6) . \chr(0x80 | $code & 0x3F);
- } elseif (0x10000 > $code) {
- $s = \chr(0xE0 | $code >> 12) . \chr(0x80 | $code >> 6 & 0x3F) . \chr(0x80 | $code & 0x3F);
- } else {
- $s = \chr(0xF0 | $code >> 18) . \chr(0x80 | $code >> 12 & 0x3F)
- . \chr(0x80 | $code >> 6 & 0x3F) . \chr(0x80 | $code & 0x3F);
- }
+ $text = $span->getText();
+ $pos = strpos($text, ':');
- return $s;
+ return StringUtil::trimAsciiRight(substr($text, 0, $pos === false ? null : $pos));
}
/**
- * mb_strlen() wrapper
+ * Returns $name without a vendor prefix.
*
- * @param string $string
- * @return int
+ * If $name has no vendor prefix, it's returned as-is.
*/
- public static function mbStrlen($string)
+ public static function unvendor(string $name): string
{
- // Use the native implementation if available.
- if (\function_exists('mb_strlen')) {
- return mb_strlen($string, 'UTF-8');
- }
+ $length = \strlen($name);
- if (\function_exists('iconv_strlen')) {
- return (int) @iconv_strlen($string, 'UTF-8');
+ if ($length < 2) {
+ return $name;
}
- throw new \LogicException('Either mbstring (recommended) or iconv is necessary to use Scssphp.');
- }
-
- /**
- * mb_substr() wrapper
- * @param string $string
- * @param int $start
- * @param null|int $length
- * @return string
- */
- public static function mbSubstr($string, $start, $length = null)
- {
- // Use the native implementation if available.
- if (\function_exists('mb_substr')) {
- return mb_substr($string, $start, $length, 'UTF-8');
+ if ($name[0] !== '-') {
+ return $name;
}
- if (\function_exists('iconv_substr')) {
- if ($start < 0) {
- $start = static::mbStrlen($string) + $start;
- if ($start < 0) {
- $start = 0;
- }
- }
+ if ($name[1] === '-') {
+ return $name;
+ }
- if (null === $length) {
- $length = 2147483647;
- } elseif ($length < 0) {
- $length = static::mbStrlen($string) + $length - $start;
- if ($length < 0) {
- return '';
- }
+ for ($i = 2; $i < $length; $i++) {
+ if ($name[$i] === '-') {
+ return substr($name, $i + 1);
}
-
- return (string)iconv_substr($string, $start, $length, 'UTF-8');
}
- throw new \LogicException('Either mbstring (recommended) or iconv is necessary to use Scssphp.');
+ return $name;
}
/**
- * mb_strpos wrapper
- * @param string $haystack
- * @param string $needle
- * @param int $offset
+ * Like {@see \SplObjectStorage::addAll()}, but for two-layer maps.
+ *
+ * This avoids copying inner maps from $source if possible.
+ *
+ * @template K1 of object
+ * @template K2 of object
+ * @template V
+ * @template Inner of \SplObjectStorage<K2, V>
*
- * @return int|false
+ * @param \SplObjectStorage<K1, Inner> $destination
+ * @param \SplObjectStorage<K1, Inner> $source
*/
- public static function mbStrpos($haystack, $needle, $offset = 0)
+ public static function mapAddAll2(\SplObjectStorage $destination, \SplObjectStorage $source): void
{
- if (\function_exists('mb_strpos')) {
- return mb_strpos($haystack, $needle, $offset, 'UTF-8');
- }
+ foreach ($source as $key) {
+ $inner = $source->getInfo();
- if (\function_exists('iconv_strpos')) {
- return iconv_strpos($haystack, $needle, $offset, 'UTF-8');
- }
+ $innerDestination = $destination[$key] ?? null;
- throw new \LogicException('Either mbstring (recommended) or iconv is necessary to use Scssphp.');
+ if ($innerDestination !== null) {
+ $innerDestination->addAll($inner);
+ } else {
+ $destination[$key] = $inner;
+ }
+ }
}
}
diff --git a/vendor/scssphp/scssphp/src/Util/ArrayUtil.php b/vendor/scssphp/scssphp/src/Util/ArrayUtil.php
new file mode 100644
index 000000000..a5c0249f3
--- /dev/null
+++ b/vendor/scssphp/scssphp/src/Util/ArrayUtil.php
@@ -0,0 +1,48 @@
+<?php
+
+/**
+ * SCSSPHP
+ *
+ * @copyright 2012-2020 Leaf Corcoran
+ *
+ * @license http://opensource.org/licenses/MIT MIT
+ *
+ * @link http://scssphp.github.io/scssphp
+ */
+
+namespace ScssPhp\ScssPhp\Util;
+
+/**
+ * @internal
+ */
+final class ArrayUtil
+{
+ /**
+ * Reduces a collection to a single value by iteratively combining elements
+ * of the collection using the provided function.
+ *
+ * The array must have at least one element.
+ * If it has only one element, that element is returned.
+ *
+ * Otherwise this method starts with the first element from the array,
+ * and then combines it with the remaining elements in iteration order.
+ *
+ * @template T
+ *
+ * @param non-empty-array<T> $items
+ * @param callable(T, T): T $combine
+ * @return T
+ *
+ * @param-immediately-invoked-callable $combine
+ */
+ public static function reduce(array $items, callable $combine)
+ {
+ if (\count($items) === 0) {
+ throw new \LogicException('Cannot reduce an empty array');
+ }
+
+ $first = array_shift($items);
+
+ return array_reduce($items, $combine, $first);
+ }
+}
diff --git a/vendor/scssphp/scssphp/src/Util/AstUtil.php b/vendor/scssphp/scssphp/src/Util/AstUtil.php
new file mode 100644
index 000000000..187e9f21f
--- /dev/null
+++ b/vendor/scssphp/scssphp/src/Util/AstUtil.php
@@ -0,0 +1,39 @@
+<?php
+
+/**
+ * SCSSPHP
+ *
+ * @copyright 2012-2020 Leaf Corcoran
+ *
+ * @license http://opensource.org/licenses/MIT MIT
+ *
+ * @link http://scssphp.github.io/scssphp
+ */
+
+namespace ScssPhp\ScssPhp\Util;
+
+use ScssPhp\ScssPhp\Ast\Sass\ArgumentInvocation;
+use ScssPhp\ScssPhp\Ast\Sass\Expression;
+use ScssPhp\ScssPhp\Ast\Sass\Expression\FunctionExpression;
+
+/**
+ * @internal
+ */
+final class AstUtil
+{
+ /**
+ * Converts $expression to an equivalent `calc()`.
+ *
+ * This assumes that $expression already returns a number. It's intended for
+ * use in end-user messaging, and may not produce directly evaluable
+ * expressions.
+ */
+ public static function expressionToCalc(Expression $expression): FunctionExpression
+ {
+ return new FunctionExpression(
+ 'calc',
+ new ArgumentInvocation([$expression->accept(new MakeExpressionCalculationSafe())], [], $expression->getSpan()),
+ $expression->getSpan()
+ );
+ }
+}
diff --git a/vendor/scssphp/scssphp/src/Util/Box.php b/vendor/scssphp/scssphp/src/Util/Box.php
new file mode 100644
index 000000000..f3435851f
--- /dev/null
+++ b/vendor/scssphp/scssphp/src/Util/Box.php
@@ -0,0 +1,50 @@
+<?php
+
+/**
+ * SCSSPHP
+ *
+ * @copyright 2012-2020 Leaf Corcoran
+ *
+ * @license http://opensource.org/licenses/MIT MIT
+ *
+ * @link http://scssphp.github.io/scssphp
+ */
+
+namespace ScssPhp\ScssPhp\Util;
+
+/**
+ * An unmodifiable reference to a value that may be mutated elsewhere.
+ *
+ * This uses reference equality based on the underlying {@see ModifiableBox}, even
+ * when the underlying type uses value equality.
+ *
+ * @template T
+ */
+final class Box implements Equatable
+{
+ /**
+ * @var ModifiableBox<T>
+ */
+ private readonly ModifiableBox $inner;
+
+ /**
+ * @param ModifiableBox<T> $inner
+ */
+ public function __construct(ModifiableBox $inner)
+ {
+ $this->inner = $inner;
+ }
+
+ /**
+ * @return T
+ */
+ public function getValue()
+ {
+ return $this->inner->getValue();
+ }
+
+ public function equals(object $other): bool
+ {
+ return $other instanceof Box && $this->inner === $other->inner;
+ }
+}
diff --git a/vendor/scssphp/scssphp/src/Util/Character.php b/vendor/scssphp/scssphp/src/Util/Character.php
new file mode 100644
index 000000000..f5535f24f
--- /dev/null
+++ b/vendor/scssphp/scssphp/src/Util/Character.php
@@ -0,0 +1,169 @@
+<?php
+
+/**
+ * SCSSPHP
+ *
+ * @copyright 2012-2020 Leaf Corcoran
+ *
+ * @license http://opensource.org/licenses/MIT MIT
+ *
+ * @link http://scssphp.github.io/scssphp
+ */
+
+namespace ScssPhp\ScssPhp\Util;
+
+/**
+ * @internal
+ */
+final class Character
+{
+ /**
+ * The difference between upper- and lowercase ASCII letters.
+ *
+ * `0b100000` can be bitwise-ORed with uppercase ASCII letters to get their
+ * lowercase equivalents.
+ */
+ private const ASCII_CASE_BIT = 0x20;
+
+ /**
+ * Returns whether $character is an ASCII whitespace character.
+ */
+ public static function isWhitespace(?string $character): bool
+ {
+ return $character === ' ' || $character === "\t" || $character === "\n" || $character === "\r" || $character === "\f";
+ }
+
+ /**
+ * Returns whether $character is a space or a tab character.
+ */
+ public static function isSpaceOrTab(?string $character): bool
+ {
+ return $character === ' ' || $character === "\t";
+ }
+
+ /**
+ * Returns whether $character is an ASCII newline character.
+ */
+ public static function isNewline(?string $character): bool
+ {
+ return $character === "\n" || $character === "\r" || $character === "\f";
+ }
+
+ /**
+ * Returns whether $character is a letter or a number.
+ */
+ public static function isAlphanumeric(string $character): bool
+ {
+ return self::isAlphabetic($character) || self::isDigit($character);
+ }
+
+ /**
+ * Returns whether $character is a letter.
+ */
+ public static function isAlphabetic(string $character): bool
+ {
+ $charCode = \ord($character);
+
+ return ($charCode >= \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 $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
+ {
+ return match ($character) {
+ '(' => ')',
+ '{' => '}',
+ '[' => ']',
+ default => throw new \InvalidArgumentException(sprintf('Expected a brace character. Got "%s"', $character)),
+ };
+ }
+
+ public static function equalsIgnoreCase(string $character1, string $character2): bool
+ {
+ if ($character1 === $character2) {
+ return true;
+ }
+
+ // If this check fails, the characters are definitely different. If it
+ // succeeds *and* either character is an ASCII letter, they're equivalent.
+ if ((\ord($character1) ^ \ord($character2)) !== self::ASCII_CASE_BIT) {
+ return false;
+ }
+
+ // Now we just need to verify that one of the characters is an ASCII letter.
+ $upperCase1 = \ord($character1) & ~self::ASCII_CASE_BIT;
+
+ return $upperCase1 >= \ord('A') && $upperCase1 <= \ord('Z');
+ }
+}
diff --git a/vendor/scssphp/scssphp/src/Util/Equatable.php b/vendor/scssphp/scssphp/src/Util/Equatable.php
new file mode 100644
index 000000000..739c8b1c3
--- /dev/null
+++ b/vendor/scssphp/scssphp/src/Util/Equatable.php
@@ -0,0 +1,21 @@
+<?php
+
+/**
+ * SCSSPHP
+ *
+ * @copyright 2012-2020 Leaf Corcoran
+ *
+ * @license http://opensource.org/licenses/MIT MIT
+ *
+ * @link http://scssphp.github.io/scssphp
+ */
+
+namespace ScssPhp\ScssPhp\Util;
+
+/**
+ * @internal
+ */
+interface Equatable
+{
+ public function equals(object $other): bool;
+}
diff --git a/vendor/scssphp/scssphp/src/Util/EquatableUtil.php b/vendor/scssphp/scssphp/src/Util/EquatableUtil.php
new file mode 100644
index 000000000..c1e60ea5a
--- /dev/null
+++ b/vendor/scssphp/scssphp/src/Util/EquatableUtil.php
@@ -0,0 +1,88 @@
+<?php
+
+/**
+ * SCSSPHP
+ *
+ * @copyright 2012-2020 Leaf Corcoran
+ *
+ * @license http://opensource.org/licenses/MIT MIT
+ *
+ * @link http://scssphp.github.io/scssphp
+ */
+
+namespace ScssPhp\ScssPhp\Util;
+
+/**
+ * @internal
+ */
+final class EquatableUtil
+{
+ /**
+ * @param iterable<mixed> $list
+ */
+ public static function iterableContains(iterable $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.
+ */
+ public static function equals(mixed $item1, mixed $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<mixed> $list1
+ * @param list<mixed> $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/vendor/scssphp/scssphp/src/Util/ErrorUtil.php b/vendor/scssphp/scssphp/src/Util/ErrorUtil.php
new file mode 100644
index 000000000..10d3e525e
--- /dev/null
+++ b/vendor/scssphp/scssphp/src/Util/ErrorUtil.php
@@ -0,0 +1,97 @@
+<?php
+
+/**
+ * SCSSPHP
+ *
+ * @copyright 2018-2020 Anthon Pang
+ *
+ * @license http://opensource.org/licenses/MIT MIT
+ *
+ * @link http://scssphp.github.io/scssphp
+ */
+
+namespace ScssPhp\ScssPhp\Util;
+
+use ScssPhp\ScssPhp\StackTrace\Trace;
+use SourceSpan\FileSpan;
+
+/**
+ * @internal
+ */
+final class ErrorUtil
+{
+ /**
+ * @throws \OutOfRangeException
+ */
+ public static function checkIntInInterval(int $value, int $minValue, int $maxValue, ?string $name = null): void
+ {
+ if ($value < $minValue || $value > $maxValue) {
+ $nameDisplay = $name ? " $name" : '';
+
+ throw new \OutOfRangeException("Invalid value:$nameDisplay must be between $minValue and $maxValue: $value.");
+ }
+ }
+
+ public static function formatErrorMessage(string $message, FileSpan $span, Trace $sassTrace): string
+ {
+ $formattedMessage = $message . "\n" . $span->highlight();
+
+ foreach (explode("\n", $sassTrace->getFormattedTrace()) as $frame) {
+ if ($frame === '') {
+ continue;
+ }
+ $formattedMessage .= "\n";
+ $formattedMessage .= ' ' . $frame;
+ }
+
+ return $formattedMessage;
+ }
+
+ /**
+ * @param array<string, FileSpan> $secondarySpans
+ */
+ public static function formatErrorMessageMultiple(string $message, FileSpan $span, string $primaryLabel, array $secondarySpans, Trace $sassTrace): string
+ {
+ $formattedMessage = $message . "\n" . $span->highlightMultiple($primaryLabel, $secondarySpans);
+
+ foreach (explode("\n", $sassTrace->getFormattedTrace()) as $frame) {
+ if ($frame === '') {
+ continue;
+ }
+ $formattedMessage .= "\n";
+ $formattedMessage .= ' ' . $frame;
+ }
+
+ return $formattedMessage;
+ }
+
+ /**
+ * 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 ??= '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 ??= 'end';
+ $endNameDisplay = $endName ? " $endName" : '';
+
+ throw new \OutOfRangeException("Invalid value:$endNameDisplay must be between $start and $length: $end.");
+ }
+ }
+ }
+}
diff --git a/vendor/scssphp/scssphp/src/Util/IterableUtil.php b/vendor/scssphp/scssphp/src/Util/IterableUtil.php
new file mode 100644
index 000000000..918adc96a
--- /dev/null
+++ b/vendor/scssphp/scssphp/src/Util/IterableUtil.php
@@ -0,0 +1,98 @@
+<?php
+
+/**
+ * SCSSPHP
+ *
+ * @copyright 2018-2020 Anthon Pang
+ *
+ * @license http://opensource.org/licenses/MIT MIT
+ *
+ * @link http://scssphp.github.io/scssphp
+ */
+
+namespace ScssPhp\ScssPhp\Util;
+
+/**
+ * @internal
+ */
+final class IterableUtil
+{
+ /**
+ * @template T
+ *
+ * @param iterable<T> $list
+ * @param callable(T): bool $callback
+ *
+ * @param-immediately-invoked-callable $callback
+ */
+ public static function any(iterable $list, callable $callback): bool
+ {
+ foreach ($list as $item) {
+ if ($callback($item)) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * @template T
+ *
+ * @param iterable<T> $list
+ * @param callable(T): bool $callback
+ *
+ * @param-immediately-invoked-callable $callback
+ */
+ public static function every(iterable $list, callable $callback): bool
+ {
+ foreach ($list as $item) {
+ if (!$callback($item)) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * @template T
+ *
+ * @param iterable<T> $iterable
+ * @return T|null
+ */
+ public static function firstOrNull(iterable $iterable): mixed
+ {
+ foreach ($iterable as $item) {
+ return $item;
+ }
+
+ return null;
+ }
+
+ /**
+ * Returns the first `T` returned by $callback for an element of $iterable,
+ * or `null` if it returns `null` for every element.
+ *
+ * @template T
+ * @template E
+ * @param iterable<E> $iterable
+ * @param callable(E): (T|null) $callback
+ *
+ * @return T|null
+ *
+ * @param-immediately-invoked-callable $callback
+ */
+ public static function search(iterable $iterable, callable $callback)
+ {
+ foreach ($iterable as $element) {
+ $value = $callback($element);
+
+ if ($value !== null) {
+ return $value;
+ }
+ }
+
+ return null;
+ }
+}
diff --git a/vendor/scssphp/scssphp/src/Util/ListUtil.php b/vendor/scssphp/scssphp/src/Util/ListUtil.php
new file mode 100644
index 000000000..df5ca49fa
--- /dev/null
+++ b/vendor/scssphp/scssphp/src/Util/ListUtil.php
@@ -0,0 +1,157 @@
+<?php
+
+/**
+ * SCSSPHP
+ *
+ * @copyright 2018-2020 Anthon Pang
+ *
+ * @license http://opensource.org/licenses/MIT MIT
+ *
+ * @link http://scssphp.github.io/scssphp
+ */
+
+namespace ScssPhp\ScssPhp\Util;
+
+/**
+ * @internal
+ */
+final class ListUtil
+{
+ /**
+ * Flattens the first level of nested arrays in $queues.
+ *
+ * The return value is ordered first by index in the nested iterable, then by
+ * the index *of* that iterable in $queues. For example,
+ * `flattenVertically([["1a", "1b"], ["2a", "2b"]])` returns `["1a", "2a",
+ * "1b", "2b"]`.
+ *
+ * @template T
+ *
+ * @param list<list<T>> $queues
+ *
+ * @return list<T>
+ */
+ 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<T> $list1
+ * @param list<T> $list2
+ * @param (callable(T, T): (T|null))|null $select
+ *
+ * @return list<T>
+ */
+ public static function longestCommonSubsequence(array $list1, array $list2, ?callable $select = null): array
+ {
+ if ($select === null) {
+ $select = fn($element1, $element2) => 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;
+ }
+ }
+
+ /**
+ * @param int $i
+ * @param int $j
+ * @return list<T>
+ */
+ $backtrack = function (int $i, int $j) use ($selections, $lengths, &$backtrack) {
+ 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<T> $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<T> $list
+ *
+ * @return list<T>
+ */
+ 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/vendor/scssphp/scssphp/src/Util/LoggerUtil.php b/vendor/scssphp/scssphp/src/Util/LoggerUtil.php
new file mode 100644
index 000000000..0f21f9a1f
--- /dev/null
+++ b/vendor/scssphp/scssphp/src/Util/LoggerUtil.php
@@ -0,0 +1,34 @@
+<?php
+
+/**
+ * SCSSPHP
+ *
+ * @copyright 2012-2020 Leaf Corcoran
+ *
+ * @license http://opensource.org/licenses/MIT MIT
+ *
+ * @link http://scssphp.github.io/scssphp
+ */
+
+namespace ScssPhp\ScssPhp\Util;
+
+use ScssPhp\ScssPhp\Deprecation;
+use ScssPhp\ScssPhp\Logger\DeprecationProcessingLogger;
+use ScssPhp\ScssPhp\Logger\LoggerInterface;
+use ScssPhp\ScssPhp\StackTrace\Trace;
+use SourceSpan\FileSpan;
+
+/**
+ * @internal
+ */
+final class LoggerUtil
+{
+ public static function warnForDeprecation(LoggerInterface $logger, Deprecation $deprecation, string $message, ?FileSpan $span = null, ?Trace $trace = null): void
+ {
+ if ($deprecation->isFuture() && !$logger instanceof DeprecationProcessingLogger) {
+ return;
+ }
+
+ $logger->warn($message, $deprecation, $span, $trace);
+ }
+}
diff --git a/vendor/scssphp/scssphp/src/Util/MakeExpressionCalculationSafe.php b/vendor/scssphp/scssphp/src/Util/MakeExpressionCalculationSafe.php
new file mode 100644
index 000000000..4f7e132cf
--- /dev/null
+++ b/vendor/scssphp/scssphp/src/Util/MakeExpressionCalculationSafe.php
@@ -0,0 +1,71 @@
+<?php
+
+/**
+ * SCSSPHP
+ *
+ * @copyright 2012-2020 Leaf Corcoran
+ *
+ * @license http://opensource.org/licenses/MIT MIT
+ *
+ * @link http://scssphp.github.io/scssphp
+ */
+
+namespace ScssPhp\ScssPhp\Util;
+
+use ScssPhp\ScssPhp\Ast\Sass\ArgumentInvocation;
+use ScssPhp\ScssPhp\Ast\Sass\Expression;
+use ScssPhp\ScssPhp\Ast\Sass\Expression\BinaryOperationExpression;
+use ScssPhp\ScssPhp\Ast\Sass\Expression\BinaryOperator;
+use ScssPhp\ScssPhp\Ast\Sass\Expression\FunctionExpression;
+use ScssPhp\ScssPhp\Ast\Sass\Expression\InterpolatedFunctionExpression;
+use ScssPhp\ScssPhp\Ast\Sass\Expression\NumberExpression;
+use ScssPhp\ScssPhp\Ast\Sass\Expression\UnaryOperationExpression;
+use ScssPhp\ScssPhp\Ast\Sass\Expression\UnaryOperator;
+use ScssPhp\ScssPhp\Visitor\ReplaceExpressionVisitor;
+
+/**
+ * A visitor that replaces constructs that can't be used in a calculation with
+ * those that can.
+ *
+ * @internal
+ */
+final class MakeExpressionCalculationSafe extends ReplaceExpressionVisitor
+{
+ public function visitBinaryOperationExpression(BinaryOperationExpression $node): Expression
+ {
+ // `calc()` doesn't support `%` for modulo but Sass doesn't yet support the
+ // `mod()` calculation function because there's no browser support, so we have
+ // to work around it by wrapping the call in a Sass function.
+ if ($node->getOperator() === BinaryOperator::MODULO) {
+ return new FunctionExpression('max', new ArgumentInvocation([$node], [], $node->getSpan()), $node->getSpan(), 'math');
+ }
+
+ return parent::visitBinaryOperationExpression($node);
+ }
+
+ public function visitInterpolatedFunctionExpression(InterpolatedFunctionExpression $node): Expression
+ {
+ return $node;
+ }
+
+ public function visitUnaryOperationExpression(UnaryOperationExpression $node): Expression
+ {
+ switch ($node->getOperator()) {
+ // `calc()` doesn't support unary operations.
+ case UnaryOperator::PLUS:
+ return $node->getOperand();
+
+ case UnaryOperator::MINUS:
+ return new BinaryOperationExpression(
+ BinaryOperator::TIMES,
+ new NumberExpression(-1, $node->getSpan()),
+ $node->getOperand()
+ );
+
+ // Other unary operations don't produce numbers, so keep them as-is to
+ // give the user a more useful syntax error after serialization.
+ default:
+ return parent::visitUnaryOperationExpression($node);
+ }
+ }
+}
diff --git a/vendor/scssphp/scssphp/src/Util/ModifiableBox.php b/vendor/scssphp/scssphp/src/Util/ModifiableBox.php
new file mode 100644
index 000000000..f52b26654
--- /dev/null
+++ b/vendor/scssphp/scssphp/src/Util/ModifiableBox.php
@@ -0,0 +1,67 @@
+<?php
+
+/**
+ * SCSSPHP
+ *
+ * @copyright 2012-2020 Leaf Corcoran
+ *
+ * @license http://opensource.org/licenses/MIT MIT
+ *
+ * @link http://scssphp.github.io/scssphp
+ */
+
+namespace ScssPhp\ScssPhp\Util;
+
+/**
+ * A mutable reference to a (presumably immutable) value.
+ *
+ * This always uses reference equality, even when the underlying type uses
+ * value equality.
+ *
+ * @template T
+ *
+ * @internal
+ */
+final class ModifiableBox
+{
+ /**
+ * @var T
+ */
+ private mixed $value;
+
+ /**
+ * @param T $value
+ */
+ public function __construct(mixed $value)
+ {
+ $this->value = $value;
+ }
+
+ /**
+ * @return T
+ */
+ public function getValue()
+ {
+ return $this->value;
+ }
+
+ /**
+ * @param T $value
+ */
+ public function setValue(mixed $value): void
+ {
+ $this->value = $value;
+ }
+
+ /**
+ * Returns an unmodifiable reference to this box.
+ *
+ * The underlying modifiable box may still be modified.
+ *
+ * @return Box<T>
+ */
+ public function seal(): Box
+ {
+ return new Box($this);
+ }
+}
diff --git a/vendor/scssphp/scssphp/src/Util/NumberUtil.php b/vendor/scssphp/scssphp/src/Util/NumberUtil.php
new file mode 100644
index 000000000..7a54ad771
--- /dev/null
+++ b/vendor/scssphp/scssphp/src/Util/NumberUtil.php
@@ -0,0 +1,305 @@
+<?php
+
+/**
+ * SCSSPHP
+ *
+ * @copyright 2018-2020 Anthon Pang
+ *
+ * @license http://opensource.org/licenses/MIT MIT
+ *
+ * @link http://scssphp.github.io/scssphp
+ */
+
+namespace ScssPhp\ScssPhp\Util;
+
+use ScssPhp\ScssPhp\Value\SassNumber;
+
+/**
+ * Utilities to deal with numbers with fuzziness for the Sass precision
+ *
+ * @internal
+ */
+final class NumberUtil
+{
+ /**
+ * The power of ten to which to round Sass numbers to determine if they're
+ * fuzzy equal to one another
+ *
+ * This is also the minimum distance such that `a - b > 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 clamp(float $value, float $lowerLimit, float $upperLimit): float
+ {
+ if ($value < $lowerLimit) {
+ return $lowerLimit;
+ }
+
+ if ($value > $upperLimit) {
+ return $upperLimit;
+ }
+
+ return $value;
+ }
+
+ 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;
+ }
+
+ if ($number > \PHP_INT_MAX || $number < \PHP_INT_MIN) {
+ 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;
+ }
+
+ /**
+ * @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.
+ */
+ 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 (is_infinite($num1)) {
+ return NAN;
+ }
+
+ if (is_infinite($num2)) {
+ return self::signIncludingZero($num1) === self::sign($num2) ? $num1 : NAN;
+ }
+
+ 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;
+ }
+
+ public static function sqrt(SassNumber $number): SassNumber
+ {
+ $number->assertNoUnits('number');
+
+ return SassNumber::create(sqrt($number->getValue()));
+ }
+
+ public static function sin(SassNumber $number): SassNumber
+ {
+ return SassNumber::create(sin($number->coerceValueToUnit('rad', 'number')));
+ }
+
+ public static function cos(SassNumber $number): SassNumber
+ {
+ return SassNumber::create(cos($number->coerceValueToUnit('rad', 'number')));
+ }
+
+ public static function tan(SassNumber $number): SassNumber
+ {
+ return SassNumber::create(tan($number->coerceValueToUnit('rad', 'number')));
+ }
+
+ public static function atan(SassNumber $number): SassNumber
+ {
+ $number->assertNoUnits('number');
+ return self::radiansToDegrees(atan($number->getValue()));
+ }
+
+ public static function asin(SassNumber $number): SassNumber
+ {
+ $number->assertNoUnits('number');
+ return self::radiansToDegrees(asin($number->getValue()));
+ }
+
+ public static function acos(SassNumber $number): SassNumber
+ {
+ $number->assertNoUnits('number');
+ return self::radiansToDegrees(acos($number->getValue()));
+ }
+
+ public static function abs(SassNumber $number): SassNumber
+ {
+ return SassNumber::create(abs($number->getValue()))->coerceToMatch($number);
+ }
+
+ public static function log(SassNumber $number, ?SassNumber $base): SassNumber
+ {
+ if ($base !== null) {
+ return SassNumber::create(self::divideLikeSass(log($number->getValue()), log($base->getValue())));
+ }
+
+ return SassNumber::create(log($number->getValue()));
+ }
+
+ public static function pow(SassNumber $base, SassNumber $exponent): SassNumber
+ {
+ $base->assertNoUnits('base');
+ $exponent->assertNoUnits('exponent');
+
+ if (\PHP_VERSION_ID >= 80400) {
+ $value = fpow($base->getValue(), $exponent->getValue());
+ } else {
+ $value = $base->getValue() ** $exponent->getValue();
+ }
+
+ return SassNumber::create($value);
+ }
+
+ public static function atan2(SassNumber $y, SassNumber $x): SassNumber
+ {
+ return self::radiansToDegrees(atan2($y->getValue(), $x->convertValueToMatch($y, 'x', 'y')));
+ }
+
+ private static function radiansToDegrees(float $radians): SassNumber
+ {
+ return SassNumber::withUnits($radians * (180 / \M_PI), ['deg']);
+ }
+
+ public static function sign(float $num): int
+ {
+ if ($num > 0) {
+ return 1;
+ }
+
+ if ($num < 0) {
+ return -1;
+ }
+
+ return 0;
+ }
+
+ public static function signIncludingZero(float $num): int
+ {
+ // In PHP, negative 0 and positive 0 are equal even for strict equality, so we need a different detection
+ if ($num === 0.0) {
+ if ('-0' === (string) $num) {
+ return -1;
+ }
+
+ return 1;
+ }
+
+ return self::sign($num);
+ }
+}
diff --git a/vendor/scssphp/scssphp/src/Util/ParserUtil.php b/vendor/scssphp/scssphp/src/Util/ParserUtil.php
new file mode 100644
index 000000000..c70b841a1
--- /dev/null
+++ b/vendor/scssphp/scssphp/src/Util/ParserUtil.php
@@ -0,0 +1,68 @@
+<?php
+
+/**
+ * SCSSPHP
+ *
+ * @copyright 2012-2020 Leaf Corcoran
+ *
+ * @license http://opensource.org/licenses/MIT MIT
+ *
+ * @link http://scssphp.github.io/scssphp
+ */
+
+namespace ScssPhp\ScssPhp\Util;
+
+use ScssPhp\ScssPhp\Parser\StringScanner;
+
+/**
+ * @internal
+ */
+final class ParserUtil
+{
+ /**
+ * Consumes an escape sequence from $scanner and returns the character it
+ * represents.
+ */
+ public static function consumeEscapedCharacter(StringScanner $scanner): string
+ {
+ // See https://drafts.csswg.org/css-syntax-3/#consume-escaped-code-point.
+ $scanner->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 mb_chr($value, 'UTF-8');
+ }
+
+ return $scanner->readUtf8Char();
+ }
+}
diff --git a/vendor/scssphp/scssphp/src/Util/Path.php b/vendor/scssphp/scssphp/src/Util/Path.php
index f399e41ab..eb183927c 100644
--- a/vendor/scssphp/scssphp/src/Util/Path.php
+++ b/vendor/scssphp/scssphp/src/Util/Path.php
@@ -2,17 +2,40 @@
namespace ScssPhp\ScssPhp\Util;
+use League\Uri\BaseUri;
+use League\Uri\Contracts\UriInterface;
+use League\Uri\Uri;
+use Symfony\Component\Filesystem\Exception\InvalidArgumentException;
+use Symfony\Component\Filesystem\Path as SymfonyPath;
+
/**
* @internal
*/
-class Path
+final class Path
{
/**
- * @param string $path
- *
- * @return bool
+ * @var array<string, string>
*/
- public static function isAbsolute($path)
+ private static array $realCaseCache = [];
+ public static function toUri(string $path): UriInterface
+ {
+ if (\DIRECTORY_SEPARATOR === '\\') {
+ return Uri::fromWindowsPath($path);
+ }
+
+ return Uri::fromUnixPath($path);
+ }
+
+ public static function fromUri(UriInterface $uri): string
+ {
+ if (\DIRECTORY_SEPARATOR === '\\') {
+ return BaseUri::from($uri)->windowsPath() ?? throw new \InvalidArgumentException("Uri $uri must have scheme 'file:'.");
+ }
+
+ return BaseUri::from($uri)->unixPath() ?? throw new \InvalidArgumentException("Uri $uri must have scheme 'file:'.");
+ }
+
+ public static function isAbsolute(string $path): bool
{
if ($path === '') {
return false;
@@ -22,10 +45,133 @@ class Path
return true;
}
- if (\DIRECTORY_SEPARATOR !== '\\') {
+ if (\DIRECTORY_SEPARATOR === '\\') {
+ return self::isWindowsAbsolute($path);
+ }
+
+ return false;
+ }
+
+ /**
+ * Canonicalizes $path.
+ *
+ * This is guaranteed to return the same path for two different input paths
+ * if and only if both input paths point to the same location. Unlike
+ * {@see normalize}, it returns absolute paths when possible and canonicalizes
+ * ASCII case on Windows.
+ *
+ * Note that this does not resolve symlinks.
+ */
+ public static function canonicalize(string $path): string
+ {
+ return self::realCasePath(self::normalize(self::absolute($path)));
+ }
+
+ /**
+ * Normalizes $path, simplifying it by handling `..`, and `.`, and
+ * removing redundant path separators whenever possible.
+ *
+ * Note that this is *not* guaranteed to return the same result for two
+ * equivalent input paths.
+ */
+ public static function normalize(string $path): string
+ {
+ $normalized = SymfonyPath::canonicalize($path);
+
+ // The Symfony Path class always uses / as separator, while we want to use the platform one to get a real path
+ if (\DIRECTORY_SEPARATOR === '\\') {
+ $normalized = str_replace('/', '\\', $normalized);
+ }
+
+ return $normalized;
+ }
+
+ /**
+ * Attempts to convert $path to an equivalent relative path from $from.
+ *
+ * Since there is no relative path from one drive letter to another on Windows,
+ * this will return an absolute path in those cases.
+ */
+ public static function relative(string $path, string $from): string
+ {
+ try {
+ $relativePath = SymfonyPath::makeRelative($path, $from);
+ } catch (InvalidArgumentException) {
+ return $path;
+ }
+
+ // The Symfony Path class always uses / as separator, while we want to use the platform one to get a real path
+ if (\DIRECTORY_SEPARATOR === '\\') {
+ $relativePath = str_replace('/', '\\', $relativePath);
+ }
+
+ return $relativePath;
+ }
+
+ private static function realCasePath(string $path): string
+ {
+ if (!(\PHP_OS_FAMILY === 'Windows' || \PHP_OS_FAMILY === 'Darwin')) {
+ return $path;
+ }
+
+ if (\PHP_OS_FAMILY === 'Windows') {
+ // Drive names are *always* case-insensitive, so convert them to uppercase.
+ if (self::isAbsolute($path) && Character::isAlphabetic($path[0])) {
+ $path = strtoupper(substr($path, 0, 3)) . substr($path, 3);
+ }
+ }
+
+ return self::realCasePathHelper($path);
+ }
+
+ private static function realCasePathHelper(string $path): string
+ {
+ $dirname = dirname($path);
+
+ if ($dirname === $path || $dirname === '.') {
+ return $path;
+ }
+
+ return self::$realCaseCache[$path] ??= self::computeRealCasePath($path);
+ }
+
+ private static function computeRealCasePath(string $path): string
+ {
+ $realDirname = self::realCasePathHelper(dirname($path));
+ $basename = basename($path);
+
+ $files = @scandir($realDirname);
+
+ if ($files === false) {
+ // If there's an error listing a directory, it's likely because we're
+ // trying to reach too far out of the current directory into something
+ // we don't have permissions for. In that case, just assume we have the
+ // real path.
+ return $path;
+ }
+
+ $matches = array_values(array_filter($files, fn ($realPath) => StringUtil::equalsIgnoreCase(basename($realPath), $basename)));
+
+ if (\count($matches) === 1) {
+ return self::join($realDirname, $matches[0]);
+ }
+
+ // If the file doesn't exist, or if there are multiple options
+ // (meaning the filesystem isn't actually case-insensitive), use
+ // `basename` as-is.
+ return self::join($realDirname, $basename);
+ }
+
+ public static function isWindowsAbsolute(string $path): bool
+ {
+ if ($path === '') {
return false;
}
+ if ($path[0] === '/') {
+ return true;
+ }
+
if ($path[0] === '\\') {
return true;
}
@@ -49,13 +195,7 @@ class Path
return true;
}
- /**
- * @param string $part1
- * @param string $part2
- *
- * @return string
- */
- public static function join($part1, $part2)
+ public static function join(string $part1, string $part2): string
{
if ($part1 === '' || self::isAbsolute($part2)) {
return $part2;
@@ -74,4 +214,76 @@ class Path
return $part1 . $separator . $part2;
}
+
+ public static function absolute(string $path): string
+ {
+ $cwd = getcwd();
+
+ if ($cwd === false) {
+ return $path;
+ }
+
+ return self::join($cwd, $path);
+ }
+
+ /**
+ * Gets the file extension of $path: the portion of basename from the last
+ * `.` to the end (including the `.` itself).
+ *
+ * If the file name starts with a `.`, then that is not considered the
+ * extension
+ */
+ public static function extension(string $path): string
+ {
+ $basename = basename($path);
+
+ $lastDot = strrpos($basename, '.');
+
+ if ($lastDot === false || $lastDot === 0) {
+ return '';
+ }
+
+ return substr($basename, $lastDot);
+ }
+
+ public static function withoutExtension(string $path): string
+ {
+ $extension = self::extension($path);
+
+ if ($extension === '') {
+ return $path;
+ }
+
+ return substr($path, 0, -\strlen($extension));
+ }
+
+ /**
+ * Returns a pretty URI for a path
+ */
+ public static function prettyUri(string|UriInterface $path): string
+ {
+ if ($path instanceof UriInterface) {
+ if ($path->getScheme() !== 'file') {
+ return (string) $path;
+ }
+
+ $path = self::fromUri($path);
+ }
+
+ $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 (str_starts_with($normalizedPath, $normalizedRootDirectory)) {
+ return substr($path, \strlen($normalizedRootDirectory));
+ }
+
+ return $path;
+ }
}
diff --git a/vendor/scssphp/scssphp/src/Util/SpanUtil.php b/vendor/scssphp/scssphp/src/Util/SpanUtil.php
new file mode 100644
index 000000000..a464b4a37
--- /dev/null
+++ b/vendor/scssphp/scssphp/src/Util/SpanUtil.php
@@ -0,0 +1,148 @@
+<?php
+
+/**
+ * SCSSPHP
+ *
+ * @copyright 2012-2020 Leaf Corcoran
+ *
+ * @license http://opensource.org/licenses/MIT MIT
+ *
+ * @link http://scssphp.github.io/scssphp
+ */
+
+namespace ScssPhp\ScssPhp\Util;
+
+use ScssPhp\ScssPhp\Parser\StringScanner;
+use SourceSpan\FileSpan;
+use SourceSpan\SourceFile;
+
+/**
+ * @internal
+ */
+final class SpanUtil
+{
+ public static function bogusSpan(): FileSpan
+ {
+ return SourceFile::fromString('')->span(0);
+ }
+
+ /**
+ * Returns this span with all whitespace trimmed from both sides.
+ */
+ public static function trim(FileSpan $span): FileSpan
+ {
+ return self::trimRight(self::trimLeft($span));
+ }
+
+ /**
+ * Returns this span with all leading whitespace trimmed.
+ */
+ public static function trimLeft(FileSpan $span): FileSpan
+ {
+ $start = 0;
+ $text = $span->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/vendor/scssphp/scssphp/src/Util/StringUtil.php b/vendor/scssphp/scssphp/src/Util/StringUtil.php
new file mode 100644
index 000000000..751d7869a
--- /dev/null
+++ b/vendor/scssphp/scssphp/src/Util/StringUtil.php
@@ -0,0 +1,181 @@
+<?php
+
+/**
+ * SCSSPHP
+ *
+ * @copyright 2012-2020 Leaf Corcoran
+ *
+ * @license http://opensource.org/licenses/MIT MIT
+ *
+ * @link http://scssphp.github.io/scssphp
+ */
+
+namespace ScssPhp\ScssPhp\Util;
+
+/**
+ * @internal
+ */
+final class StringUtil
+{
+ /**
+ * @param non-empty-array<string> $iter
+ */
+ public static function toSentence(array $iter, string $conjunction = 'and'): string
+ {
+ if (\count($iter) === 1) {
+ return $iter[array_key_first($iter)];
+ }
+
+ $last = array_pop($iter);
+
+ return implode(', ', $iter) . ' ' . $conjunction . ' ' . $last;
+ }
+
+ /**
+ * Returns $name if $number is 1, or the plural of $name otherwise.
+ *
+ * By default, this just adds "s" to the end of $name to get the plural. If
+ * $plural is passed, that's used instead.
+ */
+ public static function pluralize(string $name, int $number, ?string $plural = null): string
+ {
+ if ($number === 1) {
+ return $name;
+ }
+
+ if ($plural !== null) {
+ return $plural;
+ }
+
+ return $name . 's';
+ }
+
+ public static function trimAscii(string $string, bool $excludeEscape = false): string
+ {
+ $start = self::firstNonWhitespace($string);
+
+ if ($start === null) {
+ return '';
+ }
+
+ $end = self::lastNonWhitespace($string, $excludeEscape);
+ assert($end !== null);
+
+ return substr($string, $start, $end + 1);
+ }
+
+ 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 first 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 firstNonWhitespace(string $string): ?int
+ {
+ for ($i = 0; $i < \strlen($string); $i++) {
+ $char = $string[$i];
+
+ if (!Character::isWhitespace($char)) {
+ return $i;
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * 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.
+ */
+ 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);
+ }
+
+ /**
+ * Returns whether $string starts with $prefix, ignoring ASCII case.
+ */
+ public static function startsWithIgnoreCase(string $string, string $prefix): bool
+ {
+ if (\strlen($string) < \strlen($prefix)) {
+ return false;
+ }
+
+ for ($i = 0; $i < \strlen($prefix); $i++) {
+ if (!Character::equalsIgnoreCase($string[$i], $prefix[$i])) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * Converts all ASCII chars to lowercase in the input string.
+ *
+ * This does not use `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.
+ */
+ public static function toAsciiLowerCase(string $string): string
+ {
+ return strtr($string, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz');
+ }
+
+ /**
+ * Converts all ASCII chars to uppercase in the input string.
+ *
+ * This does not use `strtoupper` because `strtoupper` 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.
+ */
+ public static function toAsciiUpperCase(string $string): string
+ {
+ return strtr($string, 'abcdefghijklmnopqrstuvwxyz', 'ABCDEFGHIJKLMNOPQRSTUVWXYZ');
+ }
+}
diff --git a/vendor/scssphp/scssphp/src/Util/UriUtil.php b/vendor/scssphp/scssphp/src/Util/UriUtil.php
new file mode 100644
index 000000000..e0dbc34ad
--- /dev/null
+++ b/vendor/scssphp/scssphp/src/Util/UriUtil.php
@@ -0,0 +1,40 @@
+<?php
+
+/**
+ * SCSSPHP
+ *
+ * @copyright 2012-2020 Leaf Corcoran
+ *
+ * @license http://opensource.org/licenses/MIT MIT
+ *
+ * @link http://scssphp.github.io/scssphp
+ */
+
+namespace ScssPhp\ScssPhp\Util;
+
+use League\Uri\BaseUri;
+use League\Uri\Contracts\UriInterface;
+
+/**
+ * @internal
+ */
+final class UriUtil
+{
+ public static function resolve(UriInterface $baseUrl, string $reference): UriInterface
+ {
+ $resolvedUri = BaseUri::from($baseUrl)->resolve($reference)->getUri();
+
+ \assert($resolvedUri instanceof UriInterface);
+
+ return $resolvedUri;
+ }
+
+ public static function resolveUri(UriInterface $baseUrl, UriInterface $url): UriInterface
+ {
+ $resolvedUri = BaseUri::from($baseUrl)->resolve($url)->getUri();
+
+ \assert($resolvedUri instanceof UriInterface);
+
+ return $resolvedUri;
+ }
+}
diff --git a/vendor/scssphp/scssphp/src/Value/CalculationOperation.php b/vendor/scssphp/scssphp/src/Value/CalculationOperation.php
new file mode 100644
index 000000000..baf88f8ef
--- /dev/null
+++ b/vendor/scssphp/scssphp/src/Value/CalculationOperation.php
@@ -0,0 +1,77 @@
+<?php
+
+/**
+ * SCSSPHP
+ *
+ * @copyright 2012-2020 Leaf Corcoran
+ *
+ * @license http://opensource.org/licenses/MIT MIT
+ *
+ * @link http://scssphp.github.io/scssphp
+ */
+
+namespace ScssPhp\ScssPhp\Value;
+
+use ScssPhp\ScssPhp\Serializer\Serializer;
+use ScssPhp\ScssPhp\Util\Equatable;
+
+/**
+ * A binary operation that can appear in a {@see SassCalculation}.
+ */
+final class CalculationOperation implements Equatable, \Stringable
+{
+ private readonly CalculationOperator $operator;
+
+ /**
+ * The left-hand operand.
+ *
+ * This is either a {@see SassNumber}, a {@see SassCalculation}, an unquoted
+ * {@see SassString}, or a {@see CalculationOperation}.
+ */
+ private readonly object $left;
+
+ /**
+ * The right-hand operand.
+ *
+ * This is either a {@see SassNumber}, a {@see SassCalculation}, an unquoted
+ * {@see SassString}, or a {@see CalculationOperation}.
+ */
+ private readonly object $right;
+
+ public function __construct(CalculationOperator $operator, object $left, object $right)
+ {
+ $this->operator = $operator;
+ $this->left = $left;
+ $this->right = $right;
+ }
+
+ public function getOperator(): CalculationOperator
+ {
+ 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);
+ }
+
+ public function __toString(): string
+ {
+ $parenthesized = Serializer::serializeValue(SassCalculation::unsimplified('', [$this]), true);
+
+ return substr($parenthesized, 1, \strlen($parenthesized) - 2);
+ }
+}
diff --git a/vendor/scssphp/scssphp/src/Value/CalculationOperator.php b/vendor/scssphp/scssphp/src/Value/CalculationOperator.php
new file mode 100644
index 000000000..cc1eefca2
--- /dev/null
+++ b/vendor/scssphp/scssphp/src/Value/CalculationOperator.php
@@ -0,0 +1,49 @@
+<?php
+
+/**
+ * SCSSPHP
+ *
+ * @copyright 2012-2020 Leaf Corcoran
+ *
+ * @license http://opensource.org/licenses/MIT MIT
+ *
+ * @link http://scssphp.github.io/scssphp
+ */
+
+namespace ScssPhp\ScssPhp\Value;
+
+/**
+ * An enumeration of possible operators for {@see CalculationOperation}.
+ */
+enum CalculationOperator
+{
+ case PLUS;
+ case MINUS;
+ case TIMES;
+ case DIVIDED_BY;
+
+ public function getOperator(): string
+ {
+ return match ($this) {
+ self::PLUS => '+',
+ self::MINUS => '-',
+ self::TIMES => '*',
+ self::DIVIDED_BY => '/',
+ };
+ }
+
+ /**
+ * The precedence of the operator
+ *
+ * An operator with higher precedence binds tighter.
+ *
+ * @internal
+ */
+ public function getPrecedence(): int
+ {
+ return match ($this) {
+ self::PLUS, self::MINUS => 1,
+ self::TIMES, self::DIVIDED_BY => 2,
+ };
+ }
+}
diff --git a/vendor/scssphp/scssphp/src/Value/ColorFormat.php b/vendor/scssphp/scssphp/src/Value/ColorFormat.php
new file mode 100644
index 000000000..0188fe46e
--- /dev/null
+++ b/vendor/scssphp/scssphp/src/Value/ColorFormat.php
@@ -0,0 +1,23 @@
+<?php
+
+/**
+ * SCSSPHP
+ *
+ * @copyright 2012-2020 Leaf Corcoran
+ *
+ * @license http://opensource.org/licenses/MIT MIT
+ *
+ * @link http://scssphp.github.io/scssphp
+ */
+
+namespace ScssPhp\ScssPhp\Value;
+
+use JiriPudil\SealedClasses\Sealed;
+
+/**
+ * @internal
+ */
+#[Sealed(permits: [ColorFormatEnum::class, SpanColorFormat::class])]
+interface ColorFormat
+{
+}
diff --git a/vendor/scssphp/scssphp/src/Value/ColorFormatEnum.php b/vendor/scssphp/scssphp/src/Value/ColorFormatEnum.php
new file mode 100644
index 000000000..4fe490e1a
--- /dev/null
+++ b/vendor/scssphp/scssphp/src/Value/ColorFormatEnum.php
@@ -0,0 +1,28 @@
+<?php
+
+/**
+ * SCSSPHP
+ *
+ * @copyright 2012-2020 Leaf Corcoran
+ *
+ * @license http://opensource.org/licenses/MIT MIT
+ *
+ * @link http://scssphp.github.io/scssphp
+ */
+
+namespace ScssPhp\ScssPhp\Value;
+
+/**
+ * @internal
+ */
+enum ColorFormatEnum implements ColorFormat
+{
+ /**
+ * A color defined using the `rgb()` or `rgba()` functions.
+ */
+ case rgbFunction;
+ /**
+ * A color defined using the `hsl()` or `hsla()` functions.
+ */
+ case hslFunction;
+}
diff --git a/vendor/scssphp/scssphp/src/Value/ComplexSassNumber.php b/vendor/scssphp/scssphp/src/Value/ComplexSassNumber.php
new file mode 100644
index 000000000..a0f50c738
--- /dev/null
+++ b/vendor/scssphp/scssphp/src/Value/ComplexSassNumber.php
@@ -0,0 +1,92 @@
+<?php
+
+/**
+ * SCSSPHP
+ *
+ * @copyright 2012-2020 Leaf Corcoran
+ *
+ * @license http://opensource.org/licenses/MIT MIT
+ *
+ * @link http://scssphp.github.io/scssphp
+ */
+
+namespace ScssPhp\ScssPhp\Value;
+
+/**
+ * A specialized subclass of {@see SassNumber} for numbers that are neither {@see UnitlessSassNumber} nor {@see SingleUnitSassNumber}.
+ *
+ * @internal
+ */
+final class ComplexSassNumber extends SassNumber
+{
+ /**
+ * @var list<string>
+ */
+ private readonly array $numeratorUnits;
+
+ /**
+ * @var list<string>
+ */
+ private readonly array $denominatorUnits;
+
+ /**
+ * @param list<string> $numeratorUnits
+ * @param list<string> $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 hasComplexUnits(): 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/vendor/scssphp/scssphp/src/Value/ListSeparator.php b/vendor/scssphp/scssphp/src/Value/ListSeparator.php
new file mode 100644
index 000000000..b2cd69900
--- /dev/null
+++ b/vendor/scssphp/scssphp/src/Value/ListSeparator.php
@@ -0,0 +1,34 @@
+<?php
+
+/**
+ * SCSSPHP
+ *
+ * @copyright 2012-2020 Leaf Corcoran
+ *
+ * @license http://opensource.org/licenses/MIT MIT
+ *
+ * @link http://scssphp.github.io/scssphp
+ */
+
+namespace ScssPhp\ScssPhp\Value;
+
+/**
+ * An enum of list separator types.
+ */
+enum ListSeparator
+{
+ case COMMA;
+ case SPACE;
+ case SLASH;
+ case UNDECIDED;
+
+ public function getSeparator(): ?string
+ {
+ return match ($this) {
+ self::COMMA => ',',
+ self::SPACE => ' ',
+ self::SLASH => '/',
+ self::UNDECIDED => null,
+ };
+ }
+}
diff --git a/vendor/scssphp/scssphp/src/Value/SassArgumentList.php b/vendor/scssphp/scssphp/src/Value/SassArgumentList.php
new file mode 100644
index 000000000..f4accc9d8
--- /dev/null
+++ b/vendor/scssphp/scssphp/src/Value/SassArgumentList.php
@@ -0,0 +1,60 @@
+<?php
+
+/**
+ * SCSSPHP
+ *
+ * @copyright 2012-2020 Leaf Corcoran
+ *
+ * @license http://opensource.org/licenses/MIT MIT
+ *
+ * @link http://scssphp.github.io/scssphp
+ */
+
+namespace ScssPhp\ScssPhp\Value;
+
+/**
+ * A SassScript argument list.
+ *
+ * An argument list comes from a rest argument. It's distinct from a normal
+ * {@see SassList} in that it may contain a keyword map as well as the positional
+ * arguments.
+ */
+final class SassArgumentList extends SassList
+{
+ /**
+ * @var array<string, Value>
+ */
+ private readonly array $keywords;
+
+ private bool $keywordAccessed = false;
+
+ /**
+ * SassArgumentList constructor.
+ *
+ * @param list<Value> $contents
+ * @param array<string, Value> $keywords
+ */
+ public function __construct(array $contents, array $keywords, ListSeparator $separator)
+ {
+ parent::__construct($contents, $separator);
+ $this->keywords = $keywords;
+ }
+
+ /**
+ * @return array<string, Value>
+ */
+ public function getKeywords(): array
+ {
+ $this->keywordAccessed = true;
+
+ return $this->keywords;
+ }
+
+ /**
+ * @internal
+ */
+ public function wereKeywordAccessed(): bool
+ {
+ return $this->keywordAccessed;
+ }
+}
diff --git a/vendor/scssphp/scssphp/src/Value/SassBoolean.php b/vendor/scssphp/scssphp/src/Value/SassBoolean.php
new file mode 100644
index 000000000..0120b1ecd
--- /dev/null
+++ b/vendor/scssphp/scssphp/src/Value/SassBoolean.php
@@ -0,0 +1,75 @@
+<?php
+
+/**
+ * SCSSPHP
+ *
+ * @copyright 2012-2020 Leaf Corcoran
+ *
+ * @license http://opensource.org/licenses/MIT MIT
+ *
+ * @link http://scssphp.github.io/scssphp
+ */
+
+namespace ScssPhp\ScssPhp\Value;
+
+use ScssPhp\ScssPhp\Visitor\ValueVisitor;
+
+/**
+ * A SassScript boolean value.
+ */
+final class SassBoolean extends Value
+{
+ private static SassBoolean $trueInstance;
+
+ private static SassBoolean $falseInstance;
+
+ private readonly bool $value;
+
+ public static function create(bool $value): SassBoolean
+ {
+ if ($value) {
+ return self::$trueInstance ??= new self(true);
+ }
+
+ return self::$falseInstance ??= new self(false);
+ }
+
+ private function __construct(bool $value)
+ {
+ $this->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/vendor/scssphp/scssphp/src/Value/SassCalculation.php b/vendor/scssphp/scssphp/src/Value/SassCalculation.php
new file mode 100644
index 000000000..f6508361c
--- /dev/null
+++ b/vendor/scssphp/scssphp/src/Value/SassCalculation.php
@@ -0,0 +1,1153 @@
+<?php
+
+/**
+ * SCSSPHP
+ *
+ * @copyright 2012-2020 Leaf Corcoran
+ *
+ * @license http://opensource.org/licenses/MIT MIT
+ *
+ * @link http://scssphp.github.io/scssphp
+ */
+
+namespace ScssPhp\ScssPhp\Value;
+
+use ScssPhp\ScssPhp\Deprecation;
+use ScssPhp\ScssPhp\Exception\SassScriptException;
+use ScssPhp\ScssPhp\Util\Character;
+use ScssPhp\ScssPhp\Util\Equatable;
+use ScssPhp\ScssPhp\Util\NumberUtil;
+use ScssPhp\ScssPhp\Util\StringUtil;
+use ScssPhp\ScssPhp\Visitor\ValueVisitor;
+use ScssPhp\ScssPhp\Warn;
+
+/**
+ * A SassScript calculation.
+ *
+ * Although calculations can in principle have any name or any number of
+ * arguments, this class only exposes the specific calculations that are
+ * supported by the Sass spec. This ensures that all calculations that the user
+ * works with are always fully simplified.
+ */
+final class SassCalculation extends Value
+{
+ /**
+ * The calculation's name, such as `"calc"`.
+ */
+ private readonly string $name;
+
+ /**
+ * The calculation's arguments.
+ *
+ * Each argument is either a {@see SassNumber}, a {@see SassCalculation}, an unquoted
+ * {@see SassString}, or a {@see CalculationOperation}.
+ *
+ * @var list<object>
+ */
+ private readonly array $arguments;
+
+ /**
+ * Creates a new calculation with the given $name and $arguments
+ * that will not be simplified.
+ *
+ * @param list<object> $arguments
+ *
+ * @internal
+ */
+ public static function unsimplified(string $name, array $arguments): SassCalculation
+ {
+ 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}, or a {@see CalculationOperation}.
+ *
+ * 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.
+ *
+ * @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}, or a {@see CalculationOperation}. 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<object> $arguments
+ *
+ * @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}, or a {@see CalculationOperation}. 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<object> $arguments
+ *
+ * @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 `hypot()` calculation with the given $arguments.
+ *
+ * Each argument must be either a {@see SassNumber}, a {@see SassCalculation}, an
+ * unquoted {@see SassString}, or a {@see CalculationOperation}. 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<object> $arguments
+ */
+ public static function hypot(array $arguments): Value
+ {
+ $args = self::simplifyArguments($arguments);
+
+ if (!$args) {
+ throw new \InvalidArgumentException('hypot() must have at least one argument.');
+ }
+
+ self::verifyCompatibleNumbers($args);
+
+ $subTotal = 0.0;
+ $first = $args[0];
+
+ if (!$first instanceof SassNumber || $first->hasUnit('%')) {
+ return new SassCalculation('hypot', $args);
+ }
+
+ foreach ($args as $i => $number) {
+ if (!$number instanceof SassNumber || !$number->hasCompatibleUnits($first)) {
+ return new SassCalculation('hypot', $args);
+ }
+
+ $sassIndex = $i + 1;
+ $value = $number->convertValueToMatch($first, "number[$sassIndex]", 'numbers[1]');
+ $subTotal += $value * $value;
+ }
+
+ return SassNumber::withUnits(sqrt($subTotal), $first->getNumeratorUnits(), $first->getDenominatorUnits());
+ }
+
+ /**
+ * Creates a `sqrt()` calculation with the given $argument.
+ *
+ * The $argument must be either a {@see SassNumber}, a {@see SassCalculation}, an
+ * unquoted {@see SassString}, or a {@see CalculationOperation}.
+ *
+ * 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.
+ */
+ public static function sqrt(object $argument): Value
+ {
+ return self::singleArgument('sqrt', $argument, NumberUtil::class . '::sqrt', true);
+ }
+
+ /**
+ * Creates a `sin()` calculation with the given $argument.
+ *
+ * The $argument must be either a {@see SassNumber}, a {@see SassCalculation}, an
+ * unquoted {@see SassString}, or a {@see CalculationOperation}.
+ *
+ * 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.
+ */
+ public static function sin(object $argument): Value
+ {
+ return self::singleArgument('sin', $argument, NumberUtil::class . '::sin');
+ }
+
+ /**
+ * Creates a `cos()` calculation with the given $argument.
+ *
+ * The $argument must be either a {@see SassNumber}, a {@see SassCalculation}, an
+ * unquoted {@see SassString}, or a {@see CalculationOperation}.
+ *
+ * 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.
+ */
+ public static function cos(object $argument): Value
+ {
+ return self::singleArgument('cos', $argument, NumberUtil::class . '::cos');
+ }
+
+ /**
+ * Creates a `tan()` calculation with the given $argument.
+ *
+ * The $argument must be either a {@see SassNumber}, a {@see SassCalculation}, an
+ * unquoted {@see SassString}, or a {@see CalculationOperation}.
+ *
+ * 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.
+ */
+ public static function tan(object $argument): Value
+ {
+ return self::singleArgument('tan', $argument, NumberUtil::class . '::tan');
+ }
+
+ /**
+ * Creates an `atan()` calculation with the given $argument.
+ *
+ * The $argument must be either a {@see SassNumber}, a {@see SassCalculation}, an
+ * unquoted {@see SassString}, or a {@see CalculationOperation}.
+ *
+ * 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.
+ */
+ public static function atan(object $argument): Value
+ {
+ return self::singleArgument('atan', $argument, NumberUtil::class . '::atan', true);
+ }
+
+ /**
+ * Creates an `asin()` calculation with the given $argument.
+ *
+ * The $argument must be either a {@see SassNumber}, a {@see SassCalculation}, an
+ * unquoted {@see SassString}, or a {@see CalculationOperation}.
+ *
+ * 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.
+ */
+ public static function asin(object $argument): Value
+ {
+ return self::singleArgument('asin', $argument, NumberUtil::class . '::asin', true);
+ }
+
+ /**
+ * Creates an `acos()` calculation with the given $argument.
+ *
+ * The $argument must be either a {@see SassNumber}, a {@see SassCalculation}, an
+ * unquoted {@see SassString}, or a {@see CalculationOperation}.
+ *
+ * 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.
+ */
+ public static function acos(object $argument): Value
+ {
+ return self::singleArgument('acos', $argument, NumberUtil::class . '::acos', true);
+ }
+
+ /**
+ * Creates an `abs()` calculation with the given $argument.
+ *
+ * The $argument must be either a {@see SassNumber}, a {@see SassCalculation}, an
+ * unquoted {@see SassString}, or a {@see CalculationOperation}.
+ *
+ * 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.
+ */
+ public static function abs(object $argument): Value
+ {
+ $argument = self::simplify($argument);
+
+ if (!$argument instanceof SassNumber) {
+ return new SassCalculation('abs', [$argument]);
+ }
+
+ if ($argument->hasUnit('%')) {
+ $message = <<<WARNING
+Passing percentage units to the global abs() function is deprecated.
+In the future, this will emit a CSS abs() function to be resolved by the browser.
+To preserve current behavior: math.abs($argument)
+
+To emit a CSS abs() now: abs(#{{$argument}})
+More info: https://sass-lang.com/d/abs-percent
+WARNING;
+
+ Warn::forDeprecation($message, Deprecation::absPercent);
+ }
+
+ return NumberUtil::abs($argument);
+ }
+
+ /**
+ * Creates an `exp()` calculation with the given $argument.
+ *
+ * The $argument must be either a {@see SassNumber}, a {@see SassCalculation}, an
+ * unquoted {@see SassString}, or a {@see CalculationOperation}.
+ *
+ * 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.
+ */
+ public static function exp(object $argument): Value
+ {
+ $argument = self::simplify($argument);
+
+ if (!$argument instanceof SassNumber) {
+ return new SassCalculation('exp', [$argument]);
+ }
+
+ $argument->assertNoUnits();
+
+ return NumberUtil::pow(SassNumber::create(M_E), $argument);
+ }
+
+ /**
+ * Creates a `sign()` calculation with the given $argument.
+ *
+ * The $argument must be either a {@see SassNumber}, a {@see SassCalculation}, an
+ * unquoted {@see SassString}, or a {@see CalculationOperation}.
+ *
+ * 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.
+ */
+ public static function sign(object $argument): Value
+ {
+ $argument = self::simplify($argument);
+
+ if (!$argument instanceof SassNumber) {
+ return new SassCalculation('sign', [$argument]);
+ }
+
+ if (!$argument->hasUnits() && (is_nan($argument->getValue()) || $argument->getValue() === 0.0)) {
+ return $argument;
+ }
+
+ if (!$argument->hasUnit('%')) {
+ return SassNumber::create(NumberUtil::sign($argument->getValue()))->coerceToMatch($argument);
+ }
+
+ return new SassCalculation('sign', [$argument]);
+ }
+
+ /**
+ * 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}, or a {@see CalculationOperation}.
+ *
+ * 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.
+ *
+ * @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_values(array_filter([$min, $value, $max]));
+ self::verifyCompatibleNumbers($args);
+ self::verifyLength($args, 3);
+
+ return new SassCalculation('clamp', $args);
+ }
+
+ /**
+ * Creates a `pow()` calculation with the given $base and $exponent.
+ *
+ * Each argument must be either a {@see SassNumber}, a {@see SassCalculation}, an
+ * unquoted {@see SassString}, or a {@see CalculationOperation}.
+ *
+ * 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 two arguments, but only if one of the
+ * arguments is an unquoted `var()` string.
+ */
+ public static function pow(object $base, ?object $exponent): Value
+ {
+ $args = [$base];
+ if ($exponent !== null) {
+ $args[] = $exponent;
+ }
+ self::verifyLength($args, 2);
+ $base = self::simplify($base);
+ if ($exponent !== null) {
+ $exponent = self::simplify($exponent);
+ }
+
+ if (!$base instanceof SassNumber || !$exponent instanceof SassNumber) {
+ return new SassCalculation('pow', $args);
+ }
+
+ $base->assertNoUnits();
+ $exponent->assertNoUnits();
+
+ return NumberUtil::pow($base, $exponent);
+ }
+
+ /**
+ * Creates a `log()` calculation with the given $number and $base.
+ *
+ * Each argument must be either a {@see SassNumber}, a {@see SassCalculation}, an
+ * unquoted {@see SassString}, or a {@see CalculationOperation}.
+ *
+ * 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.
+ *
+ * If arguments contains exactly a single argument, the base is set to
+ * `math.e` by default.
+ */
+ public static function log(object $number, ?object $base): Value
+ {
+ $number = self::simplify($number);
+ $args = [$number];
+ if ($base !== null) {
+ $base = self::simplify($base);
+ $args[] = $base;
+ }
+
+ if (!$number instanceof SassNumber || ($base !== null && !$base instanceof SassNumber)) {
+ return new SassCalculation('log', $args);
+ }
+
+ $number->assertNoUnits();
+
+ if ($base instanceof SassNumber) {
+ $base->assertNoUnits();
+
+ return NumberUtil::log($number, $base);
+ }
+
+ return NumberUtil::log($number, null);
+ }
+
+ /**
+ * Creates a `atan2()` calculation for $y and $x.
+ *
+ * Each argument must be either a {@see SassNumber}, a {@see SassCalculation}, an
+ * unquoted {@see SassString}, or a {@see CalculationOperation}.
+ *
+ * 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 two arguments, but only if one of the
+ * arguments is an unquoted `var()` string.
+ */
+ public static function atan2(object $y, ?object $x): Value
+ {
+ $y = self::simplify($y);
+ $args = [$y];
+
+ if ($x !== null) {
+ $x = self::simplify($x);
+ $args[] = $x;
+ }
+ self::verifyLength($args, 2);
+ self::verifyCompatibleNumbers($args);
+
+ if (!$y instanceof SassNumber || !$x instanceof SassNumber || $y->hasUnit('%') || $x->hasUnit('%') || !$y->hasCompatibleUnits($x)) {
+ return new SassCalculation('atan2', $args);
+ }
+
+ return NumberUtil::atan2($y, $x);
+ }
+
+ /**
+ * Creates a `rem()` calculation with the given $dividend and $modulus.
+ *
+ * Each argument must be either a {@see SassNumber}, a {@see SassCalculation}, an
+ * unquoted {@see SassString}, or a {@see CalculationOperation}.
+ *
+ * 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 two arguments, but only if one of the
+ * arguments is an unquoted `var()` string.
+ */
+ public static function rem(object $dividend, ?object $modulus): Value
+ {
+ $dividend = self::simplify($dividend);
+ $args = [$dividend];
+
+ if ($modulus !== null) {
+ $modulus = self::simplify($modulus);
+ $args[] = $modulus;
+ }
+ self::verifyLength($args, 2);
+ self::verifyCompatibleNumbers($args);
+
+ if (!$dividend instanceof SassNumber || !$modulus instanceof SassNumber || !$dividend->hasCompatibleUnits($modulus)) {
+ return new SassCalculation('rem', $args);
+ }
+
+ $result = $dividend->modulo($modulus);
+
+ if (NumberUtil::signIncludingZero($modulus->getValue()) !== NumberUtil::signIncludingZero($dividend->getValue())) {
+ if (is_infinite($modulus->getValue())) {
+ return $dividend;
+ }
+
+ if ($result->getValue() === 0.0) {
+ return $result->unaryMinus();
+ }
+
+ return $result->minus($modulus);
+ }
+
+ return $result;
+ }
+
+ /**
+ * Creates a `mod()` calculation with the given $dividend and $modulus.
+ *
+ * Each argument must be either a {@see SassNumber}, a {@see SassCalculation}, an
+ * unquoted {@see SassString}, or a {@see CalculationOperation}.
+ *
+ * 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 two arguments, but only if one of the
+ * arguments is an unquoted `var()` string.
+ */
+ public static function mod(object $dividend, ?object $modulus): Value
+ {
+ $dividend = self::simplify($dividend);
+ $args = [$dividend];
+
+ if ($modulus !== null) {
+ $modulus = self::simplify($modulus);
+ $args[] = $modulus;
+ }
+ self::verifyLength($args, 2);
+ self::verifyCompatibleNumbers($args);
+
+ if (!$dividend instanceof SassNumber || !$modulus instanceof SassNumber || !$dividend->hasCompatibleUnits($modulus)) {
+ return new SassCalculation('mod', $args);
+ }
+
+ return $dividend->modulo($modulus);
+ }
+
+ /**
+ * Creates a `round()` calculation with the given $strategyOrNumber,
+ * $numberOrStep, and $step. Strategy must be either nearest, up, down or
+ * to-zero.
+ *
+ * Number and step must be either a {@see SassNumber}, a {@see SassCalculation}, an
+ * unquoted {@see SassString}, or a {@see CalculationOperation}.
+ *
+ * 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 two arguments, but only if one of the
+ * arguments is an unquoted `var()` string.
+ */
+ public static function round(object $strategyOrNumber, ?object $numberOrStep = null, ?object $step = null): Value
+ {
+ $strategyOrNumber = self::simplify($strategyOrNumber);
+ if ($numberOrStep !== null) {
+ $numberOrStep = self::simplify($numberOrStep);
+ }
+ if ($step !== null) {
+ $step = self::simplify($step);
+ }
+
+ switch (true) {
+ case $strategyOrNumber instanceof SassNumber && $numberOrStep === null && $step === null:
+ return self::matchUnits(round($strategyOrNumber->getValue()), $strategyOrNumber);
+
+ case $strategyOrNumber instanceof SassNumber && $numberOrStep instanceof SassNumber && $step === null:
+ self::verifyCompatibleNumbers([$strategyOrNumber, $numberOrStep]);
+
+ if (!$strategyOrNumber->hasCompatibleUnits($numberOrStep)) {
+ return new SassCalculation('round', [$strategyOrNumber, $numberOrStep]);
+ }
+
+ return self::roundWithStep('nearest', $strategyOrNumber, $numberOrStep);
+
+ case $strategyOrNumber instanceof SassString && \in_array($strategyOrNumber->getText(), ['nearest', 'up', 'down', 'to-zero'], true) && $numberOrStep instanceof SassNumber && $step instanceof SassNumber:
+ self::verifyCompatibleNumbers([$numberOrStep, $step]);
+
+ if (!$numberOrStep->hasCompatibleUnits($step)) {
+ return new SassCalculation('round', [$strategyOrNumber, $numberOrStep, $step]);
+ }
+
+ return self::roundWithStep($strategyOrNumber->getText(), $numberOrStep, $step);
+
+ case $strategyOrNumber instanceof SassString && \in_array($strategyOrNumber->getText(), ['nearest', 'up', 'down', 'to-zero'], true) && $numberOrStep instanceof SassString && $step === null:
+ return new SassCalculation('round', [$strategyOrNumber, $numberOrStep]);
+
+ case $strategyOrNumber instanceof SassString && \in_array($strategyOrNumber->getText(), ['nearest', 'up', 'down', 'to-zero'], true) && $numberOrStep !== null && $step === null:
+ throw new SassScriptException('If strategy is not null, step is required.');
+
+ case $strategyOrNumber instanceof SassString && \in_array($strategyOrNumber->getText(), ['nearest', 'up', 'down', 'to-zero'], true) && $numberOrStep === null && $step === null:
+ throw new SassScriptException('Number to round and step arguments are required.');
+
+ case $strategyOrNumber instanceof SassString && $numberOrStep === null && $step === null:
+ return new SassCalculation('round', [$strategyOrNumber]);
+
+ case $numberOrStep === null && $step === null:
+ throw new SassScriptException("Single argument $strategyOrNumber expected to be simplifiable.");
+
+ case $step === null:
+ return new SassCalculation('round', [$strategyOrNumber, $numberOrStep]);
+
+ case $strategyOrNumber instanceof SassString && (\in_array($strategyOrNumber->getText(), ['nearest', 'up', 'down', 'to-zero'], true) || $strategyOrNumber->isVar()) && $numberOrStep !== null:
+ return new SassCalculation('round', [$strategyOrNumber, $numberOrStep, $step]);
+
+ case $numberOrStep !== null:
+ throw new SassScriptException("$strategyOrNumber must be either nearest, up, down or to-zero.");
+
+ default:
+ throw new SassScriptException('Invalid parameters.');
+ }
+ }
+
+ /**
+ * 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}, or a {@see CalculationOperation}.
+ *
+ * @throws SassScriptException
+ */
+ public static function operate(CalculationOperator $operator, object $left, object $right): object
+ {
+ return self::operateInternal($operator, $left, $right, false, true);
+ }
+
+ /**
+ * Like {@see operate}, but with the internal-only $inLegacySassFunction parameter.
+ *
+ * If $inLegacySassFunction 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.
+ *
+ * @return SassNumber|CalculationOperation|SassString|SassCalculation|Value
+ *
+ * @throws SassScriptException
+ *
+ * @internal
+ */
+ public static function operateInternal(CalculationOperator $operator, object $left, object $right, bool $inLegacySassFunction, 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 && ($inLegacySassFunction ? $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 list<object> $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<object>
+ */
+ 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;
+ }
+
+ /**
+ * Returns $value coerced to $number's units.
+ */
+ private static function matchUnits(float $value, SassNumber $number): SassNumber
+ {
+ return SassNumber::withUnits($value, $number->getNumeratorUnits(), $number->getDenominatorUnits());
+ }
+
+ /**
+ * Returns a rounded $number based on a selected rounding $strategy,
+ * to the nearest integer multiple of $step.
+ */
+ private static function roundWithStep(string $strategy, SassNumber $number, SassNumber $step): SassNumber
+ {
+ if (!\in_array($strategy, ['nearest', 'up', 'down', 'to-zero'], true)) {
+ throw new \InvalidArgumentException('$strategy must be either nearest, up, down or to-zero.');
+ }
+
+ if (is_infinite($number->getValue()) && is_infinite($step->getValue()) || $step->getValue() === 0.0 || is_nan($number->getValue()) || is_nan($step->getValue())) {
+ return self::matchUnits(NAN, $number);
+ }
+
+ if (is_infinite($number->getValue())) {
+ return $number;
+ }
+
+ if (is_infinite($step->getValue())) {
+ if ($number->getValue() === 0.0) {
+ return $number;
+ }
+
+ switch ($strategy) {
+ case 'nearest':
+ case 'to-zero':
+ if ($number->getValue() > 0) {
+ return self::matchUnits(0.0, $number);
+ }
+
+ return self::matchUnits(-0.0, $number);
+
+ case 'up':
+ if ($number->getValue() > 0) {
+ return self::matchUnits(INF, $number);
+ }
+
+ return self::matchUnits(-0.0, $number);
+
+ case 'down':
+ if ($number->getValue() < 0) {
+ return self::matchUnits(-INF, $number);
+ }
+
+ return self::matchUnits(0.0, $number);
+ }
+ }
+
+ $stepWithNumberUnit = $step->convertValueToMatch($number);
+
+ switch ($strategy) {
+ case 'nearest':
+ return self::matchUnits(round($number->getValue() / $stepWithNumberUnit) * $stepWithNumberUnit, $number);
+ case 'up':
+ return self::matchUnits(($step->getValue() < 0 ? floor($number->getValue() / $stepWithNumberUnit) : ceil($number->getValue() / $stepWithNumberUnit)) * $stepWithNumberUnit, $number);
+ case 'down':
+ return self::matchUnits(($step->getValue() < 0 ? ceil($number->getValue() / $stepWithNumberUnit) : floor($number->getValue() / $stepWithNumberUnit)) * $stepWithNumberUnit, $number);
+ case 'to-zero':
+ if ($number->getValue() < 0) {
+ return self::matchUnits(ceil($number->getValue() / $stepWithNumberUnit) * $stepWithNumberUnit, $number);
+ }
+
+ return self::matchUnits(floor($number->getValue() / $stepWithNumberUnit) * $stepWithNumberUnit, $number);
+
+ default:
+ return self::matchUnits(NAN, $number);
+ }
+ }
+
+ /**
+ * @param list<object> $args
+ *
+ * @return list<object>
+ *
+ * @throws SassScriptException
+ */
+ private static function simplifyArguments(array $args): array
+ {
+ return array_map([self::class, 'simplify'], $args);
+ }
+
+ /**
+ * @return SassNumber|CalculationOperation|SassString|SassCalculation
+ *
+ * @throws SassScriptException
+ */
+ private static function simplify(object $arg): object
+ {
+ if ($arg instanceof SassNumber || $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) {
+ if ($arg->getName() === 'calc') {
+ $argument = $arg->getArguments()[0];
+
+ if ($argument instanceof SassString && !$argument->hasQuotes() && self::needsParentheses($argument->getText())) {
+ return new SassString("({$argument->getText()})", false);
+ }
+
+ \assert($argument instanceof SassNumber || $argument instanceof SassString || $argument instanceof SassCalculation || $argument instanceof CalculationOperation);
+
+ return $argument;
+ }
+
+ return $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_debug_type($arg)));
+ }
+
+ /**
+ * Returns whether $text needs parentheses if it's the contents of a
+ * `calc()` being embedded in another calculation.
+ */
+ private static function needsParentheses(string $text): bool
+ {
+ $first = $text[0];
+ if (self::charNeedsParentheses($first)) {
+ return true;
+ }
+
+ $couldBeVar = \strlen($text) > 4 && ($first === 'v' || $first === 'V');
+
+ if (\strlen($text) < 2) {
+ return false;
+ }
+ $second = $text[1];
+ if (self::charNeedsParentheses($second)) {
+ return true;
+ }
+ $couldBeVar = $couldBeVar && ($second === 'a' || $second === 'A');
+
+ if (\strlen($text) < 3) {
+ return false;
+ }
+ $third = $text[2];
+ if (self::charNeedsParentheses($third)) {
+ return true;
+ }
+ $couldBeVar = $couldBeVar && ($third === 'r' || $third === 'R');
+
+ if (\strlen($text) < 4) {
+ return false;
+ }
+ $fourth = $text[3];
+ if ($couldBeVar && $fourth === '(') {
+ return true;
+ }
+ if (self::charNeedsParentheses($fourth)) {
+ return true;
+ }
+
+ for ($i = 4; $i < \strlen($text); ++$i) {
+ if (self::charNeedsParentheses($text[$i])) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Returns whether $character intrinsically needs parentheses if it appears
+ * in the unquoted string argument of a `calc()` being embedded in another
+ * calculation.
+ */
+ private static function charNeedsParentheses(string $character): bool
+ {
+ return $character === '/' || $character === '*' || Character::isWhitespace($character);
+ }
+
+ /**
+ * 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<object> $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}.
+ *
+ * @param list<object> $args
+ *
+ * @throws SassScriptException
+ */
+ private static function verifyLength(array $args, int $expectedLength): void
+ {
+ if (\count($args) === $expectedLength) {
+ return;
+ }
+
+ foreach ($args as $arg) {
+ if ($arg instanceof SassString) {
+ return;
+ }
+ }
+
+ $length = \count($args);
+ $verb = StringUtil::pluralize('was', $length, 'were');
+
+ throw new SassScriptException("$expectedLength arguments required, but only $length $verb passed.");
+ }
+
+ /**
+ * @param callable(SassNumber): SassNumber $mathFunc
+ *
+ * @param-immediately-invoked-callable $mathFunc
+ */
+ private static function singleArgument(string $name, object $argument, callable $mathFunc, bool $forbidUnits = false): Value
+ {
+ $argument = self::simplify($argument);
+
+ if (!$argument instanceof SassNumber) {
+ return new SassCalculation($name, [$argument]);
+ }
+
+ if ($forbidUnits) {
+ $argument->assertNoUnits();
+ }
+
+ return $mathFunc($argument);
+ }
+}
diff --git a/vendor/scssphp/scssphp/src/Value/SassColor.php b/vendor/scssphp/scssphp/src/Value/SassColor.php
new file mode 100644
index 000000000..161ebd8b9
--- /dev/null
+++ b/vendor/scssphp/scssphp/src/Value/SassColor.php
@@ -0,0 +1,407 @@
+<?php
+
+/**
+ * SCSSPHP
+ *
+ * @copyright 2012-2020 Leaf Corcoran
+ *
+ * @license http://opensource.org/licenses/MIT MIT
+ *
+ * @link http://scssphp.github.io/scssphp
+ */
+
+namespace ScssPhp\ScssPhp\Value;
+
+use ScssPhp\ScssPhp\Exception\SassScriptException;
+use ScssPhp\ScssPhp\Util\ErrorUtil;
+use ScssPhp\ScssPhp\Util\NumberUtil;
+use ScssPhp\ScssPhp\Visitor\ValueVisitor;
+
+/**
+ * A SassScript color.
+ */
+final class SassColor extends Value
+{
+ /**
+ * This color's red channel, between `0` and `255`.
+ */
+ private ?int $red;
+
+ /**
+ * This color's blue channel, between `0` and `255`.
+ */
+ private ?int $blue;
+
+ /**
+ * This color's green channel, between `0` and `255`.
+ */
+ private ?int $green;
+
+ /**
+ * This color's hue, between `0` and `360`.
+ */
+ private ?float $hue;
+
+ /**
+ * This color's saturation, a percentage between `0` and `100`.
+ */
+ private ?float $saturation;
+
+ /**
+ * This color's lightness, a percentage between `0` and `100`.
+ */
+ private ?float $lightness;
+
+ /**
+ * This color's alpha channel, between `0` and `1`.
+ */
+ private readonly float $alpha;
+
+ private readonly ?ColorFormat $format;
+
+ /**
+ * Creates a RGB color
+ *
+ * @throws \OutOfRangeException if values are outside the expected range.
+ */
+ public static function rgb(int $red, int $green, int $blue, float $alpha = 1.0): SassColor
+ {
+ return self::rgbInternal($red, $green, $blue, $alpha);
+ }
+
+ /**
+ * Like {@see rgb} but also takes a color format.
+ *
+ * @internal
+ *
+ * @throws \OutOfRangeException if values are outside the expected range.
+ */
+ public static function rgbInternal(int $red, int $green, int $blue, float $alpha = 1.0, ?ColorFormat $format = null): SassColor
+ {
+ $alpha = NumberUtil::fuzzyAssertRange($alpha, 0, 1, 'alpha');
+
+ ErrorUtil::checkIntInInterval($red, 0, 255, 'red');
+ ErrorUtil::checkIntInInterval($green, 0, 255, 'green');
+ ErrorUtil::checkIntInInterval($blue, 0, 255, 'blue');
+
+ return new self($red, $green, $blue, null, null, null, $alpha, $format);
+ }
+
+ /**
+ * @throws \OutOfRangeException if values are outside the expected range.
+ */
+ public static function hsl(float $hue, float $saturation, float $lightness, float $alpha = 1.0): SassColor
+ {
+ return self::hslInternal($hue, $saturation, $lightness, $alpha);
+ }
+
+ /**
+ * Like {@see hsl} but also takes a color format.
+ *
+ * @internal
+ *
+ * @throws \OutOfRangeException if values are outside the expected range.
+ */
+ public static function hslInternal(float $hue, float $saturation, float $lightness, float $alpha = 1.0, ?ColorFormat $format = null): SassColor
+ {
+ $alpha = NumberUtil::fuzzyAssertRange($alpha, 0, 1, 'alpha');
+
+ $hue = fmod($hue, 360);
+ if ($hue < 0) {
+ $hue += 360;
+ }
+ $saturation = NumberUtil::fuzzyAssertRange($saturation, 0, 100, 'saturation');
+ $lightness = NumberUtil::fuzzyAssertRange($lightness, 0, 100, 'lightness');
+
+ return new self(null, null, null, $hue, $saturation, $lightness, $alpha, $format);
+ }
+
+ public static function hwb(float $hue, float $whiteness, float $blackness, float $alpha = 1.0): SassColor
+ {
+ $hue = fmod($hue, 360);
+ if ($hue < 0) {
+ $hue += 360;
+ }
+ $scaledHue = $hue / 360;
+ $scaledWhiteness = NumberUtil::fuzzyAssertRange($whiteness, 0, 100, 'whiteness') / 100;
+ $scaledBlackness = NumberUtil::fuzzyAssertRange($blackness, 0, 100, 'blackness') / 100;
+
+ $sum = $scaledWhiteness + $scaledBlackness;
+
+ if ($sum > 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.
+ */
+ private function __construct(?int $red, ?int $green, ?int $blue, ?float $hue, ?float $saturation, ?float $lightness, float $alpha, ?ColorFormat $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
+ */
+ public function getFormat(): ?ColorFormat
+ {
+ return $this->format;
+ }
+
+ public function accept(ValueVisitor $visitor)
+ {
+ return $visitor->visitColor($this);
+ }
+
+ public function assertColor(?string $name = null): SassColor
+ {
+ return $this;
+ }
+
+ 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);
+ }
+
+ 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);
+ }
+
+ 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);
+ }
+
+ 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;
+ }
+
+ 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);
+ }
+
+ if ($this->hue < 0) {
+ $this->hue += 360;
+ }
+
+ $this->lightness = 50 * ($max + $min);
+
+ if ($max == $min) {
+ $this->saturation = 0;
+ } elseif ($this->lightness < 50) {
+ $this->saturation = 100 * $delta / ($max + $min);
+ } else {
+ $this->saturation = 100 * $delta / (2 - $max - $min);
+ }
+ }
+
+ 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/vendor/scssphp/scssphp/src/Value/SassFunction.php b/vendor/scssphp/scssphp/src/Value/SassFunction.php
new file mode 100644
index 000000000..54f945b38
--- /dev/null
+++ b/vendor/scssphp/scssphp/src/Value/SassFunction.php
@@ -0,0 +1,59 @@
+<?php
+
+/**
+ * SCSSPHP
+ *
+ * @copyright 2012-2020 Leaf Corcoran
+ *
+ * @license http://opensource.org/licenses/MIT MIT
+ *
+ * @link http://scssphp.github.io/scssphp
+ */
+
+namespace ScssPhp\ScssPhp\Value;
+
+use ScssPhp\ScssPhp\SassCallable\SassCallable;
+use ScssPhp\ScssPhp\Util\EquatableUtil;
+use ScssPhp\ScssPhp\Visitor\ValueVisitor;
+
+/**
+ * A SassScript function reference.
+ *
+ * A function reference captures a function from the local environment so that
+ * it may be passed between modules.
+ */
+final class SassFunction extends Value
+{
+ private readonly SassCallable $callable;
+
+ /**
+ * @internal
+ */
+ public function __construct(SassCallable $callable)
+ {
+ $this->callable = $callable;
+ }
+
+ /**
+ * @internal
+ */
+ public function getCallable(): SassCallable
+ {
+ return $this->callable;
+ }
+
+ 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 && EquatableUtil::equals($this->callable, $other->callable);
+ }
+}
diff --git a/vendor/scssphp/scssphp/src/Value/SassList.php b/vendor/scssphp/scssphp/src/Value/SassList.php
new file mode 100644
index 000000000..249acbd0a
--- /dev/null
+++ b/vendor/scssphp/scssphp/src/Value/SassList.php
@@ -0,0 +1,139 @@
+<?php
+
+/**
+ * SCSSPHP
+ *
+ * @copyright 2012-2020 Leaf Corcoran
+ *
+ * @license http://opensource.org/licenses/MIT MIT
+ *
+ * @link http://scssphp.github.io/scssphp
+ */
+
+namespace ScssPhp\ScssPhp\Value;
+
+use JiriPudil\SealedClasses\Sealed;
+use ScssPhp\ScssPhp\Visitor\ValueVisitor;
+
+/**
+ * A SassScript list.
+ */
+#[Sealed(permits: [SassArgumentList::class])]
+class SassList extends Value
+{
+ /**
+ * @var list<Value>
+ */
+ private readonly array $contents;
+
+ private readonly ListSeparator $separator;
+
+ private readonly bool $brackets;
+
+ public static function createEmpty(ListSeparator $separator = ListSeparator::UNDECIDED, bool $brackets = false): SassList
+ {
+ return new self(array(), $separator, $brackets);
+ }
+
+ /**
+ * @param list<Value> $contents
+ */
+ public function __construct(array $contents, ListSeparator $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(): ListSeparator
+ {
+ 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/vendor/scssphp/scssphp/src/Value/SassMap.php b/vendor/scssphp/scssphp/src/Value/SassMap.php
new file mode 100644
index 000000000..f4ef90c5e
--- /dev/null
+++ b/vendor/scssphp/scssphp/src/Value/SassMap.php
@@ -0,0 +1,127 @@
+<?php
+
+/**
+ * SCSSPHP
+ *
+ * @copyright 2012-2020 Leaf Corcoran
+ *
+ * @license http://opensource.org/licenses/MIT MIT
+ *
+ * @link http://scssphp.github.io/scssphp
+ */
+
+namespace ScssPhp\ScssPhp\Value;
+
+use ScssPhp\ScssPhp\Collection\Map;
+use ScssPhp\ScssPhp\Visitor\ValueVisitor;
+
+/**
+ * A SassScript map.
+ */
+final class SassMap extends Value
+{
+ /**
+ * @var Map<Value>
+ */
+ private readonly Map $contents;
+
+ /**
+ * @param Map<Value> $contents
+ */
+ private function __construct(Map $contents)
+ {
+ $this->contents = Map::unmodifiable($contents);
+ }
+
+ public static function createEmpty(): SassMap
+ {
+ return new self(new Map());
+ }
+
+ /**
+ * @param Map<Value> $contents
+ */
+ public static function create(Map $contents): SassMap
+ {
+ return new self($contents);
+ }
+
+ /**
+ * The returned Map is unmodifiable.
+ *
+ * @return Map<Value>
+ */
+ public function getContents(): Map
+ {
+ return $this->contents;
+ }
+
+ public function getSeparator(): ListSeparator
+ {
+ 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/vendor/scssphp/scssphp/src/Value/SassMixin.php b/vendor/scssphp/scssphp/src/Value/SassMixin.php
new file mode 100644
index 000000000..394111cac
--- /dev/null
+++ b/vendor/scssphp/scssphp/src/Value/SassMixin.php
@@ -0,0 +1,62 @@
+<?php
+
+/**
+ * SCSSPHP
+ *
+ * @copyright 2012-2020 Leaf Corcoran
+ *
+ * @license http://opensource.org/licenses/MIT MIT
+ *
+ * @link http://scssphp.github.io/scssphp
+ */
+
+namespace ScssPhp\ScssPhp\Value;
+
+use ScssPhp\ScssPhp\SassCallable\SassCallable;
+use ScssPhp\ScssPhp\Util\EquatableUtil;
+use ScssPhp\ScssPhp\Visitor\ValueVisitor;
+
+/**
+ * A SassScript mixin reference.
+ *
+ * A mixin reference captures a mixin from the local environment so that
+ * it may be passed between modules.
+ */
+final class SassMixin extends Value
+{
+ private readonly SassCallable $callable;
+
+ /**
+ * @internal
+ */
+ public function __construct(SassCallable $callable)
+ {
+ $this->callable = $callable;
+ }
+
+ /**
+ * @internal
+ */
+ public function getCallable(): SassCallable
+ {
+ return $this->callable;
+ }
+
+ /**
+ * @internal
+ */
+ public function accept(ValueVisitor $visitor)
+ {
+ return $visitor->visitMixin($this);
+ }
+
+ public function assertMixin(?string $name = null): SassMixin
+ {
+ return $this;
+ }
+
+ public function equals(object $other): bool
+ {
+ return $other instanceof SassMixin && EquatableUtil::equals($this->callable, $other->callable);
+ }
+}
diff --git a/vendor/scssphp/scssphp/src/Value/SassNull.php b/vendor/scssphp/scssphp/src/Value/SassNull.php
new file mode 100644
index 000000000..a1b447613
--- /dev/null
+++ b/vendor/scssphp/scssphp/src/Value/SassNull.php
@@ -0,0 +1,62 @@
+<?php
+
+/**
+ * SCSSPHP
+ *
+ * @copyright 2012-2020 Leaf Corcoran
+ *
+ * @license http://opensource.org/licenses/MIT MIT
+ *
+ * @link http://scssphp.github.io/scssphp
+ */
+
+namespace ScssPhp\ScssPhp\Value;
+
+use ScssPhp\ScssPhp\Visitor\ValueVisitor;
+
+/**
+ * The SassScript `null` value.
+ */
+final class SassNull extends Value
+{
+ private static SassNull $instance;
+
+ public static function create(): SassNull
+ {
+ return self::$instance ??= new self();
+ }
+
+ private function __construct()
+ {
+ }
+
+ public function isTruthy(): bool
+ {
+ return false;
+ }
+
+ public function isBlank(): bool
+ {
+ return true;
+ }
+
+ public function realNull(): ?Value
+ {
+ return null;
+ }
+
+ public function accept(ValueVisitor $visitor)
+ {
+ return $visitor->visitNull();
+ }
+
+ public function equals(object $other): bool
+ {
+ return $other instanceof SassNull;
+ }
+
+ public function unaryNot(): Value
+ {
+ return SassBoolean::create(true);
+ }
+}
diff --git a/vendor/scssphp/scssphp/src/Value/SassNumber.php b/vendor/scssphp/scssphp/src/Value/SassNumber.php
new file mode 100644
index 000000000..4255ede5a
--- /dev/null
+++ b/vendor/scssphp/scssphp/src/Value/SassNumber.php
@@ -0,0 +1,1044 @@
+<?php
+
+/**
+ * SCSSPHP
+ *
+ * @copyright 2012-2020 Leaf Corcoran
+ *
+ * @license http://opensource.org/licenses/MIT MIT
+ *
+ * @link http://scssphp.github.io/scssphp
+ */
+
+namespace ScssPhp\ScssPhp\Value;
+
+use JiriPudil\SealedClasses\Sealed;
+use ScssPhp\ScssPhp\Exception\SassScriptException;
+use ScssPhp\ScssPhp\Util\NumberUtil;
+use ScssPhp\ScssPhp\Util\StringUtil;
+use ScssPhp\ScssPhp\Visitor\ValueVisitor;
+
+/**
+ * A SassScript number.
+ *
+ * Numbers can have units. Although there's no literal syntax for it, numbers
+ * support scientific-style numerator and denominator units (for example,
+ * `miles/hour`). These are expected to be resolved before being emitted to
+ * CSS.
+ */
+#[Sealed(permits: [UnitlessSassNumber::class, SingleUnitSassNumber::class, ComplexSassNumber::class])]
+abstract class SassNumber extends Value
+{
+ final const PRECISION = 10;
+
+ /**
+ * @see https://www.w3.org/TR/css-values-3/
+ */
+ private const CONVERSIONS = [
+ 'in' => [
+ '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 convertible 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',
+ ];
+
+ private readonly float $value;
+
+ /**
+ * The representation of this number as two slash-separated numbers, if it has one.
+ *
+ * @var array{SassNumber, SassNumber}|null
+ * @internal
+ */
+ private readonly ?array $asSlash;
+
+ /**
+ * @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.
+ */
+ 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 list<string> $numeratorUnits
+ * @param list<string> $denominatorUnits
+ */
+ 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<string>
+ */
+ abstract public function getNumeratorUnits(): array;
+
+ /**
+ * @return list<string>
+ */
+ 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.
+ */
+ abstract protected function withValue(float $value): SassNumber;
+
+ /**
+ * @internal
+ */
+ abstract public function withSlash(SassNumber $numerator, SassNumber $denominator): SassNumber;
+
+ public function withoutSlash(): SassNumber
+ {
+ 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.
+ *
+ * @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.
+ *
+ * @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.
+ */
+ abstract public function hasUnits(): bool;
+
+ /**
+ * Whether $this has more than one numerator unit, or any denominator units.
+ *
+ * This is `true` for numbers whose units make them unrepresentable as CSS
+ * lengths.
+ */
+ abstract public function hasComplexUnits(): bool;
+
+ /**
+ * Returns whether $this has $unit as its only unit (and as a numerator).
+ */
+ 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.
+ */
+ 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<string> $newNumeratorUnits
+ * @param list<string> $newDenominatorUnits
+ *
+ * @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<string> $newNumeratorUnits
+ * @param list<string> $newDenominatorUnits
+ *
+ * @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 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
+ *
+ * @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 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
+ *
+ * @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<string> $newNumeratorUnits
+ * @param list<string> $newDenominatorUnits
+ *
+ * @throws SassScriptException if this number's units are not compatible with $newNumeratorUnits and $newDenominatorUnits
+ */
+ public function coerce(array $newNumeratorUnits, array $newDenominatorUnits, ?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<string> $newNumeratorUnits
+ * @param list<string> $newDenominatorUnits
+ *
+ * @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
+ */
+ 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 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
+ *
+ * @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 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
+ *
+ * @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.
+ *
+ * @internal
+ */
+ public function isComparableTo(SassNumber $other): bool
+ {
+ if (!$this->hasUnits() || !$other->hasUnits()) {
+ return true;
+ }
+
+ try {
+ $this->greaterThan($other);
+ return true;
+ } catch (SassScriptException) {
+ return false;
+ }
+ }
+
+ public function greaterThan(Value $other): SassBoolean
+ {
+ if ($other instanceof SassNumber) {
+ // Not using a first-class callable for NumberUtil::fuzzyGreaterThan(),
+ // because of a PHP 8.1 bug that results in a segmentation
+ // fault, when an Exception is thrown from a function taking the FCC as
+ // a parameter.
+ //
+ // see: https://github.com/php/php-src/commit/b3e26c3036a54e9821ea7119c26cdabe484fe36d
+ // see: https://github.com/scssphp/scssphp/issues/752#issuecomment-2423857568
+ 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::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::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::fuzzyLessThanOrEquals(...)));
+ }
+
+ throw new SassScriptException("Undefined operation \"$this > $other\".");
+ }
+
+ public function modulo(Value $other): SassNumber
+ {
+ if ($other instanceof SassNumber) {
+ return $this->withValue($this->coerceUnits($other, NumberUtil::moduloLikeSass(...)));
+ }
+
+ throw new SassScriptException("Undefined operation \"$this % $other\".");
+ }
+
+ public function plus(Value $other): Value
+ {
+ if ($other instanceof SassNumber) {
+ return $this->withValue($this->coerceUnits($other, fn($num1, $num2) => $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, fn($num1, $num2) => $num1 - $num2));
+ }
+
+ if (!$other instanceof SassColor) {
+ return parent::minus($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<string> $units
+ */
+ private static function getCanonicalMultiplier(array $units): float
+ {
+ return array_reduce($units, fn($multiplier, $unit) => $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<string> $units
+ *
+ * @return list<string>
+ */
+ 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 callable(float, float): T $operation
+ *
+ * @return T
+ *
+ * @param-immediately-invoked-callable $operation
+ */
+ 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<string> $newNumeratorUnits
+ * @param list<string> $newDenominatorUnits
+ * @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
+ *
+ * @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 list<string> $newNumeratorUnits
+ * @param list<string> $newDenominatorUnits
+ */
+ 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 %s %s.', $this, StringUtil::pluralize('unit', \count($newNumeratorUnits) + \count($newDenominatorUnits)), self::buildUnitString($newNumeratorUnits, $newDenominatorUnits)), $name);
+ }
+
+ /**
+ * @param list<string> $otherNumerators
+ * @param list<string> $otherDenominators
+ */
+ 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`.
+ */
+ 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<string> $numerators
+ * @param list<string> $denominators
+ */
+ 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) : '');
+ }
+
+ /**
+ * Returns a suggested Sass snippet for converting a variable named $name
+ * (without `%`) containing this number into a number with the same value and
+ * the given $unit.
+ *
+ * If $unit is null, this forces the number to be unitless.
+ *
+ * This is used for deprecation warnings when restricting which units are
+ * allowed for a given function.
+ *
+ * @internal
+ */
+ public function unitSuggestion(string $name, ?string $unit = null): string
+ {
+ $result = "\$$name"
+ . implode(array_map(fn($unit) => " * 1$unit", $this->getDenominatorUnits()))
+ . implode(array_map(fn($unit) => " / 1$unit", $this->getNumeratorUnits()))
+ . ($unit === null ? '' : " * 1$unit");
+
+ return $this->getNumeratorUnits() === [] ? $result : "calc($result)";
+ }
+}
diff --git a/vendor/scssphp/scssphp/src/Value/SassString.php b/vendor/scssphp/scssphp/src/Value/SassString.php
new file mode 100644
index 000000000..31d21ed88
--- /dev/null
+++ b/vendor/scssphp/scssphp/src/Value/SassString.php
@@ -0,0 +1,236 @@
+<?php
+
+/**
+ * SCSSPHP
+ *
+ * @copyright 2012-2020 Leaf Corcoran
+ *
+ * @license http://opensource.org/licenses/MIT MIT
+ *
+ * @link http://scssphp.github.io/scssphp
+ */
+
+namespace ScssPhp\ScssPhp\Value;
+
+use ScssPhp\ScssPhp\Exception\SassScriptException;
+use ScssPhp\ScssPhp\Visitor\ValueVisitor;
+
+/**
+ * A SassScript string.
+ *
+ * Strings can either be quoted or unquoted. Unquoted strings are usually CSS
+ * identifiers, but they may contain any text.
+ */
+final class SassString extends Value
+{
+ /**
+ * The contents of the string.
+ *
+ * For quoted strings, this is the semantic content—any escape sequences that
+ * were been written in the source text are resolved to their Unicode values.
+ * For unquoted strings, though, escape sequences are preserved as literal
+ * backslashes.
+ *
+ * This difference allows us to distinguish between identifiers with escapes,
+ * such as `url\u28 http://example.com\u29`, and unquoted strings that
+ * contain characters that aren't valid in identifiers, such as
+ * `url(http://example.com)`. Unfortunately, it also means that we don't
+ * consider `foo` and `f\6F\6F` the same string.
+ */
+ private readonly string $text;
+
+ /**
+ * Whether this string has quotes.
+ */
+ private readonly bool $quotes;
+
+ public function __construct(string $text, bool $quotes = true)
+ {
+ $this->text = $text;
+ $this->quotes = $quotes;
+ }
+
+ public function getText(): string
+ {
+ return $this->text;
+ }
+
+ public function hasQuotes(): bool
+ {
+ return $this->quotes;
+ }
+
+ public function getSassLength(): int
+ {
+ return mb_strlen($this->text, 'UTF-8');
+ }
+
+ 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(mb_substr($this->text, 0, $codepointIndex, 'UTF-8'));
+ }
+
+ /**
+ * 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/vendor/scssphp/scssphp/src/Value/SingleUnitSassNumber.php b/vendor/scssphp/scssphp/src/Value/SingleUnitSassNumber.php
new file mode 100644
index 000000000..4b9d71f97
--- /dev/null
+++ b/vendor/scssphp/scssphp/src/Value/SingleUnitSassNumber.php
@@ -0,0 +1,333 @@
+<?php
+
+/**
+ * SCSSPHP
+ *
+ * @copyright 2012-2020 Leaf Corcoran
+ *
+ * @license http://opensource.org/licenses/MIT MIT
+ *
+ * @link http://scssphp.github.io/scssphp
+ */
+
+namespace ScssPhp\ScssPhp\Value;
+
+use ScssPhp\ScssPhp\Util\NumberUtil;
+
+/**
+ * A specialized subclass of {@see SassNumber} for numbers that have exactly one numerator unit.
+ *
+ * @internal
+ */
+final class SingleUnitSassNumber extends SassNumber
+{
+ private const COMPATIBLE_LENGTH_UNITS = ['em', 'rem', 'ex', 'rex', 'cap', 'rcap', 'ch', 'rch', 'ic', 'ric', 'lh', 'rlh', 'vw', 'lvw', 'svw', 'dvw', 'vh', 'lvh', 'svh', 'dvh', 'vi', 'lvi', 'svi', 'dvi', 'vb', 'lvb', 'svb', 'dvb', 'vmin', 'lvmin', 'svmin', 'dvmin', 'vmax', 'lvmax', 'svmax', 'dvmax', 'cqw', 'cqh', 'cqi', 'cqb', 'cqmin', 'cqmax', 'cm', 'mm', 'q', 'in', 'pc', 'pt', 'px'];
+
+ private const KNOWN_COMPATIBILITIES_BY_UNIT = [
+ // length
+ 'em' => self::COMPATIBLE_LENGTH_UNITS,
+ 'rem' => self::COMPATIBLE_LENGTH_UNITS,
+ 'ex' => self::COMPATIBLE_LENGTH_UNITS,
+ 'rex' => self::COMPATIBLE_LENGTH_UNITS,
+ 'cap' => self::COMPATIBLE_LENGTH_UNITS,
+ 'rcap' => self::COMPATIBLE_LENGTH_UNITS,
+ 'ch' => self::COMPATIBLE_LENGTH_UNITS,
+ 'rch' => self::COMPATIBLE_LENGTH_UNITS,
+ 'ic' => self::COMPATIBLE_LENGTH_UNITS,
+ 'ric' => self::COMPATIBLE_LENGTH_UNITS,
+ 'lh' => self::COMPATIBLE_LENGTH_UNITS,
+ 'rlh' => self::COMPATIBLE_LENGTH_UNITS,
+ 'vw' => self::COMPATIBLE_LENGTH_UNITS,
+ 'lvw' => self::COMPATIBLE_LENGTH_UNITS,
+ 'svw' => self::COMPATIBLE_LENGTH_UNITS,
+ 'dvw' => self::COMPATIBLE_LENGTH_UNITS,
+ 'vh' => self::COMPATIBLE_LENGTH_UNITS,
+ 'lvh' => self::COMPATIBLE_LENGTH_UNITS,
+ 'svh' => self::COMPATIBLE_LENGTH_UNITS,
+ 'dvh' => self::COMPATIBLE_LENGTH_UNITS,
+ 'vi' => self::COMPATIBLE_LENGTH_UNITS,
+ 'lvi' => self::COMPATIBLE_LENGTH_UNITS,
+ 'svi' => self::COMPATIBLE_LENGTH_UNITS,
+ 'dvi' => self::COMPATIBLE_LENGTH_UNITS,
+ 'vb' => self::COMPATIBLE_LENGTH_UNITS,
+ 'lvb' => self::COMPATIBLE_LENGTH_UNITS,
+ 'svb' => self::COMPATIBLE_LENGTH_UNITS,
+ 'dvb' => self::COMPATIBLE_LENGTH_UNITS,
+ 'vmin' => self::COMPATIBLE_LENGTH_UNITS,
+ 'lvmin' => self::COMPATIBLE_LENGTH_UNITS,
+ 'svmin' => self::COMPATIBLE_LENGTH_UNITS,
+ 'dvmin' => self::COMPATIBLE_LENGTH_UNITS,
+ 'vmax' => self::COMPATIBLE_LENGTH_UNITS,
+ 'lvmax' => self::COMPATIBLE_LENGTH_UNITS,
+ 'svmax' => self::COMPATIBLE_LENGTH_UNITS,
+ 'dvmax' => self::COMPATIBLE_LENGTH_UNITS,
+ 'cqw' => self::COMPATIBLE_LENGTH_UNITS,
+ 'cqh' => self::COMPATIBLE_LENGTH_UNITS,
+ 'cqi' => self::COMPATIBLE_LENGTH_UNITS,
+ 'cqb' => self::COMPATIBLE_LENGTH_UNITS,
+ 'cqmin' => self::COMPATIBLE_LENGTH_UNITS,
+ 'cqmax' => self::COMPATIBLE_LENGTH_UNITS,
+ 'cm' => self::COMPATIBLE_LENGTH_UNITS,
+ 'mm' => self::COMPATIBLE_LENGTH_UNITS,
+ 'q' => self::COMPATIBLE_LENGTH_UNITS,
+ 'in' => self::COMPATIBLE_LENGTH_UNITS,
+ 'pc' => self::COMPATIBLE_LENGTH_UNITS,
+ 'pt' => self::COMPATIBLE_LENGTH_UNITS,
+ 'px' => self::COMPATIBLE_LENGTH_UNITS,
+ // 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'],
+ ];
+
+ private readonly 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;
+ }
+
+ public function hasComplexUnits(): bool
+ {
+ return false;
+ }
+
+ 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 list<string> $otherNumerators
+ * @param list<string> $otherDenominators
+ */
+ protected function multiplyUnits(float $value, array $otherNumerators, array $otherDenominators): SassNumber
+ {
+ $newNumerators = $otherNumerators;
+ $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) {
+ array_unshift($newNumerators, $this->unit);
+ }
+
+ return SassNumber::withUnits($value, $newNumerators, array_values($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/vendor/scssphp/scssphp/src/Value/SpanColorFormat.php b/vendor/scssphp/scssphp/src/Value/SpanColorFormat.php
new file mode 100644
index 000000000..cad9a10e4
--- /dev/null
+++ b/vendor/scssphp/scssphp/src/Value/SpanColorFormat.php
@@ -0,0 +1,33 @@
+<?php
+
+/**
+ * SCSSPHP
+ *
+ * @copyright 2012-2020 Leaf Corcoran
+ *
+ * @license http://opensource.org/licenses/MIT MIT
+ *
+ * @link http://scssphp.github.io/scssphp
+ */
+
+namespace ScssPhp\ScssPhp\Value;
+
+use SourceSpan\FileSpan;
+
+/**
+ * @internal
+ */
+final class SpanColorFormat implements ColorFormat
+{
+ private readonly FileSpan $span;
+
+ public function __construct(FileSpan $span)
+ {
+ $this->span = $span;
+ }
+
+ public function getOriginal(): string
+ {
+ return $this->span->getText();
+ }
+}
diff --git a/vendor/scssphp/scssphp/src/Value/UnitlessSassNumber.php b/vendor/scssphp/scssphp/src/Value/UnitlessSassNumber.php
new file mode 100644
index 000000000..3a4bc99b5
--- /dev/null
+++ b/vendor/scssphp/scssphp/src/Value/UnitlessSassNumber.php
@@ -0,0 +1,223 @@
+<?php
+
+/**
+ * SCSSPHP
+ *
+ * @copyright 2012-2020 Leaf Corcoran
+ *
+ * @license http://opensource.org/licenses/MIT MIT
+ *
+ * @link http://scssphp.github.io/scssphp
+ */
+
+namespace ScssPhp\ScssPhp\Value;
+
+use ScssPhp\ScssPhp\Util\NumberUtil;
+
+/**
+ * A specialized subclass of {@see SassNumber} for numbers that have no units.
+ *
+ * @internal
+ */
+final class UnitlessSassNumber extends SassNumber
+{
+ /**
+ * @param array{SassNumber, SassNumber}|null $asSlash
+ */
+ public function __construct(float $value, ?array $asSlash = null)
+ {
+ parent::__construct($value, $asSlash);
+ }
+
+ public function getNumeratorUnits(): array
+ {
+ return [];
+ }
+
+ public function getDenominatorUnits(): array
+ {
+ return [];
+ }
+
+ public function hasUnits(): bool
+ {
+ return false;
+ }
+
+ public function hasComplexUnits(): bool
+ {
+ return false;
+ }
+
+ protected function withValue(float $value): SassNumber
+ {
+ return new self($value);
+ }
+
+ public function withSlash(SassNumber $numerator, SassNumber $denominator): SassNumber
+ {
+ return new self($this->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): SassNumber
+ {
+ 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/vendor/scssphp/scssphp/src/Value/Value.php b/vendor/scssphp/scssphp/src/Value/Value.php
new file mode 100644
index 000000000..29402ce11
--- /dev/null
+++ b/vendor/scssphp/scssphp/src/Value/Value.php
@@ -0,0 +1,683 @@
+<?php
+
+/**
+ * SCSSPHP
+ *
+ * @copyright 2012-2020 Leaf Corcoran
+ *
+ * @license http://opensource.org/licenses/MIT MIT
+ *
+ * @link http://scssphp.github.io/scssphp
+ */
+
+namespace ScssPhp\ScssPhp\Value;
+
+use JiriPudil\SealedClasses\Sealed;
+use ScssPhp\ScssPhp\Ast\Selector\ComplexSelector;
+use ScssPhp\ScssPhp\Ast\Selector\CompoundSelector;
+use ScssPhp\ScssPhp\Ast\Selector\SelectorList;
+use ScssPhp\ScssPhp\Ast\Selector\SimpleSelector;
+use ScssPhp\ScssPhp\Deprecation;
+use ScssPhp\ScssPhp\Exception\SassFormatException;
+use ScssPhp\ScssPhp\Exception\SassScriptException;
+use ScssPhp\ScssPhp\Serializer\Serializer;
+use ScssPhp\ScssPhp\Util\Equatable;
+use ScssPhp\ScssPhp\Visitor\ValueVisitor;
+use ScssPhp\ScssPhp\Warn;
+
+/**
+ * A SassScript value.
+ *
+ * All SassScript values are unmodifiable. New values can be constructed using
+ * subclass constructors like `new SassString`. Untyped values can be cast to
+ * particular types using `assert*()` functions like {@see assertString}, which
+ * throw user-friendly error messages if they fail.
+ */
+#[Sealed(permits: [SassBoolean::class, SassCalculation::class, SassColor::class, SassFunction::class, SassList::class, SassMap::class, SassMixin::class, SassNull::class, SassNumber::class, SassString::class])]
+abstract class Value implements Equatable, \Stringable
+{
+ /**
+ * Whether the value counts as `true` in an `@if` statement and other contexts
+ */
+ public function isTruthy(): bool
+ {
+ return true;
+ }
+
+ /**
+ * The separator for this value as a list.
+ *
+ * All SassScript values can be used as lists. Maps count as lists of pairs,
+ * and all other values count as single-value lists.
+ */
+ public function getSeparator(): ListSeparator
+ {
+ return ListSeparator::UNDECIDED;
+ }
+
+ /**
+ * Whether this value as a list has brackets.
+ *
+ * All SassScript values can be used as lists. Maps count as lists of pairs,
+ * and all other values count as single-value lists.
+ */
+ public function hasBrackets(): bool
+ {
+ return false;
+ }
+
+ /**
+ * This value as a list.
+ *
+ * All SassScript values can be used as lists. Maps count as lists of pairs,
+ * and all other values count as single-value lists.
+ *
+ * @return list<Value>
+ */
+ 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<T> $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
+ {
+ $indexValue = $sassIndex->assertNumber($name);
+
+ if ($indexValue->hasUnits()) {
+ $message = <<<WARNING
+\$$name: Passing a number with unit {$indexValue->getUnitString()} is deprecated.
+
+To preserve current behavior: {$indexValue->unitSuggestion($name ?? 'index')}
+
+More info: https://sass-lang.com/d/function-units
+WARNING;
+
+ Warn::forDeprecation($message, Deprecation::functionUnits);
+ }
+
+ $index = $indexValue->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.
+ *
+ * @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.
+ *
+ * @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.
+ *
+ * @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 function reference.
+ *
+ * If this came from a function argument, $name is the argument name
+ * (without the `$`). It's used for error reporting.
+ *
+ * @throws SassScriptException
+ */
+ public function assertFunction(?string $name = null): SassFunction
+ {
+ throw SassScriptException::forArgument("$this is not a function reference.", $name);
+ }
+
+ /**
+ * Throws a {@see SassScriptException} if $this isn't a mixin reference.
+ *
+ * If this came from a function argument, $name is the argument name
+ * (without the `$`). It's used for error reporting.
+ *
+ * @throws SassScriptException
+ */
+ public function assertMixin(?string $name = null): SassMixin
+ {
+ throw SassScriptException::forArgument("$this is not a mixin reference.", $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.
+ *
+ * @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.
+ */
+ 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.
+ *
+ * @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.
+ *
+ * @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, 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.
+ *
+ * @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.
+ *
+ * @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.
+ *
+ * @internal
+ */
+ public function isVar(): bool
+ {
+ return false;
+ }
+
+ /**
+ * Returns PHP's `null` value if this is Sass null, and returns `$this` otherwise
+ */
+ public function realNull(): ?Value
+ {
+ return $this;
+ }
+
+ /**
+ * Returns a new list containing $contents that defaults to this value's
+ * separator and brackets.
+ *
+ * @param list<Value> $contents
+ */
+ public function withListContents(array $contents, ?ListSeparator $separator = null, ?bool $brackets = null): SassList
+ {
+ return new SassList($contents, $separator ?? $this->getSeparator(), $brackets ?? $this->hasBrackets());
+ }
+
+ /**
+ * The SassScript = operation
+ *
+ * @internal
+ */
+ public function singleEquals(Value $other): Value
+ {
+ return new SassString(sprintf('%s=%s', $this->toCssString(), $other->toCssString()), false);
+ }
+
+ /**
+ * The SassScript `>` operation.
+ *
+ * @internal
+ */
+ public function greaterThan(Value $other): SassBoolean
+ {
+ throw new SassScriptException("Undefined operation \"$this > $other\".");
+ }
+
+ /**
+ * The SassScript `>=` operation.
+ *
+ * @internal
+ */
+ public function greaterThanOrEquals(Value $other): SassBoolean
+ {
+ throw new SassScriptException("Undefined operation \"$this >= $other\".");
+ }
+
+ /**
+ * The SassScript `<` operation.
+ *
+ * @internal
+ */
+ public function lessThan(Value $other): SassBoolean
+ {
+ throw new SassScriptException("Undefined operation \"$this < $other\".");
+ }
+
+ /**
+ * The SassScript `<=` operation.
+ *
+ * @internal
+ */
+ public function lessThanOrEquals(Value $other): SassBoolean
+ {
+ throw new SassScriptException("Undefined operation \"$this <= $other\".");
+ }
+
+ /**
+ * The SassScript `*` operation.
+ *
+ * @internal
+ */
+ public function times(Value $other): Value
+ {
+ throw new SassScriptException("Undefined operation \"$this * $other\".");
+ }
+
+ /**
+ * The SassScript `%` operation.
+ *
+ * @internal
+ */
+ public function modulo(Value $other): Value
+ {
+ throw new SassScriptException("Undefined operation \"$this % $other\".");
+ }
+
+ /**
+ * The SassScript `+` operation.
+ *
+ * @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.
+ *
+ * @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.
+ *
+ * @internal
+ */
+ public function dividedBy(Value $other): Value
+ {
+ return new SassString(sprintf('%s/%s', $this->toCssString(), $other->toCssString()), false);
+ }
+
+ /**
+ * The SassScript unary `+` operation.
+ *
+ * @internal
+ */
+ public function unaryPlus(): Value
+ {
+ return new SassString(sprintf('+%s', $this->toCssString()), false);
+ }
+
+ /**
+ * The SassScript unary `-` operation.
+ *
+ * @internal
+ */
+ public function unaryMinus(): Value
+ {
+ return new SassString(sprintf('-%s', $this->toCssString()), false);
+ }
+
+ /**
+ * The SassScript unary `/` operation.
+ *
+ * @internal
+ */
+ public function unaryDivide(): Value
+ {
+ return new SassString(sprintf('/%s', $this->toCssString()), false);
+ }
+
+ /**
+ * The SassScript unary `not` operation.
+ *
+ * @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.
+ *
+ * @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.
+ */
+ final public function __toString(): string
+ {
+ return Serializer::serializeValue($this, true);
+ }
+}
diff --git a/vendor/scssphp/scssphp/src/ValueConverter.php b/vendor/scssphp/scssphp/src/ValueConverter.php
index e12a0eb54..c874f27f2 100644
--- a/vendor/scssphp/scssphp/src/ValueConverter.php
+++ b/vendor/scssphp/scssphp/src/ValueConverter.php
@@ -12,7 +12,13 @@
namespace ScssPhp\ScssPhp;
+use ScssPhp\ScssPhp\Logger\QuietLogger;
use ScssPhp\ScssPhp\Node\Number;
+use ScssPhp\ScssPhp\Value\SassBoolean;
+use ScssPhp\ScssPhp\Value\SassNull;
+use ScssPhp\ScssPhp\Value\SassNumber;
+use ScssPhp\ScssPhp\Value\SassString;
+use ScssPhp\ScssPhp\Value\Value;
final class ValueConverter
{
@@ -28,18 +34,27 @@ final class ValueConverter
* Compiler methods for registering custom variables. No other
* guarantee about it is provided. It should be considered
* opaque values by the caller.
- *
- * @param string $source
- *
- * @return mixed
*/
- public static function parseValue($source)
+ public static function parseValue(string $source): Value
{
- $parser = new Parser(__CLASS__);
+ $value = null;
- if (!$parser->parseValue($source, $value)) {
- throw new \InvalidArgumentException(sprintf('Invalid value source "%s".', $source));
- }
+ $compiler = new Compiler();
+ $compiler->setLogger(new QuietLogger());
+ $compiler->registerFunction('scssphp-parse-value', function (array $arguments) use (&$value): Value {
+ \assert(\count($arguments) === 1);
+ \assert($arguments[0] instanceof Value);
+ $value = $arguments[0];
+
+ return SassNull::create();
+ }, ['arg']);
+ $scss = <<<SCSS
+ a {b: scssphp-parse-value(($source))}
+ SCSS;
+
+ $compiler->compileString($scss);
+
+ \assert($value !== null);
return $value;
}
@@ -51,43 +66,44 @@ final class ValueConverter
* Compiler methods for registering custom variables. No other
* guarantee about it is provided. It should be considered
* opaque values by the caller.
- *
- * @param mixed $value
- *
- * @return mixed
*/
- public static function fromPhp($value)
+ public static function fromPhp(mixed $value): Value
{
- if ($value instanceof Number) {
+ if ($value instanceof Value) {
return $value;
}
+ if ($value instanceof Number) {
+ return SassNumber::withUnits($value->getDimension(), $value->getNumeratorUnits(), $value->getDenominatorUnits());
+ }
+
if (is_array($value) && isset($value[0]) && \in_array($value[0], [Type::T_NULL, Type::T_COLOR, Type::T_KEYWORD, Type::T_LIST, Type::T_MAP, Type::T_STRING])) {
- return $value;
+ // TODO convert legacy value
+ throw new \LogicException('Not implemented');
}
if ($value === null) {
- return Compiler::$null;
+ return SassNull::create();
}
if ($value === true) {
- return Compiler::$true;
+ return SassBoolean::create(true);
}
if ($value === false) {
- return Compiler::$false;
+ return SassBoolean::create(false);
}
if ($value === '') {
- return Compiler::$emptyString;
+ return new SassString('');
}
if (\is_int($value) || \is_float($value)) {
- return new Number($value, '');
+ return SassNumber::create($value);
}
if (\is_string($value)) {
- return [Type::T_STRING, '"', [$value]];
+ return new SassString($value);
}
throw new \InvalidArgumentException(sprintf('Cannot convert the value of type "%s" to a Sass value.', gettype($value)));
diff --git a/vendor/scssphp/scssphp/src/Version.php b/vendor/scssphp/scssphp/src/Version.php
index 89bd59526..411c7be35 100644
--- a/vendor/scssphp/scssphp/src/Version.php
+++ b/vendor/scssphp/scssphp/src/Version.php
@@ -17,7 +17,7 @@ namespace ScssPhp\ScssPhp;
*
* @author Leaf Corcoran <leafot@gmail.com>
*/
-class Version
+final class Version
{
- const VERSION = '1.12.1';
+ const VERSION = '2.0.1';
}
diff --git a/vendor/scssphp/scssphp/src/Visitor/AnySelectorVisitor.php b/vendor/scssphp/scssphp/src/Visitor/AnySelectorVisitor.php
new file mode 100644
index 000000000..3af45d004
--- /dev/null
+++ b/vendor/scssphp/scssphp/src/Visitor/AnySelectorVisitor.php
@@ -0,0 +1,97 @@
+<?php
+
+/**
+ * SCSSPHP
+ *
+ * @copyright 2012-2020 Leaf Corcoran
+ *
+ * @license http://opensource.org/licenses/MIT MIT
+ *
+ * @link http://scssphp.github.io/scssphp
+ */
+
+namespace ScssPhp\ScssPhp\Visitor;
+
+use ScssPhp\ScssPhp\Ast\Selector\AttributeSelector;
+use ScssPhp\ScssPhp\Ast\Selector\ClassSelector;
+use ScssPhp\ScssPhp\Ast\Selector\ComplexSelector;
+use ScssPhp\ScssPhp\Ast\Selector\ComplexSelectorComponent;
+use ScssPhp\ScssPhp\Ast\Selector\CompoundSelector;
+use ScssPhp\ScssPhp\Ast\Selector\IDSelector;
+use ScssPhp\ScssPhp\Ast\Selector\ParentSelector;
+use ScssPhp\ScssPhp\Ast\Selector\PlaceholderSelector;
+use ScssPhp\ScssPhp\Ast\Selector\PseudoSelector;
+use ScssPhp\ScssPhp\Ast\Selector\SelectorList;
+use ScssPhp\ScssPhp\Ast\Selector\SimpleSelector;
+use ScssPhp\ScssPhp\Ast\Selector\TypeSelector;
+use ScssPhp\ScssPhp\Ast\Selector\UniversalSelector;
+use ScssPhp\ScssPhp\Util\IterableUtil;
+
+/**
+ * A visitor that visits each selector in a Sass selector AST and returns
+ * `true` if any of the individual methods return `true`.
+ *
+ * Each method returns `false` by default.
+ *
+ * @template-implements SelectorVisitor<bool>
+ * @internal
+ */
+abstract class AnySelectorVisitor implements SelectorVisitor
+{
+ public function visitComplexSelector(ComplexSelector $complex): bool
+ {
+ return IterableUtil::any($complex->getComponents(), fn (ComplexSelectorComponent $component) => $this->visitCompoundSelector($component->getSelector()));
+ }
+
+ public function visitCompoundSelector(CompoundSelector $compound): bool
+ {
+ return IterableUtil::any($compound->getComponents(), fn (SimpleSelector $simple) => $simple->accept($this));
+ }
+
+ public function visitPseudoSelector(PseudoSelector $pseudo): bool
+ {
+ $selector = $pseudo->getSelector();
+
+ return $selector === null ? false : $selector->accept($this);
+ }
+
+ public function visitSelectorList(SelectorList $list): bool
+ {
+ return IterableUtil::any($list->getComponents(), $this->visitComplexSelector(...));
+ }
+
+ 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/vendor/scssphp/scssphp/src/Visitor/CssVisitor.php b/vendor/scssphp/scssphp/src/Visitor/CssVisitor.php
new file mode 100644
index 000000000..c1e1a0496
--- /dev/null
+++ b/vendor/scssphp/scssphp/src/Visitor/CssVisitor.php
@@ -0,0 +1,79 @@
+<?php
+
+/**
+ * SCSSPHP
+ *
+ * @copyright 2012-2020 Leaf Corcoran
+ *
+ * @license http://opensource.org/licenses/MIT MIT
+ *
+ * @link http://scssphp.github.io/scssphp
+ */
+
+namespace ScssPhp\ScssPhp\Visitor;
+
+use ScssPhp\ScssPhp\Ast\Css\CssAtRule;
+use ScssPhp\ScssPhp\Ast\Css\CssComment;
+use ScssPhp\ScssPhp\Ast\Css\CssDeclaration;
+use ScssPhp\ScssPhp\Ast\Css\CssImport;
+use ScssPhp\ScssPhp\Ast\Css\CssKeyframeBlock;
+use ScssPhp\ScssPhp\Ast\Css\CssMediaRule;
+use ScssPhp\ScssPhp\Ast\Css\CssStyleRule;
+use ScssPhp\ScssPhp\Ast\Css\CssStylesheet;
+use ScssPhp\ScssPhp\Ast\Css\CssSupportsRule;
+
+/**
+ * An interface for visitors that traverse CSS statements.
+ *
+ * @internal
+ *
+ * @template T
+ * @template-extends ModifiableCssVisitor<T>
+ */
+interface CssVisitor extends ModifiableCssVisitor
+{
+ /**
+ * @return T
+ */
+ public function visitCssAtRule(CssAtRule $node);
+
+ /**
+ * @return T
+ */
+ public function visitCssComment(CssComment $node);
+
+ /**
+ * @return T
+ */
+ public function visitCssDeclaration(CssDeclaration $node);
+
+ /**
+ * @return T
+ */
+ public function visitCssImport(CssImport $node);
+
+ /**
+ * @return T
+ */
+ public function visitCssKeyframeBlock(CssKeyframeBlock $node);
+
+ /**
+ * @return T
+ */
+ public function visitCssMediaRule(CssMediaRule $node);
+
+ /**
+ * @return T
+ */
+ public function visitCssStyleRule(CssStyleRule $node);
+
+ /**
+ * @return T
+ */
+ public function visitCssStylesheet(CssStylesheet $node);
+
+ /**
+ * @return T
+ */
+ public function visitCssSupportsRule(CssSupportsRule $node);
+}
diff --git a/vendor/scssphp/scssphp/src/Visitor/EveryCssVisitor.php b/vendor/scssphp/scssphp/src/Visitor/EveryCssVisitor.php
new file mode 100644
index 000000000..1ed5766f7
--- /dev/null
+++ b/vendor/scssphp/scssphp/src/Visitor/EveryCssVisitor.php
@@ -0,0 +1,82 @@
+<?php
+
+/**
+ * SCSSPHP
+ *
+ * @copyright 2012-2020 Leaf Corcoran
+ *
+ * @license http://opensource.org/licenses/MIT MIT
+ *
+ * @link http://scssphp.github.io/scssphp
+ */
+
+namespace ScssPhp\ScssPhp\Visitor;
+
+use ScssPhp\ScssPhp\Ast\Css\CssAtRule;
+use ScssPhp\ScssPhp\Ast\Css\CssComment;
+use ScssPhp\ScssPhp\Ast\Css\CssDeclaration;
+use ScssPhp\ScssPhp\Ast\Css\CssImport;
+use ScssPhp\ScssPhp\Ast\Css\CssKeyframeBlock;
+use ScssPhp\ScssPhp\Ast\Css\CssMediaRule;
+use ScssPhp\ScssPhp\Ast\Css\CssNode;
+use ScssPhp\ScssPhp\Ast\Css\CssStyleRule;
+use ScssPhp\ScssPhp\Ast\Css\CssStylesheet;
+use ScssPhp\ScssPhp\Ast\Css\CssSupportsRule;
+use ScssPhp\ScssPhp\Util\IterableUtil;
+
+/**
+ * A visitor that visits each statement in a CSS AST and returns `true` if all
+ * of the individual methods return `true`.
+ *
+ * Each method returns `false` by default.
+ *
+ * @template-implements CssVisitor<bool>
+ * @internal
+ */
+abstract class EveryCssVisitor implements CssVisitor
+{
+ public function visitCssAtRule(CssAtRule $node): bool
+ {
+ return IterableUtil::every($node->getChildren(), fn (CssNode $child) => $child->accept($this));
+ }
+
+ public function visitCssComment(CssComment $node): bool
+ {
+ return false;
+ }
+
+ public function visitCssDeclaration(CssDeclaration $node): bool
+ {
+ return false;
+ }
+
+ public function visitCssImport(CssImport $node): bool
+ {
+ return false;
+ }
+
+ public function visitCssKeyframeBlock(CssKeyframeBlock $node): bool
+ {
+ return IterableUtil::every($node->getChildren(), fn (CssNode $child) => $child->accept($this));
+ }
+
+ public function visitCssMediaRule(CssMediaRule $node): bool
+ {
+ return IterableUtil::every($node->getChildren(), fn (CssNode $child) => $child->accept($this));
+ }
+
+ public function visitCssStyleRule(CssStyleRule $node): bool
+ {
+ return IterableUtil::every($node->getChildren(), fn (CssNode $child) => $child->accept($this));
+ }
+
+ public function visitCssStylesheet(CssStylesheet $node): bool
+ {
+ return IterableUtil::every($node->getChildren(), fn (CssNode $child) => $child->accept($this));
+ }
+
+ public function visitCssSupportsRule(CssSupportsRule $node): bool
+ {
+ return IterableUtil::every($node->getChildren(), fn (CssNode $child) => $child->accept($this));
+ }
+}
diff --git a/vendor/scssphp/scssphp/src/Visitor/ExpressionVisitor.php b/vendor/scssphp/scssphp/src/Visitor/ExpressionVisitor.php
new file mode 100644
index 000000000..a24fa737a
--- /dev/null
+++ b/vendor/scssphp/scssphp/src/Visitor/ExpressionVisitor.php
@@ -0,0 +1,126 @@
+<?php
+
+/**
+ * SCSSPHP
+ *
+ * @copyright 2012-2020 Leaf Corcoran
+ *
+ * @license http://opensource.org/licenses/MIT MIT
+ *
+ * @link http://scssphp.github.io/scssphp
+ */
+
+namespace ScssPhp\ScssPhp\Visitor;
+
+use ScssPhp\ScssPhp\Ast\Sass\Expression\BinaryOperationExpression;
+use ScssPhp\ScssPhp\Ast\Sass\Expression\BooleanExpression;
+use ScssPhp\ScssPhp\Ast\Sass\Expression\ColorExpression;
+use ScssPhp\ScssPhp\Ast\Sass\Expression\FunctionExpression;
+use ScssPhp\ScssPhp\Ast\Sass\Expression\IfExpression;
+use ScssPhp\ScssPhp\Ast\Sass\Expression\InterpolatedFunctionExpression;
+use ScssPhp\ScssPhp\Ast\Sass\Expression\ListExpression;
+use ScssPhp\ScssPhp\Ast\Sass\Expression\MapExpression;
+use ScssPhp\ScssPhp\Ast\Sass\Expression\NullExpression;
+use ScssPhp\ScssPhp\Ast\Sass\Expression\NumberExpression;
+use ScssPhp\ScssPhp\Ast\Sass\Expression\ParenthesizedExpression;
+use ScssPhp\ScssPhp\Ast\Sass\Expression\SelectorExpression;
+use ScssPhp\ScssPhp\Ast\Sass\Expression\StringExpression;
+use ScssPhp\ScssPhp\Ast\Sass\Expression\SupportsExpression;
+use ScssPhp\ScssPhp\Ast\Sass\Expression\UnaryOperationExpression;
+use ScssPhp\ScssPhp\Ast\Sass\Expression\ValueExpression;
+use ScssPhp\ScssPhp\Ast\Sass\Expression\VariableExpression;
+
+/**
+ * An interface for visitors that traverse SassScript expressions.
+ *
+ * @internal
+ *
+ * @template T
+ */
+interface ExpressionVisitor
+{
+ /**
+ * @return T
+ */
+ public function visitBinaryOperationExpression(BinaryOperationExpression $node);
+
+ /**
+ * @return T
+ */
+ public function visitBooleanExpression(BooleanExpression $node);
+
+ /**
+ * @return T
+ */
+ public function visitColorExpression(ColorExpression $node);
+
+ /**
+ * @return T
+ */
+ public function visitInterpolatedFunctionExpression(InterpolatedFunctionExpression $node);
+
+ /**
+ * @return T
+ */
+ public function visitFunctionExpression(FunctionExpression $node);
+
+ /**
+ * @return T
+ */
+ public function visitIfExpression(IfExpression $node);
+
+ /**
+ * @return T
+ */
+ public function visitListExpression(ListExpression $node);
+
+ /**
+ * @return T
+ */
+ public function visitMapExpression(MapExpression $node);
+
+ /**
+ * @return T
+ */
+ public function visitNullExpression(NullExpression $node);
+
+ /**
+ * @return T
+ */
+ public function visitNumberExpression(NumberExpression $node);
+
+ /**
+ * @return T
+ */
+ public function visitParenthesizedExpression(ParenthesizedExpression $node);
+
+ /**
+ * @return T
+ */
+ public function visitSelectorExpression(SelectorExpression $node);
+
+ /**
+ * @return T
+ */
+ public function visitStringExpression(StringExpression $node);
+
+ /**
+ * @return T
+ */
+ public function visitSupportsExpression(SupportsExpression $node);
+
+ /**
+ * @return T
+ */
+ public function visitUnaryOperationExpression(UnaryOperationExpression $node);
+
+ /**
+ * @return T
+ */
+ public function visitValueExpression(ValueExpression $node);
+
+ /**
+ * @return T
+ */
+ public function visitVariableExpression(VariableExpression $node);
+}
diff --git a/vendor/scssphp/scssphp/src/Visitor/ModifiableCssVisitor.php b/vendor/scssphp/scssphp/src/Visitor/ModifiableCssVisitor.php
new file mode 100644
index 000000000..3e7fb9095
--- /dev/null
+++ b/vendor/scssphp/scssphp/src/Visitor/ModifiableCssVisitor.php
@@ -0,0 +1,78 @@
+<?php
+
+/**
+ * SCSSPHP
+ *
+ * @copyright 2012-2020 Leaf Corcoran
+ *
+ * @license http://opensource.org/licenses/MIT MIT
+ *
+ * @link http://scssphp.github.io/scssphp
+ */
+
+namespace ScssPhp\ScssPhp\Visitor;
+
+use ScssPhp\ScssPhp\Ast\Css\ModifiableCssAtRule;
+use ScssPhp\ScssPhp\Ast\Css\ModifiableCssComment;
+use ScssPhp\ScssPhp\Ast\Css\ModifiableCssDeclaration;
+use ScssPhp\ScssPhp\Ast\Css\ModifiableCssImport;
+use ScssPhp\ScssPhp\Ast\Css\ModifiableCssKeyframeBlock;
+use ScssPhp\ScssPhp\Ast\Css\ModifiableCssMediaRule;
+use ScssPhp\ScssPhp\Ast\Css\ModifiableCssStyleRule;
+use ScssPhp\ScssPhp\Ast\Css\ModifiableCssStylesheet;
+use ScssPhp\ScssPhp\Ast\Css\ModifiableCssSupportsRule;
+
+/**
+ * An interface for visitors that traverse CSS statements.
+ *
+ * @internal
+ *
+ * @template T
+ */
+interface ModifiableCssVisitor
+{
+ /**
+ * @return T
+ */
+ public function visitCssAtRule(ModifiableCssAtRule $node);
+
+ /**
+ * @return T
+ */
+ public function visitCssComment(ModifiableCssComment $node);
+
+ /**
+ * @return T
+ */
+ public function visitCssDeclaration(ModifiableCssDeclaration $node);
+
+ /**
+ * @return T
+ */
+ public function visitCssImport(ModifiableCssImport $node);
+
+ /**
+ * @return T
+ */
+ public function visitCssKeyframeBlock(ModifiableCssKeyframeBlock $node);
+
+ /**
+ * @return T
+ */
+ public function visitCssMediaRule(ModifiableCssMediaRule $node);
+
+ /**
+ * @return T
+ */
+ public function visitCssStyleRule(ModifiableCssStyleRule $node);
+
+ /**
+ * @return T
+ */
+ public function visitCssStylesheet(ModifiableCssStylesheet $node);
+
+ /**
+ * @return T
+ */
+ public function visitCssSupportsRule(ModifiableCssSupportsRule $node);
+}
diff --git a/vendor/scssphp/scssphp/src/Visitor/ReplaceExpressionVisitor.php b/vendor/scssphp/scssphp/src/Visitor/ReplaceExpressionVisitor.php
new file mode 100644
index 000000000..4848302de
--- /dev/null
+++ b/vendor/scssphp/scssphp/src/Visitor/ReplaceExpressionVisitor.php
@@ -0,0 +1,216 @@
+<?php
+
+/**
+ * SCSSPHP
+ *
+ * @copyright 2012-2020 Leaf Corcoran
+ *
+ * @license http://opensource.org/licenses/MIT MIT
+ *
+ * @link http://scssphp.github.io/scssphp
+ */
+
+namespace ScssPhp\ScssPhp\Visitor;
+
+use ScssPhp\ScssPhp\Ast\Sass\ArgumentInvocation;
+use ScssPhp\ScssPhp\Ast\Sass\Expression;
+use ScssPhp\ScssPhp\Ast\Sass\Expression\BinaryOperationExpression;
+use ScssPhp\ScssPhp\Ast\Sass\Expression\BooleanExpression;
+use ScssPhp\ScssPhp\Ast\Sass\Expression\ColorExpression;
+use ScssPhp\ScssPhp\Ast\Sass\Expression\FunctionExpression;
+use ScssPhp\ScssPhp\Ast\Sass\Expression\IfExpression;
+use ScssPhp\ScssPhp\Ast\Sass\Expression\InterpolatedFunctionExpression;
+use ScssPhp\ScssPhp\Ast\Sass\Expression\ListExpression;
+use ScssPhp\ScssPhp\Ast\Sass\Expression\MapExpression;
+use ScssPhp\ScssPhp\Ast\Sass\Expression\NullExpression;
+use ScssPhp\ScssPhp\Ast\Sass\Expression\NumberExpression;
+use ScssPhp\ScssPhp\Ast\Sass\Expression\ParenthesizedExpression;
+use ScssPhp\ScssPhp\Ast\Sass\Expression\SelectorExpression;
+use ScssPhp\ScssPhp\Ast\Sass\Expression\StringExpression;
+use ScssPhp\ScssPhp\Ast\Sass\Expression\SupportsExpression;
+use ScssPhp\ScssPhp\Ast\Sass\Expression\UnaryOperationExpression;
+use ScssPhp\ScssPhp\Ast\Sass\Expression\ValueExpression;
+use ScssPhp\ScssPhp\Ast\Sass\Expression\VariableExpression;
+use ScssPhp\ScssPhp\Ast\Sass\Interpolation;
+use ScssPhp\ScssPhp\Ast\Sass\SupportsCondition;
+use ScssPhp\ScssPhp\Ast\Sass\SupportsCondition\SupportsDeclaration;
+use ScssPhp\ScssPhp\Ast\Sass\SupportsCondition\SupportsInterpolation;
+use ScssPhp\ScssPhp\Ast\Sass\SupportsCondition\SupportsNegation;
+use ScssPhp\ScssPhp\Ast\Sass\SupportsCondition\SupportsOperation;
+
+/**
+ * A visitor that recursively traverses each expression in a SassScript AST and
+ * replaces its contents with the values returned by nested recursion.
+ *
+ * In addition to the methods from {@see ExpressionVisitor}, this has more general
+ * protected methods that can be overridden to add behavior for a wide variety
+ * of AST nodes:
+ *
+ * * {@see visitArgumentInvocation}
+ * * {@see visitSupportsCondition}
+ * * {@see visitInterpolation}
+ *
+ * @template-implements ExpressionVisitor<Expression>
+ *
+ * @internal
+ */
+abstract class ReplaceExpressionVisitor implements ExpressionVisitor
+{
+ public function visitBinaryOperationExpression(BinaryOperationExpression $node): Expression
+ {
+ return new BinaryOperationExpression($node->getOperator(), $node->getLeft()->accept($this), $node->getRight()->accept($this));
+ }
+
+ public function visitBooleanExpression(BooleanExpression $node): Expression
+ {
+ return $node;
+ }
+
+ public function visitColorExpression(ColorExpression $node): Expression
+ {
+ return $node;
+ }
+
+ public function visitFunctionExpression(FunctionExpression $node): Expression
+ {
+ return new FunctionExpression(
+ $node->getOriginalName(),
+ $this->visitArgumentInvocation($node->getArguments()),
+ $node->getSpan(),
+ $node->getNamespace()
+ );
+ }
+
+ public function visitInterpolatedFunctionExpression(InterpolatedFunctionExpression $node): Expression
+ {
+ return new InterpolatedFunctionExpression(
+ $this->visitInterpolation($node->getName()),
+ $this->visitArgumentInvocation($node->getArguments()),
+ $node->getSpan()
+ );
+ }
+
+ public function visitIfExpression(IfExpression $node): Expression
+ {
+ return new IfExpression($this->visitArgumentInvocation($node->getArguments()), $node->getSpan());
+ }
+
+ public function visitListExpression(ListExpression $node): Expression
+ {
+ return new ListExpression(
+ array_map(fn(Expression $item) => $item->accept($this), $node->getContents()),
+ $node->getSeparator(),
+ $node->getSpan(),
+ $node->hasBrackets()
+ );
+ }
+
+ public function visitMapExpression(MapExpression $node): Expression
+ {
+ return new MapExpression(
+ array_map(fn(array $pair) => [$pair[0]->accept($this), $pair[1]->accept($this)], $node->getPairs()),
+ $node->getSpan()
+ );
+ }
+
+ public function visitNullExpression(NullExpression $node): Expression
+ {
+ return $node;
+ }
+
+ public function visitNumberExpression(NumberExpression $node): Expression
+ {
+ return $node;
+ }
+
+ public function visitParenthesizedExpression(ParenthesizedExpression $node): Expression
+ {
+ return new ParenthesizedExpression($node->getExpression()->accept($this), $node->getSpan());
+ }
+
+ public function visitSelectorExpression(SelectorExpression $node): Expression
+ {
+ return $node;
+ }
+
+ public function visitStringExpression(StringExpression $node): Expression
+ {
+ return new StringExpression($this->visitInterpolation($node->getText()), $node->hasQuotes());
+ }
+
+ public function visitSupportsExpression(SupportsExpression $node): Expression
+ {
+ return new SupportsExpression($this->visitSupportsCondition($node->getCondition()));
+ }
+
+ public function visitUnaryOperationExpression(UnaryOperationExpression $node): Expression
+ {
+ return new UnaryOperationExpression($node->getOperator(), $node->getOperand()->accept($this), $node->getSpan());
+ }
+
+ public function visitValueExpression(ValueExpression $node): Expression
+ {
+ return $node;
+ }
+
+ public function visitVariableExpression(VariableExpression $node): Expression
+ {
+ return $node;
+ }
+
+ /**
+ * Replaces each expression in an invocation.
+ *
+ * The default implementation of the visit methods calls this to replace any
+ * argument invocation in an expression.
+ */
+ protected function visitArgumentInvocation(ArgumentInvocation $invocation): ArgumentInvocation
+ {
+ return new ArgumentInvocation(
+ array_map(fn(Expression $expression) => $expression->accept($this), $invocation->getPositional()),
+ array_map(fn(Expression $expression) => $expression->accept($this), $invocation->getNamed()),
+ $invocation->getSpan(),
+ $invocation->getRest()?->accept($this),
+ $invocation->getKeywordRest()?->accept($this)
+ );
+ }
+
+ /**
+ * Replaces each expression in $condition.
+ *
+ * The default implementation of the visit methods call this to visit any
+ * {@see SupportsCondition} they encounter.
+ */
+ protected function visitSupportsCondition(SupportsCondition $condition): SupportsCondition
+ {
+ if ($condition instanceof SupportsOperation) {
+ return new SupportsOperation(
+ $this->visitSupportsCondition($condition->getLeft()),
+ $this->visitSupportsCondition($condition->getRight()),
+ $condition->getOperator(),
+ $condition->getSpan()
+ );
+ }
+
+ if ($condition instanceof SupportsNegation) {
+ return new SupportsNegation($this->visitSupportsCondition($condition->getCondition()), $condition->getSpan());
+ }
+
+ if ($condition instanceof SupportsInterpolation) {
+ return new SupportsInterpolation($condition->getExpression()->accept($this), $condition->getSpan());
+ }
+
+ if ($condition instanceof SupportsDeclaration) {
+ return new SupportsDeclaration($condition->getName()->accept($this), $condition->getValue()->accept($this), $condition->getSpan());
+ }
+
+ throw new \UnexpectedValueException('BUG: Unknown SupportsCondition ' . get_class($condition));
+ }
+
+ protected function visitInterpolation(Interpolation $interpolation): Interpolation
+ {
+ return new Interpolation(array_map(function ($node) {
+ return $node instanceof Expression ? $node->accept($this) : $node;
+ }, $interpolation->getContents()), $interpolation->getSpan());
+ }
+}
diff --git a/vendor/scssphp/scssphp/src/Visitor/SelectorSearchVisitor.php b/vendor/scssphp/scssphp/src/Visitor/SelectorSearchVisitor.php
new file mode 100644
index 000000000..0e72133dd
--- /dev/null
+++ b/vendor/scssphp/scssphp/src/Visitor/SelectorSearchVisitor.php
@@ -0,0 +1,92 @@
+<?php
+
+namespace ScssPhp\ScssPhp\Visitor;
+
+use ScssPhp\ScssPhp\Ast\Selector\AttributeSelector;
+use ScssPhp\ScssPhp\Ast\Selector\ClassSelector;
+use ScssPhp\ScssPhp\Ast\Selector\ComplexSelector;
+use ScssPhp\ScssPhp\Ast\Selector\ComplexSelectorComponent;
+use ScssPhp\ScssPhp\Ast\Selector\CompoundSelector;
+use ScssPhp\ScssPhp\Ast\Selector\IDSelector;
+use ScssPhp\ScssPhp\Ast\Selector\ParentSelector;
+use ScssPhp\ScssPhp\Ast\Selector\PlaceholderSelector;
+use ScssPhp\ScssPhp\Ast\Selector\PseudoSelector;
+use ScssPhp\ScssPhp\Ast\Selector\SelectorList;
+use ScssPhp\ScssPhp\Ast\Selector\SimpleSelector;
+use ScssPhp\ScssPhp\Ast\Selector\TypeSelector;
+use ScssPhp\ScssPhp\Ast\Selector\UniversalSelector;
+use ScssPhp\ScssPhp\Util\IterableUtil;
+
+/**
+ * A {@see SelectorVisitor} whose `visit*` methods default to returning `null`, but
+ * which returns the first non-`null` value returned by any method.
+ *
+ * This can be extended to find the first instance of particular nodes in the
+ * AST.
+ *
+ * @template T
+ * @template-implements SelectorVisitor<T|null>
+ *
+ * @internal
+ */
+abstract class SelectorSearchVisitor implements SelectorVisitor
+{
+ public function visitAttributeSelector(AttributeSelector $attribute)
+ {
+ return null;
+ }
+
+ public function visitClassSelector(ClassSelector $klass)
+ {
+ return null;
+ }
+
+ public function visitIDSelector(IDSelector $id)
+ {
+ return null;
+ }
+
+ public function visitParentSelector(ParentSelector $parent)
+ {
+ return null;
+ }
+
+ public function visitPlaceholderSelector(PlaceholderSelector $placeholder)
+ {
+ return null;
+ }
+
+ public function visitTypeSelector(TypeSelector $type)
+ {
+ return null;
+ }
+
+ public function visitUniversalSelector(UniversalSelector $universal)
+ {
+ return null;
+ }
+
+ public function visitComplexSelector(ComplexSelector $complex)
+ {
+ return IterableUtil::search($complex->getComponents(), fn(ComplexSelectorComponent $component) => $this->visitCompoundSelector($component->getSelector()));
+ }
+
+ public function visitCompoundSelector(CompoundSelector $compound)
+ {
+ return IterableUtil::search($compound->getComponents(), fn(SimpleSelector $simple) => $simple->accept($this));
+ }
+
+ public function visitPseudoSelector(PseudoSelector $pseudo)
+ {
+ if ($pseudo->getSelector() !== null) {
+ return $this->visitSelectorList($pseudo->getSelector());
+ }
+
+ return null;
+ }
+
+ public function visitSelectorList(SelectorList $list)
+ {
+ return IterableUtil::search($list->getComponents(), $this->visitComplexSelector(...));
+ }
+}
diff --git a/vendor/scssphp/scssphp/src/Visitor/SelectorVisitor.php b/vendor/scssphp/scssphp/src/Visitor/SelectorVisitor.php
new file mode 100644
index 000000000..3deb9e67f
--- /dev/null
+++ b/vendor/scssphp/scssphp/src/Visitor/SelectorVisitor.php
@@ -0,0 +1,90 @@
+<?php
+
+/**
+ * SCSSPHP
+ *
+ * @copyright 2012-2020 Leaf Corcoran
+ *
+ * @license http://opensource.org/licenses/MIT MIT
+ *
+ * @link http://scssphp.github.io/scssphp
+ */
+
+namespace ScssPhp\ScssPhp\Visitor;
+
+use ScssPhp\ScssPhp\Ast\Selector\AttributeSelector;
+use ScssPhp\ScssPhp\Ast\Selector\ClassSelector;
+use ScssPhp\ScssPhp\Ast\Selector\ComplexSelector;
+use ScssPhp\ScssPhp\Ast\Selector\CompoundSelector;
+use ScssPhp\ScssPhp\Ast\Selector\IDSelector;
+use ScssPhp\ScssPhp\Ast\Selector\ParentSelector;
+use ScssPhp\ScssPhp\Ast\Selector\PlaceholderSelector;
+use ScssPhp\ScssPhp\Ast\Selector\PseudoSelector;
+use ScssPhp\ScssPhp\Ast\Selector\SelectorList;
+use ScssPhp\ScssPhp\Ast\Selector\TypeSelector;
+use ScssPhp\ScssPhp\Ast\Selector\UniversalSelector;
+
+/**
+ * An interface for visitors that traverse selectors.
+ *
+ * @internal
+ *
+ * @template T
+ */
+interface SelectorVisitor
+{
+ /**
+ * @return T
+ */
+ public function visitAttributeSelector(AttributeSelector $attribute);
+
+ /**
+ * @return T
+ */
+ public function visitClassSelector(ClassSelector $klass);
+
+ /**
+ * @return T
+ */
+ public function visitComplexSelector(ComplexSelector $complex);
+
+ /**
+ * @return T
+ */
+ public function visitCompoundSelector(CompoundSelector $compound);
+
+ /**
+ * @return T
+ */
+ public function visitIDSelector(IDSelector $id);
+
+ /**
+ * @return T
+ */
+ public function visitParentSelector(ParentSelector $parent);
+
+ /**
+ * @return T
+ */
+ public function visitPlaceholderSelector(PlaceholderSelector $placeholder);
+
+ /**
+ * @return T
+ */
+ public function visitPseudoSelector(PseudoSelector $pseudo);
+
+ /**
+ * @return T
+ */
+ public function visitSelectorList(SelectorList $list);
+
+ /**
+ * @return T
+ */
+ public function visitTypeSelector(TypeSelector $type);
+
+ /**
+ * @return T
+ */
+ public function visitUniversalSelector(UniversalSelector $universal);
+}
diff --git a/vendor/scssphp/scssphp/src/Visitor/StatementSearchVisitor.php b/vendor/scssphp/scssphp/src/Visitor/StatementSearchVisitor.php
new file mode 100644
index 000000000..1cb011f00
--- /dev/null
+++ b/vendor/scssphp/scssphp/src/Visitor/StatementSearchVisitor.php
@@ -0,0 +1,230 @@
+<?php
+
+/**
+ * SCSSPHP
+ *
+ * @copyright 2012-2020 Leaf Corcoran
+ *
+ * @license http://opensource.org/licenses/MIT MIT
+ *
+ * @link http://scssphp.github.io/scssphp
+ */
+
+namespace ScssPhp\ScssPhp\Visitor;
+
+use ScssPhp\ScssPhp\Ast\Sass\Statement;
+use ScssPhp\ScssPhp\Ast\Sass\Statement\AtRootRule;
+use ScssPhp\ScssPhp\Ast\Sass\Statement\AtRule;
+use ScssPhp\ScssPhp\Ast\Sass\Statement\CallableDeclaration;
+use ScssPhp\ScssPhp\Ast\Sass\Statement\ContentBlock;
+use ScssPhp\ScssPhp\Ast\Sass\Statement\ContentRule;
+use ScssPhp\ScssPhp\Ast\Sass\Statement\DebugRule;
+use ScssPhp\ScssPhp\Ast\Sass\Statement\Declaration;
+use ScssPhp\ScssPhp\Ast\Sass\Statement\EachRule;
+use ScssPhp\ScssPhp\Ast\Sass\Statement\ErrorRule;
+use ScssPhp\ScssPhp\Ast\Sass\Statement\ExtendRule;
+use ScssPhp\ScssPhp\Ast\Sass\Statement\ForRule;
+use ScssPhp\ScssPhp\Ast\Sass\Statement\FunctionRule;
+use ScssPhp\ScssPhp\Ast\Sass\Statement\IfClause;
+use ScssPhp\ScssPhp\Ast\Sass\Statement\IfRule;
+use ScssPhp\ScssPhp\Ast\Sass\Statement\ImportRule;
+use ScssPhp\ScssPhp\Ast\Sass\Statement\IncludeRule;
+use ScssPhp\ScssPhp\Ast\Sass\Statement\LoudComment;
+use ScssPhp\ScssPhp\Ast\Sass\Statement\MediaRule;
+use ScssPhp\ScssPhp\Ast\Sass\Statement\MixinRule;
+use ScssPhp\ScssPhp\Ast\Sass\Statement\ParentStatement;
+use ScssPhp\ScssPhp\Ast\Sass\Statement\ReturnRule;
+use ScssPhp\ScssPhp\Ast\Sass\Statement\SilentComment;
+use ScssPhp\ScssPhp\Ast\Sass\Statement\StyleRule;
+use ScssPhp\ScssPhp\Ast\Sass\Statement\Stylesheet;
+use ScssPhp\ScssPhp\Ast\Sass\Statement\SupportsRule;
+use ScssPhp\ScssPhp\Ast\Sass\Statement\VariableDeclaration;
+use ScssPhp\ScssPhp\Ast\Sass\Statement\WarnRule;
+use ScssPhp\ScssPhp\Ast\Sass\Statement\WhileRule;
+use ScssPhp\ScssPhp\Util\IterableUtil;
+
+/**
+ * A StatementVisitor whose `visit*` methods default to returning `null`, but
+ * which returns the first non-`null` value returned by any method.
+ *
+ * This can be extended to find the first instance of particular nodes in the
+ * AST.
+ *
+ * @internal
+ *
+ * @template T
+ * @template-implements StatementVisitor<T|null>
+ */
+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 = IterableUtil::search($node->getClauses(), fn(IfClause $clause) => IterableUtil::search($clause->getChildren(), fn(Statement $child) => $child->accept($this)));
+
+ if ($node->getLastClause() !== null) {
+ $value ??= IterableUtil::search($node->getLastClause()->getChildren(), fn(Statement $child) => $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)
+ {
+ return IterableUtil::search($children, fn (Statement $child) => $child->accept($this));
+ }
+}
diff --git a/vendor/scssphp/scssphp/src/Visitor/StatementVisitor.php b/vendor/scssphp/scssphp/src/Visitor/StatementVisitor.php
new file mode 100644
index 000000000..56342345f
--- /dev/null
+++ b/vendor/scssphp/scssphp/src/Visitor/StatementVisitor.php
@@ -0,0 +1,174 @@
+<?php
+
+/**
+ * SCSSPHP
+ *
+ * @copyright 2012-2020 Leaf Corcoran
+ *
+ * @license http://opensource.org/licenses/MIT MIT
+ *
+ * @link http://scssphp.github.io/scssphp
+ */
+
+namespace ScssPhp\ScssPhp\Visitor;
+
+use ScssPhp\ScssPhp\Ast\Sass\Statement\AtRootRule;
+use ScssPhp\ScssPhp\Ast\Sass\Statement\AtRule;
+use ScssPhp\ScssPhp\Ast\Sass\Statement\ContentBlock;
+use ScssPhp\ScssPhp\Ast\Sass\Statement\ContentRule;
+use ScssPhp\ScssPhp\Ast\Sass\Statement\DebugRule;
+use ScssPhp\ScssPhp\Ast\Sass\Statement\Declaration;
+use ScssPhp\ScssPhp\Ast\Sass\Statement\EachRule;
+use ScssPhp\ScssPhp\Ast\Sass\Statement\ErrorRule;
+use ScssPhp\ScssPhp\Ast\Sass\Statement\ExtendRule;
+use ScssPhp\ScssPhp\Ast\Sass\Statement\ForRule;
+use ScssPhp\ScssPhp\Ast\Sass\Statement\FunctionRule;
+use ScssPhp\ScssPhp\Ast\Sass\Statement\IfRule;
+use ScssPhp\ScssPhp\Ast\Sass\Statement\ImportRule;
+use ScssPhp\ScssPhp\Ast\Sass\Statement\IncludeRule;
+use ScssPhp\ScssPhp\Ast\Sass\Statement\LoudComment;
+use ScssPhp\ScssPhp\Ast\Sass\Statement\MediaRule;
+use ScssPhp\ScssPhp\Ast\Sass\Statement\MixinRule;
+use ScssPhp\ScssPhp\Ast\Sass\Statement\ReturnRule;
+use ScssPhp\ScssPhp\Ast\Sass\Statement\SilentComment;
+use ScssPhp\ScssPhp\Ast\Sass\Statement\StyleRule;
+use ScssPhp\ScssPhp\Ast\Sass\Statement\Stylesheet;
+use ScssPhp\ScssPhp\Ast\Sass\Statement\SupportsRule;
+use ScssPhp\ScssPhp\Ast\Sass\Statement\VariableDeclaration;
+use ScssPhp\ScssPhp\Ast\Sass\Statement\WarnRule;
+use ScssPhp\ScssPhp\Ast\Sass\Statement\WhileRule;
+
+/**
+ * An interface for visitors that traverse SassScript statements.
+ *
+ * @internal
+ *
+ * @template T
+ */
+interface StatementVisitor
+{
+ /**
+ * @return T
+ */
+ public function visitAtRootRule(AtRootRule $node);
+
+ /**
+ * @return T
+ */
+ public function visitAtRule(AtRule $node);
+
+ /**
+ * @return T
+ */
+ public function visitContentBlock(ContentBlock $node);
+
+ /**
+ * @return T
+ */
+ public function visitContentRule(ContentRule $node);
+
+ /**
+ * @return T
+ */
+ public function visitDebugRule(DebugRule $node);
+
+ /**
+ * @return T
+ */
+ public function visitDeclaration(Declaration $node);
+
+ /**
+ * @return T
+ */
+ public function visitEachRule(EachRule $node);
+
+ /**
+ * @return T
+ */
+ public function visitErrorRule(ErrorRule $node);
+
+ /**
+ * @return T
+ */
+ public function visitExtendRule(ExtendRule $node);
+
+ /**
+ * @return T
+ */
+ public function visitForRule(ForRule $node);
+
+ /**
+ * @return T
+ */
+ public function visitFunctionRule(FunctionRule $node);
+
+ /**
+ * @return T
+ */
+ public function visitIfRule(IfRule $node);
+
+ /**
+ * @return T
+ */
+ public function visitImportRule(ImportRule $node);
+
+ /**
+ * @return T
+ */
+ public function visitIncludeRule(IncludeRule $node);
+
+ /**
+ * @return T
+ */
+ public function visitLoudComment(LoudComment $node);
+
+ /**
+ * @return T
+ */
+ public function visitMediaRule(MediaRule $node);
+
+ /**
+ * @return T
+ */
+ public function visitMixinRule(MixinRule $node);
+
+ /**
+ * @return T
+ */
+ public function visitReturnRule(ReturnRule $node);
+
+ /**
+ * @return T
+ */
+ public function visitSilentComment(SilentComment $node);
+
+ /**
+ * @return T
+ */
+ public function visitStyleRule(StyleRule $node);
+
+ /**
+ * @return T
+ */
+ public function visitStylesheet(Stylesheet $node);
+
+ /**
+ * @return T
+ */
+ public function visitSupportsRule(SupportsRule $node);
+
+ /**
+ * @return T
+ */
+ public function visitVariableDeclaration(VariableDeclaration $node);
+
+ /**
+ * @return T
+ */
+ public function visitWarnRule(WarnRule $node);
+
+ /**
+ * @return T
+ */
+ public function visitWhileRule(WhileRule $node);
+}
diff --git a/vendor/scssphp/scssphp/src/Visitor/ValueVisitor.php b/vendor/scssphp/scssphp/src/Visitor/ValueVisitor.php
new file mode 100644
index 000000000..3042c9e23
--- /dev/null
+++ b/vendor/scssphp/scssphp/src/Visitor/ValueVisitor.php
@@ -0,0 +1,83 @@
+<?php
+
+/**
+ * SCSSPHP
+ *
+ * @copyright 2012-2020 Leaf Corcoran
+ *
+ * @license http://opensource.org/licenses/MIT MIT
+ *
+ * @link http://scssphp.github.io/scssphp
+ */
+
+namespace ScssPhp\ScssPhp\Visitor;
+
+use ScssPhp\ScssPhp\Value\SassBoolean;
+use ScssPhp\ScssPhp\Value\SassCalculation;
+use ScssPhp\ScssPhp\Value\SassColor;
+use ScssPhp\ScssPhp\Value\SassFunction;
+use ScssPhp\ScssPhp\Value\SassList;
+use ScssPhp\ScssPhp\Value\SassMap;
+use ScssPhp\ScssPhp\Value\SassMixin;
+use ScssPhp\ScssPhp\Value\SassNumber;
+use ScssPhp\ScssPhp\Value\SassString;
+
+/**
+ * An interface for visitors that traverse SassScript $values.
+ *
+ * @internal
+ *
+ * @template T
+ */
+interface ValueVisitor
+{
+ /**
+ * @return T
+ */
+ public function visitBoolean(SassBoolean $value);
+
+ /**
+ * @return T
+ */
+ public function visitCalculation(SassCalculation $value);
+
+ /**
+ * @return T
+ */
+ public function visitColor(SassColor $value);
+
+ /**
+ * @return T
+ */
+ public function visitFunction(SassFunction $value);
+
+ /**
+ * @return T
+ */
+ public function visitMixin(SassMixin $value);
+
+ /**
+ * @return T
+ */
+ public function visitList(SassList $value);
+
+ /**
+ * @return T
+ */
+ public function visitMap(SassMap $value);
+
+ /**
+ * @return T
+ */
+ public function visitNull();
+
+ /**
+ * @return T
+ */
+ public function visitNumber(SassNumber $value);
+
+ /**
+ * @return T
+ */
+ public function visitString(SassString $value);
+}
diff --git a/vendor/scssphp/scssphp/src/Warn.php b/vendor/scssphp/scssphp/src/Warn.php
index 592b44c70..1641c2f8a 100644
--- a/vendor/scssphp/scssphp/src/Warn.php
+++ b/vendor/scssphp/scssphp/src/Warn.php
@@ -12,73 +12,37 @@
namespace ScssPhp\ScssPhp;
+use ScssPhp\ScssPhp\Evaluation\EvaluationContext;
+
final class Warn
{
/**
- * @var callable|null
- * @phpstan-var (callable(string, bool): void)|null
- */
- private static $callback;
-
- /**
* Prints a warning message associated with the current `@import` or function call.
*
* This may only be called within a custom function or importer callback.
- *
- * @param string $message
- *
- * @return void
*/
- public static function warning($message)
+ public static function warning(string $message): void
{
- self::reportWarning($message, false);
+ self::reportWarning($message, null);
}
/**
* Prints a deprecation warning message associated with the current `@import` or function call.
*
* This may only be called within a custom function or importer callback.
- *
- * @param string $message
- *
- * @return void
*/
- public static function deprecation($message)
+ public static function deprecation(string $message): void
{
- self::reportWarning($message, true);
+ self::reportWarning($message, Deprecation::userAuthored);
}
- /**
- * @param callable|null $callback
- *
- * @return callable|null The previous warn callback
- *
- * @phpstan-param (callable(string, bool): void)|null $callback
- *
- * @phpstan-return (callable(string, bool): void)|null
- *
- * @internal
- */
- public static function setCallback(callable $callback = null)
+ public static function forDeprecation(string $message, Deprecation $deprecation): void
{
- $previousCallback = self::$callback;
- self::$callback = $callback;
-
- return $previousCallback;
+ self::reportWarning($message, $deprecation);
}
- /**
- * @param string $message
- * @param bool $deprecation
- *
- * @return void
- */
- private static function reportWarning($message, $deprecation)
+ private static function reportWarning(string $message, ?Deprecation $deprecation): void
{
- if (self::$callback === null) {
- throw new \BadMethodCallException('The warning Reporter may only be called within a custom function or importer callback.');
- }
-
- \call_user_func(self::$callback, $message, $deprecation);
+ EvaluationContext::getCurrent()->warn($message, $deprecation);
}
}
diff --git a/vendor/scssphp/source-span/LICENSE.md b/vendor/scssphp/source-span/LICENSE.md
new file mode 100644
index 000000000..4b352a1f9
--- /dev/null
+++ b/vendor/scssphp/source-span/LICENSE.md
@@ -0,0 +1,20 @@
+Copyright (c) 2024-present, the Scssphp project authors
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
diff --git a/vendor/scssphp/source-span/README.md b/vendor/scssphp/source-span/README.md
new file mode 100644
index 000000000..d7a7524e5
--- /dev/null
+++ b/vendor/scssphp/source-span/README.md
@@ -0,0 +1,22 @@
+# SourceSpan
+
+`scssphp/source-span` is a library for tracking locations in source code. It's designed
+to provide a standard representation for source code locations and spans so that
+disparate packages can easily pass them among one another, and to make it easy
+to generate human-friendly messages associated with a given piece of code.
+
+The most commonly-used interface is the package's namesake, `SourceSpan\SourceSpan`. It
+represents a span of characters in some source file, and is often attached to an
+object that has been parsed to indicate where it was parsed from. It provides
+access to the text of the span via `SourceSpan::getText()` and can be used to produce
+human-friendly messages using `SourceSpan::message()`. It's most simple implementation
+is `SourceSpan\SimpleSourceSpan` which holds directly the span information.
+
+When parsing code from a file, `SourceSpan\SourceFile` is useful. Not only does it provide
+an efficient means of computing line and column numbers, `SourceFile#span()`
+returns special `FileSpan`s that are able to provide more context for their
+error messages.
+
+## Credits
+
+This library is a PHP port of the [Dart `source_span` package](https://github.com/dart-lang/source_span).
diff --git a/vendor/scssphp/source-span/composer.json b/vendor/scssphp/source-span/composer.json
new file mode 100644
index 000000000..d76e32f58
--- /dev/null
+++ b/vendor/scssphp/source-span/composer.json
@@ -0,0 +1,42 @@
+{
+ "name": "scssphp/source-span",
+ "type": "library",
+ "description": "Provides a representation for source code locations and spans.",
+ "keywords": ["parsing"],
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Christophe Coevoet",
+ "homepage": "https://github.com/stof"
+ }
+ ],
+ "autoload": {
+ "psr-4": { "SourceSpan\\": "src/" }
+ },
+ "autoload-dev": {
+ "psr-4": { "SourceSpan\\Tests\\": "tests/" }
+ },
+ "require": {
+ "php": ">=8.1",
+ "league/uri": "^7.4",
+ "league/uri-interfaces": "^7.4"
+ },
+ "require-dev": {
+ "phpstan/phpstan": "^2.0",
+ "phpstan/phpstan-deprecation-rules": "^2.0",
+ "phpunit/phpunit": "^9.5.6",
+ "squizlabs/php_codesniffer": "~3.5",
+ "symfony/phpunit-bridge": "^5.1",
+ "symfony/var-dumper": "^6.3"
+ },
+ "extra": {
+ "branch-alias": {
+ "dev-main": "1.x-dev"
+ }
+ },
+ "config": {
+ "sort-packages": true
+ }
+}
diff --git a/vendor/scssphp/source-span/src/ConcreteFileSpan.php b/vendor/scssphp/source-span/src/ConcreteFileSpan.php
new file mode 100644
index 000000000..43a0bc149
--- /dev/null
+++ b/vendor/scssphp/source-span/src/ConcreteFileSpan.php
@@ -0,0 +1,156 @@
+<?php
+
+namespace SourceSpan;
+
+use League\Uri\Contracts\UriInterface;
+
+/**
+ * The implementation of {@see FileSpan} based on a {@see SourceFile}.
+ *
+ * @see SourceFile::span()
+ *
+ * @internal
+ */
+final class ConcreteFileSpan extends SourceSpanMixin implements FileSpan
+{
+ /**
+ * @param int $start The offset of the beginning of the span.
+ * @param int $end The offset of the end of the span.
+ */
+ public function __construct(
+ private readonly SourceFile $file,
+ private readonly int $start,
+ private readonly int $end,
+ ) {
+ if ($this->end < $this->start) {
+ throw new \InvalidArgumentException("End $this->end must come after start $this->start.");
+ }
+
+ if ($this->end > $this->file->getLength()) {
+ throw new \OutOfRangeException("End $this->end not be greater than the number of characters in the file, {$this->file->getLength()}.");
+ }
+
+ if ($this->start < 0) {
+ throw new \OutOfRangeException("Start may not be negative, was $this->start.");
+ }
+ }
+
+ public function getFile(): SourceFile
+ {
+ return $this->file;
+ }
+
+ public function getSourceUrl(): ?UriInterface
+ {
+ return $this->file->getSourceUrl();
+ }
+
+ public function getLength(): int
+ {
+ return $this->end - $this->start;
+ }
+
+ public function getStart(): FileLocation
+ {
+ return new FileLocation($this->file, $this->start);
+ }
+
+ public function getEnd(): FileLocation
+ {
+ return new FileLocation($this->file, $this->end);
+ }
+
+ public function getText(): string
+ {
+ return $this->file->getText($this->start, $this->end);
+ }
+
+ public function getContext(): string
+ {
+ $endLine = $this->file->getLine($this->end);
+ $endColumn = $this->file->getColumn($this->end);
+
+ if ($endColumn === 0 && $endLine !== 0) {
+ // If $this->end is at the very beginning of the line, the span covers the
+ // previous newline, so we only want to include the previous line in the
+ // context...
+
+ if ($this->getLength() === 0) {
+ // ...unless this is a point span, in which case we want to include the
+ // next line (or the empty string if this is the end of the file).
+ return $endLine === $this->file->getLines() - 1 ? '' : $this->file->getText($this->file->getOffset($endLine), $this->file->getOffset($endLine + 1));
+ }
+
+ $endOffset = $this->end;
+ } elseif ($endLine === $this->file->getLines() - 1) {
+ // If the span covers the last line of the file, the context should go all
+ // the way to the end of the file.
+ $endOffset = $this->file->getLength();
+ } else {
+ // Otherwise, the context should cover the full line on which [end]
+ // appears.
+ $endOffset = $this->file->getOffset($endLine + 1);
+ }
+
+ return $this->file->getText($this->file->getOffset($this->file->getLine($this->start)), $endOffset);
+ }
+
+ public function compareTo(SourceSpan $other): int
+ {
+ if (!$other instanceof ConcreteFileSpan) {
+ return parent::compareTo($other);
+ }
+
+ $result = $this->start <=> $other->start;
+
+ if ($result !== 0) {
+ return $result;
+ }
+
+ return $this->end <=> $other->end;
+ }
+
+ public function union(SourceSpan $other): SourceSpan
+ {
+ if (!$other instanceof FileSpan) {
+ return parent::union($other);
+ }
+
+ $span = $this->expand($other);
+
+ if ($other instanceof ConcreteFileSpan) {
+ if ($this->start > $other->end || $other->start > $this->end) {
+ throw new \InvalidArgumentException("Spans are disjoint.");
+ }
+ } else {
+ if ($this->start > $other->getEnd()->getOffset() || $other->getStart()->getOffset() > $this->end) {
+ throw new \InvalidArgumentException("Spans are disjoint.");
+ }
+ }
+
+ return $span;
+ }
+
+ public function expand(FileSpan $other): FileSpan
+ {
+ if ($this->file->getSourceUrl() !== $other->getFile()->getSourceUrl()) {
+ throw new \InvalidArgumentException('Source map URLs don\'t match.');
+ }
+
+ $start = min($this->start, $other->getStart()->getOffset());
+ $end = max($this->end, $other->getEnd()->getOffset());
+
+ return new ConcreteFileSpan($this->file, $start, $end);
+ }
+
+ public function subspan(int $start, ?int $end = null): FileSpan
+ {
+ Util::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/vendor/scssphp/source-span/src/FileLocation.php b/vendor/scssphp/source-span/src/FileLocation.php
new file mode 100644
index 000000000..8028196c6
--- /dev/null
+++ b/vendor/scssphp/source-span/src/FileLocation.php
@@ -0,0 +1,52 @@
+<?php
+
+namespace SourceSpan;
+
+use League\Uri\Contracts\UriInterface;
+
+/**
+ * The implementation of {@see SourceLocation} based on a {@see SourceFile}.
+ *
+ * @see SourceFile::location()
+ */
+final class FileLocation extends SourceLocationMixin
+{
+ /**
+ * @internal
+ */
+ public function __construct(
+ private readonly SourceFile $file,
+ private readonly int $offset,
+ ) {
+ }
+
+ public function getFile(): SourceFile
+ {
+ return $this->file;
+ }
+
+ public function getOffset(): int
+ {
+ return $this->offset;
+ }
+
+ public function getLine(): int
+ {
+ return $this->file->getLine($this->offset);
+ }
+
+ public function getColumn(): int
+ {
+ return $this->file->getColumn($this->offset);
+ }
+
+ public function getSourceUrl(): ?UriInterface
+ {
+ return $this->file->getSourceUrl();
+ }
+
+ public function pointSpan(): FileSpan
+ {
+ return new ConcreteFileSpan($this->file, $this->offset, $this->offset);
+ }
+}
diff --git a/vendor/scssphp/source-span/src/FileSpan.php b/vendor/scssphp/source-span/src/FileSpan.php
new file mode 100644
index 000000000..0ff936844
--- /dev/null
+++ b/vendor/scssphp/source-span/src/FileSpan.php
@@ -0,0 +1,20 @@
+<?php
+
+namespace SourceSpan;
+
+interface FileSpan extends SourceSpanWithContext
+{
+ public function getFile(): SourceFile;
+
+ public function getStart(): FileLocation;
+
+ public function getEnd(): FileLocation;
+
+ public function expand(FileSpan $other): FileSpan;
+
+ /**
+ * 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;
+}
diff --git a/vendor/scssphp/source-span/src/Highlighter/AsciiGlyph.php b/vendor/scssphp/source-span/src/Highlighter/AsciiGlyph.php
new file mode 100644
index 000000000..70af345a4
--- /dev/null
+++ b/vendor/scssphp/source-span/src/Highlighter/AsciiGlyph.php
@@ -0,0 +1,18 @@
+<?php
+
+namespace SourceSpan\Highlighter;
+
+/**
+ * @internal
+ */
+final class AsciiGlyph
+{
+ public const horizontalLine = '-';
+ public const verticalLine = '|';
+ public const topLeftCorner = ',';
+ public const bottomLeftCorner = "'";
+ public const cross = '+';
+ public const upEnd = "'";
+ public const downEnd = ',';
+ public const horizontalLineBold = '=';
+}
diff --git a/vendor/scssphp/source-span/src/Highlighter/Highlight.php b/vendor/scssphp/source-span/src/Highlighter/Highlight.php
new file mode 100644
index 000000000..16f9ceaea
--- /dev/null
+++ b/vendor/scssphp/source-span/src/Highlighter/Highlight.php
@@ -0,0 +1,205 @@
+<?php
+
+namespace SourceSpan\Highlighter;
+
+use SourceSpan\SimpleSourceLocation;
+use SourceSpan\SimpleSourceSpanWithContext;
+use SourceSpan\SourceSpan;
+use SourceSpan\SourceSpanWithContext;
+use SourceSpan\Util;
+
+/**
+ * Information about how to highlight a single section of a source file.
+ *
+ * @internal
+ */
+final class Highlight
+{
+ /**
+ * The section of the source file to highlight.
+ *
+ * This is normalized to make it easier for {@see Highlighter} to work with.
+ */
+ public readonly SourceSpanWithContext $span;
+
+ /**
+ * The label to include inline when highlighting {@see $span}.
+ *
+ * This helps distinguish clarify what each highlight means when multiple are
+ * used in the same message.
+ */
+ public readonly ?string $label;
+
+ public function __construct(
+ SourceSpan $span,
+ private readonly bool $primary = false,
+ ?string $label = null,
+ ) {
+ $this->span = self::normalizeSpan($span);
+ $this->label = $label === null ? null : str_replace("\r\n", "\n", $label);
+ }
+
+ /**
+ * Whether this is the primary span in the highlight.
+ *
+ * The primary span is highlighted with a different character than
+ * non-primary spans.
+ */
+ public function isPrimary(): bool
+ {
+ return $this->primary;
+ }
+
+ private static function normalizeSpan(SourceSpan $span): SourceSpanWithContext
+ {
+ $newSpan = self::normalizeContext($span);
+ $newSpan = self::normalizeNewlines($newSpan);
+ $newSpan = self::normalizeTrailingNewline($newSpan);
+
+ return self::normalizeEndOfLine($newSpan);
+ }
+
+ /**
+ * Normalizes $span to ensure that it's a {@see SourceSpanWithContext} whose
+ * context actually contains its text at the expected column.
+ *
+ * If it's not already a {@see SourceSpanWithContext}, adjust the start and end
+ * locations' line and column fields so that the highlighter can assume they
+ * match up with the context.
+ */
+ private static function normalizeContext(SourceSpan $span): SourceSpanWithContext
+ {
+ if ($span instanceof SourceSpanWithContext && Util::findLineStart($span->getContext(), $span->getText(), $span->getStart()->getColumn()) !== null) {
+ return $span;
+ }
+
+ return new SimpleSourceSpanWithContext(
+ new SimpleSourceLocation($span->getStart()->getOffset(), $span->getSourceUrl(), 0, 0),
+ new SimpleSourceLocation($span->getEnd()->getOffset(), $span->getSourceUrl(), substr_count($span->getText(), "\n"), self::lastLineLength($span->getText())),
+ $span->getText(),
+ $span->getText()
+ );
+ }
+
+ /**
+ * Normalizes $span to replace Windows-style newlines with Unix-style
+ * newlines.
+ */
+ private static function normalizeNewlines(SourceSpanWithContext $span): SourceSpanWithContext
+ {
+ $text = $span->getText();
+ if (!str_contains($text, "\r\n")) {
+ return $span;
+ }
+
+ $endOffset = $span->getEnd()->getOffset() - substr_count($text, "\r\n");
+
+ return new SimpleSourceSpanWithContext(
+ $span->getStart(),
+ new SimpleSourceLocation($endOffset, $span->getSourceUrl(), $span->getEnd()->getLine(), $span->getEnd()->getColumn()),
+ str_replace("\r\n", "\n", $text),
+ str_replace("\r\n", "\n", $span->getContext())
+ );
+ }
+
+ /**
+ * Normalizes $span to remove a trailing newline from `$span->getContext()`.
+ *
+ * If necessary, also adjust `$span->getEnd()` so that it doesn't point past where
+ * the trailing newline used to be.
+ */
+ private static function normalizeTrailingNewline(SourceSpanWithContext $span): SourceSpanWithContext
+ {
+ if (!str_ends_with($span->getContext(), "\n")) {
+ return $span;
+ }
+
+ // If there's a full blank line on the end of `$span->getContext()`, it's probably
+ // significant, so we shouldn't trim it.
+ if (str_ends_with($span->getText(), "\n\n")) {
+ return $span;
+ }
+
+ $context = substr($span->getContext(), 0, -1);
+ $text = $span->getText();
+ $start = $span->getStart();
+ $end = $span->getEnd();
+
+ if (str_ends_with($text, "\n") && self::isTextAtEndOfContext($span)) {
+ $text = substr($text, 0, -1);
+
+ if ($text === '') {
+ $end = $start;
+ } else {
+ $end = new SimpleSourceLocation(
+ $end->getOffset() - 1,
+ $span->getSourceUrl(),
+ $end->getLine() - 1,
+ self::lastLineLength($context)
+ );
+ $start = $span->getStart()->getOffset() === $span->getEnd()->getOffset() ? $end : $span->getStart();
+ }
+ }
+
+ return new SimpleSourceSpanWithContext($start, $end, $text, $context);
+ }
+
+ /**
+ * Normalizes $span so that the end location is at the end of a line rather
+ * than at the beginning of the next line.
+ */
+ private static function normalizeEndOfLine(SourceSpanWithContext $span): SourceSpanWithContext
+ {
+ if ($span->getEnd()->getColumn() !== 0) {
+ return $span;
+ }
+
+ if ($span->getEnd()->getLine() === $span->getStart()->getLine()) {
+ return $span;
+ }
+
+ $text = substr($span->getText(), 0, -1);
+
+ return new SimpleSourceSpanWithContext(
+ $span->getStart(),
+ new SimpleSourceLocation(
+ $span->getEnd()->getOffset() - 1,
+ $span->getSourceUrl(),
+ $span->getEnd()->getLine() - 1,
+ \strlen($text) - Util::lastIndexOf($text, "\n") - 1
+ ),
+ $text,
+ // If the context also ends with a newline, it's possible that we don't
+ // have the full context for that line, so we shouldn't print it at all.
+ str_ends_with($span->getContext(), "\n") ? substr($span->getContext(), 0, -1) : $span->getContext()
+ );
+ }
+
+ /**
+ * Returns the length of the last line in $text, whether or not it ends in a
+ * newline.
+ */
+ private static function lastLineLength(string $text): int
+ {
+ if ($text === '') {
+ return 0;
+ }
+
+ if ($text[\strlen($text) - 1] === '\n') {
+ return \strlen($text) === 1 ? 0 : \strlen($text) - Util::lastIndexOf($text, "\n", \strlen($text) - 2) - 1;
+ }
+
+ return \strlen($text) - Util::lastIndexOf($text, "\n") - 1;
+ }
+
+ /**
+ * Returns whether $span's text runs all the way to the end of its context.
+ */
+ private static function isTextAtEndOfContext(SourceSpanWithContext $span): bool
+ {
+ $lineStart = Util::findLineStart($span->getContext(), $span->getText(), $span->getStart()->getColumn());
+ \assert($lineStart !== null);
+
+ return $lineStart + $span->getStart()->getColumn() + $span->getLength() === \strlen($span->getContext());
+ }
+}
diff --git a/vendor/scssphp/source-span/src/Highlighter/Highlighter.php b/vendor/scssphp/source-span/src/Highlighter/Highlighter.php
new file mode 100644
index 000000000..b8b7aee24
--- /dev/null
+++ b/vendor/scssphp/source-span/src/Highlighter/Highlighter.php
@@ -0,0 +1,538 @@
+<?php
+
+namespace SourceSpan\Highlighter;
+
+use League\Uri\Contracts\UriInterface;
+use SourceSpan\SourceSpan;
+use SourceSpan\Util;
+
+/**
+ * A class for writing a chunk of text with a particular span highlighted.
+ *
+ * @internal
+ */
+final class Highlighter
+{
+ /**
+ * The number of spaces to render for hard tabs that appear in `_span.text`.
+ *
+ * We don't want to render raw tabs, because they'll mess up our character
+ * alignment.
+ */
+ private const SPACES_PER_TAB = 4;
+
+ /**
+ * The lines to display, including context around the highlighted spans.
+ *
+ * @var list<Line>
+ */
+ private array $lines;
+
+ /**
+ * The number of characters before the bar in the sidebar.
+ */
+ private readonly int $paddingBeforeSidebar;
+
+ /**
+ * The maximum number of multiline spans that cover any part of a single
+ * line in {@see $lines}.
+ */
+ private readonly int $maxMultilineSpans;
+
+ /**
+ * Whether {@see $lines} includes lines from multiple different files.
+ */
+ private readonly bool $multipleFiles;
+
+ /**
+ * The buffer to which to write the result.
+ */
+ private string $buffer = '';
+
+ /**
+ * Creates a {@see Highlighter} that will return a string highlighting $span
+ * within the text of its file when {@see highlight} is called.
+ */
+ public static function create(SourceSpan $span): Highlighter
+ {
+ return new Highlighter(self::collateLines([new Highlight($span, primary: true)]));
+ }
+
+ /**
+ * Creates a {@see Highlighter} that will return a string highlighting
+ * $primarySpan as well as all the spans in $secondarySpans within the text
+ * of their file when {@see highlight} is called.
+ *
+ * Each span has an associated label that will be written alongside it. For
+ * $primarySpan this message is $primaryLabel, and for $secondarySpans the
+ * labels are the map keys.
+ *
+ * @param array<string, SourceSpan> $secondarySpans
+ */
+ public static function multiple(SourceSpan $primarySpan, string $primaryLabel, array $secondarySpans): Highlighter
+ {
+ $highlights = [new Highlight($primarySpan, primary: true, label: $primaryLabel)];
+ foreach ($secondarySpans as $secondaryLabel => $secondarySpan) {
+ $highlights[] = new Highlight($secondarySpan, label: $secondaryLabel);
+ }
+
+ return new Highlighter(self::collateLines($highlights));
+ }
+
+ /**
+ * @param list<Line> $lines
+ */
+ private function __construct(array $lines)
+ {
+ $this->lines = $lines;
+ $this->paddingBeforeSidebar = 1 + max(
+ \strlen((string) (Util::listLast($lines)->number + 1)),
+ // If $lines aren't contiguous, we'll write "..." in place of a
+ // line number.
+ self::contiguous($lines) ? 0 : 3
+ );
+ $this->maxMultilineSpans = array_reduce(array_map(fn (Line $line) => \count(array_filter($line->highlights, fn (Highlight $highlight) => Util::isMultiline($highlight->span))), $lines), 'max', 0);
+ $this->multipleFiles = !Util::isAllTheSame(array_map(fn (Line $line) => $line->url, $lines));
+ }
+
+ /**
+ * Returns whether $lines contains any adjacent lines from the same source
+ * file that aren't adjacent in the original file.
+ *
+ * @param list<Line> $lines
+ */
+ private static function contiguous(array $lines): bool
+ {
+ for ($i = 0; $i < \count($lines) - 1; $i++) {
+ $thisLine = $lines[$i];
+ $nextLine = $lines[$i + 1];
+
+ if ($thisLine->number + 1 !== $nextLine->number && Util::isSame($thisLine->url, $nextLine->url)) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * Collect all the source lines from the contexts of all spans in
+ * $highlights, and associates them with the highlights that cover them.
+ *
+ * @param list<Highlight> $highlights
+ * @return list<Line>
+ */
+ private static function collateLines(array $highlights): array
+ {
+ // Assign spans without URLs opaque strings as keys. Each such string will
+ // be different, but they can then be used later on to determine which lines
+ // came from the same span even if they'd all otherwise have `null` URLs.
+ $highlightsByUrl = [];
+ $urls = [];
+ foreach ($highlights as $highlight) {
+ $url = $highlight->span->getSourceUrl() ?? new \stdClass();
+ $key = $url instanceof UriInterface ? $url->toString() : spl_object_hash($url);
+ $highlightsByUrl[$key][] = $highlight;
+ $urls[$key] = $url;
+ }
+
+ foreach ($highlightsByUrl as &$list) {
+ usort($list, fn (Highlight $highlight1, Highlight $highlight2) => $highlight1->span->compareTo($highlight2->span));
+ }
+
+ return iterator_to_array(self::expandMapIterable($highlightsByUrl, function (array $highlightsForFile, string $urlKey) use ($urls) {
+ // First, create a list of all the lines in the current file that we have
+ // context for along with their line numbers.
+ $lines = [];
+
+ /** @var Highlight $highlight */
+ foreach ($highlightsForFile as $highlight) {
+ $context = $highlight->span->getContext();
+ // If `$highlight->span->getContext()` contains lines prior to the one
+ // `$highlight->span->getText()` appears on, write those first.
+ $lineStart = Util::findLineStart($context, $highlight->span->getText(), $highlight->span->getStart()->getColumn());
+ \assert($lineStart !== null);
+ $linesBeforeSpan = substr_count(substr($context, 0, $lineStart), "\n");
+
+ $lineNumber = $highlight->span->getStart()->getLine() - $linesBeforeSpan;
+
+ foreach (explode("\n", $context) as $line) {
+ // Only add a line if it hasn't already been added for a previous span
+ if ($lines === [] || $lineNumber > Util::listLast($lines)->number) {
+ $lines[] = new Line($line, $lineNumber, $urls[$urlKey]);
+ }
+ $lineNumber++;
+ }
+ }
+
+ // Next, associate each line with each highlight that covers it.
+ $activeHighlights = [];
+ $highlightIndex = 0;
+
+ foreach ($lines as $line) {
+ $activeHighlights = array_values(array_filter($activeHighlights, fn (Highlight $highlight) => $highlight->span->getEnd()->getLine() >= $line->number));
+
+ $oldHighlightLength = \count($activeHighlights);
+
+ foreach (array_slice($highlightsForFile, $highlightIndex) as $highlight) {
+ if ($highlight->span->getStart()->getLine() > $line->number) {
+ break;
+ }
+ $activeHighlights[] = $highlight;
+ }
+
+ $highlightIndex += \count($activeHighlights) - $oldHighlightLength;
+
+ foreach ($activeHighlights as $activeHighlight) {
+ $line->highlights[] = $activeHighlight;
+ }
+ }
+
+ return $lines;
+ }), false);
+ }
+
+ /**
+ * Returns the highlighted span text.
+ *
+ * This method should only be called once.
+ */
+ public function highlight(): string
+ {
+ $this->writeFileStart($this->lines[0]->url);
+
+ // Each index of this list represents a column after the sidebar that could
+ // contain a line indicating an active highlight. If it's `null`, that
+ // column is empty; if it contains a highlight, it should be drawn for that
+ // column.
+ $highlightsByColumn = array_fill(0, $this->maxMultilineSpans, null);
+
+ foreach ($this->lines as $i => $line) {
+ if ($i > 0) {
+ $lastLine = $this->lines[$i - 1];
+
+ if (!Util::isSame($lastLine->url, $line->url)) {
+ $this->writeSidebar(end: AsciiGlyph::upEnd);
+ $this->buffer .= "\n";
+ $this->writeFileStart($line->url);
+ } elseif ($lastLine->number + 1 !== $line->number) {
+ $this->writeSidebar(text: '...');
+ $this->buffer .= "\n";
+ }
+ }
+
+ // If a highlight covers the entire first line other than initial
+ // whitespace, don't bother pointing out exactly where it begins. Iterate
+ // in reverse so that longer highlights (which are sorted after shorter
+ // highlights) appear further out, leading to fewer crossed lines.
+ foreach (array_reverse($line->highlights) as $highlight) {
+ if (Util::isMultiline($highlight->span) && $highlight->span->getStart()->getLine() === $line->number && $this->isOnlyWhitespace(substr($line->text, 0, $highlight->span->getStart()->getColumn()))) {
+ Util::replaceFirstNull($highlightsByColumn, $highlight);
+ }
+ }
+
+ $this->writeSidebar(line: $line->number);
+ $this->buffer .= ' ';
+ $this->writeMultilineHighlights($line, $highlightsByColumn);
+
+ if ($highlightsByColumn !== []) {
+ $this->buffer .= ' ';
+ }
+ $primaryIdx = Util::indexWhere($line->highlights, fn (Highlight $highlight) => $highlight->isPrimary());
+ $primary = $primaryIdx === null ? null : $line->highlights[$primaryIdx];
+
+ $this->writeText($line->text);
+ $this->buffer .= "\n";
+
+ // Always write the primary span's indicator first so that it's right next
+ // to the highlighted text.
+ if ($primary !== null) {
+ $this->writeIndicator($line, $primary, $highlightsByColumn);
+ }
+
+ foreach ($line->highlights as $highlight) {
+ if ($highlight->isPrimary()) {
+ continue;
+ }
+ $this->writeIndicator($line, $highlight, $highlightsByColumn);
+ }
+ }
+
+ $this->writeSidebar(end: AsciiGlyph::upEnd);
+
+ return $this->buffer;
+ }
+
+ /**
+ * Writes the beginning of the file highlight for the file with the given
+ * $url (or opaque object if it comes from a span with a null URL).
+ */
+ private function writeFileStart(object $url): void
+ {
+ if (!$this->multipleFiles || !$url instanceof UriInterface) {
+ $this->writeSidebar(end: AsciiGlyph::downEnd);
+ } else {
+ $this->writeSidebar(end: AsciiGlyph::topLeftCorner);
+ $this->buffer .= str_repeat(AsciiGlyph::horizontalLine, 2) . '> ';
+ $this->buffer .= Util::prettyUri($url);
+ }
+
+ $this->buffer .= "\n";
+ }
+
+ /**
+ * Writes the post-sidebar highlight bars for $line according to
+ * $highlightsByColumn.
+ *
+ * If $current is passed, it's the highlight for which an indicator is being
+ * written. If it appears in $highlightsByColumn, a horizontal line is
+ * written from its column to the rightmost column.
+ *
+ * @param list<Highlight|null> $highlightsByColumn
+ */
+ private function writeMultilineHighlights(Line $line, array $highlightsByColumn, ?Highlight $current = null): void
+ {
+ // Whether we've written a sidebar indicator for opening a new span on this
+ // line.
+ $openedOnThisLine = false;
+ $foundCurrent = false;
+
+ foreach ($highlightsByColumn as $highlight) {
+ $startLine = $highlight?->span->getStart()->getLine();
+ $endLine = $highlight?->span->getEnd()->getLine();
+
+ if ($current !== null && $highlight === $current) {
+ $foundCurrent = true;
+ \assert($startLine === $line->number || $endLine === $line->number);
+ $this->buffer .= $startLine === $line->number ? AsciiGlyph::topLeftCorner : AsciiGlyph::bottomLeftCorner;
+ } elseif ($foundCurrent) {
+ $this->buffer .= $highlight === null ? AsciiGlyph::horizontalLine : AsciiGlyph::cross;
+ } elseif ($highlight === null) {
+ if ($openedOnThisLine) {
+ $this->buffer .= AsciiGlyph::horizontalLine;
+ } else {
+ $this->buffer .= ' ';
+ }
+ } else {
+ $vertical = $openedOnThisLine ? AsciiGlyph::cross : AsciiGlyph::verticalLine;
+
+ if ($current !== null) {
+ $this->buffer .= $vertical;
+ } elseif ($startLine === $line->number) {
+ $this->buffer .= '/';
+ $openedOnThisLine = true;
+ } elseif ($endLine === $line->number && $highlight->span->getEnd()->getColumn() === \strlen($line->text)) {
+ $this->buffer .= $highlight->label === null ? '\\' : $vertical;
+ } else {
+ $this->buffer .= $vertical;
+ }
+ }
+ }
+ }
+
+ /**
+ * Writes an indicator for where $highlight starts, ends, or both below
+ * $line.
+ *
+ * This may either add or remove $highlight from $highlightsByColumn.
+ *
+ * @param list<Highlight|null> $highlightsByColumn
+ */
+ private function writeIndicator(Line $line, Highlight $highlight, array &$highlightsByColumn): void
+ {
+ if (!Util::isMultiline($highlight->span)) {
+ $this->writeSidebar();
+ $this->buffer .= ' ';
+ $this->writeMultilineHighlights($line, $highlightsByColumn, $highlight);
+
+ if ($highlightsByColumn !== []) {
+ $this->buffer .= ' ';
+ }
+
+ $start = \strlen($this->buffer);
+ $this->writeUnderline($line, $highlight->span, $highlight->isPrimary() ? '^' : AsciiGlyph::horizontalLineBold);
+ $underlineLength = \strlen($this->buffer) - $start;
+ $this->writeLabel($highlight, $highlightsByColumn, $underlineLength);
+ } elseif ($highlight->span->getStart()->getLine() === $line->number) {
+ if (\in_array($highlight, $highlightsByColumn, true)) {
+ return;
+ }
+
+ Util::replaceFirstNull($highlightsByColumn, $highlight);
+
+ $this->writeSidebar();
+ $this->buffer .= ' ';
+ $this->writeMultilineHighlights($line, $highlightsByColumn, $highlight);
+ $this->writeArrow($line, $highlight->span->getStart()->getColumn());
+ $this->buffer .= "\n";
+ } elseif ($highlight->span->getEnd()->getLine() === $line->number) {
+ $coversWholeLine = $highlight->span->getEnd()->getColumn() === \strlen($line->text);
+ if ($coversWholeLine && $highlight->label === null) {
+ Util::replaceWithNull($highlightsByColumn, $highlight);
+ return;
+ }
+
+ $this->writeSidebar();
+ $this->buffer .= ' ';
+ $this->writeMultilineHighlights($line, $highlightsByColumn, $highlight);
+
+ $start = \strlen($this->buffer);
+ if ($coversWholeLine) {
+ $this->buffer .= str_repeat(AsciiGlyph::horizontalLine, 3);
+ } else {
+ $this->writeArrow($line, max($highlight->span->getEnd()->getColumn() - 1, 0), false);
+ }
+ $underlineLength = \strlen($this->buffer) - $start;
+ $this->writeLabel($highlight, $highlightsByColumn, $underlineLength);
+ Util::replaceWithNull($highlightsByColumn, $highlight);
+ }
+ }
+
+ /**
+ * Underlines the portion of $line covered by $span with repeated instances
+ * of $character.
+ */
+ private function writeUnderline(Line $line, SourceSpan $span, string $character): void
+ {
+ \assert(!Util::isMultiline($span));
+ \assert(str_contains($line->text, $span->getText()));
+
+ $startColumn = $span->getStart()->getColumn();
+ $endColumn = $span->getEnd()->getColumn();
+
+ // Adjust the start and end columns to account for any tabs that were
+ // converted to spaces.
+ $tabsBefore = substr_count(substr($line->text, 0, $startColumn), "\t");
+ $tabsInside = substr_count(Util::substring($line->text, $startColumn, $endColumn), "\t");
+
+ $startColumn += $tabsBefore * (self::SPACES_PER_TAB - 1);
+ $endColumn += ($tabsBefore + $tabsInside) * (self::SPACES_PER_TAB - 1);
+
+ $this->buffer .= str_repeat(' ', $startColumn);
+ $this->buffer .= str_repeat($character, max($endColumn - $startColumn, 1));
+ }
+
+ /**
+ * Write an arrow pointing to column $column in $line.
+ *
+ * If the arrow points to a tab character, this will point to the beginning
+ * of the tab if $beginning is `true` and the end if it's `false`.
+ */
+ private function writeArrow(Line $line, int $column, bool $beginning = true): void
+ {
+ $tabs = substr_count(substr($line->text, 0, $column + ($beginning ? 0 : 1)), "\t");
+
+ $this->buffer .= str_repeat(AsciiGlyph::horizontalLine, 1 + $column + $tabs * (self::SPACES_PER_TAB - 1));
+ $this->buffer .= '^';
+ }
+
+ /**
+ * Writes $highlight's label.
+ *
+ * The {@see $buffer} is assumed to be written to the point where the first line
+ * of `$highlight->label` can be written after a space, but this takes care of
+ * writing indentation and highlight columns for later lines.
+ *
+ * The $highlightsByColumn are used to write ongoing highlight lines if the
+ * label is more than one line long.
+ *
+ * The $underlineLength is the length of the line written between the
+ * highlights and the beginning of the first label.
+ *
+ * @param list<Highlight|null> $highlightsByColumn
+ */
+ private function writeLabel(Highlight $highlight, array $highlightsByColumn, int $underlineLength): void
+ {
+ $label = $highlight->label;
+
+ if ($label === null) {
+ $this->buffer .= "\n";
+ return;
+ }
+
+ $lines = explode("\n", $label);
+ $this->buffer .= ' ';
+ $this->buffer .= $lines[0];
+ $this->buffer .= "\n";
+
+ foreach (array_slice($lines, 1) as $text) {
+ $this->writeSidebar();
+ $this->buffer .= ' ';
+
+ foreach ($highlightsByColumn as $columnHighlight) {
+ if ($columnHighlight === null || $columnHighlight === $highlight) {
+ $this->buffer .= ' ';
+ } else {
+ $this->buffer .= AsciiGlyph::verticalLine;
+ }
+ }
+
+ $this->buffer .= str_repeat(' ', $underlineLength + 1);
+ $this->buffer .= $text;
+ $this->buffer .= "\n";
+ }
+ }
+
+ /**
+ * Writes a snippet from the source text, converting hard tab characters into
+ * plain indentation.
+ */
+ private function writeText(string $text): void
+ {
+ $this->buffer .= str_replace("\t", str_repeat(' ', self::SPACES_PER_TAB), $text);
+ }
+
+ /**
+ * Writes a sidebar to {@see $buffer} that includes $line as the line number if
+ * given and writes $end at the end (defaults to {@see AsciiGlyph::verticalLine}).
+ *
+ * If $text is given, it's used in place of the line number. It can't be
+ * passed at the same time as $line.
+ */
+ private function writeSidebar(?int $line = null, ?string $text = null, ?string $end = null): void
+ {
+ \assert($line === null || $text === null);
+
+ if ($line !== null) {
+ // Add 1 to line to convert from computer-friendly 0-indexed line numbers to
+ // human-friendly 1-indexed line numbers.
+ $text = (string) ($line + 1);
+ }
+
+ $this->buffer .= str_pad($text ?? '', $this->paddingBeforeSidebar);
+ $this->buffer .= $end ?? AsciiGlyph::verticalLine;
+ }
+
+ /**
+ * Returns whether $text contains only space or tab characters.
+ */
+ private function isOnlyWhitespace(string $text): bool
+ {
+ for ($i = 0; $i < \strlen($text); $i++) {
+ $char = $text[$i];
+
+ if ($char !== ' ' && $char !== "\t") {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * @template K
+ * @template E
+ * @template T
+ * @param iterable<K, E> $elements
+ * @param callable(E, K): iterable<T> $callback
+ * @return \Traversable<T>
+ *
+ * @param-immediately-invoked-callable $callback
+ */
+ private static function expandMapIterable(iterable $elements, callable $callback): \Traversable
+ {
+ foreach ($elements as $key => $element) {
+ yield from $callback($element, $key);
+ }
+ }
+}
diff --git a/vendor/scssphp/source-span/src/Highlighter/Line.php b/vendor/scssphp/source-span/src/Highlighter/Line.php
new file mode 100644
index 000000000..0dc800c38
--- /dev/null
+++ b/vendor/scssphp/source-span/src/Highlighter/Line.php
@@ -0,0 +1,40 @@
+<?php
+
+namespace SourceSpan\Highlighter;
+
+/**
+ * A single line of the source file being highlighted.
+ *
+ * @internal
+ */
+final class Line
+{
+ /**
+ * All highlights that cover any portion of this line, in source span order.
+ *
+ * This is populated after the initial line is created.
+ *
+ * @var list<Highlight>
+ */
+ public array $highlights = [];
+
+ /**
+ * The URL of the source file in which this line appears.
+ *
+ * For lines created from spans without an explicit URL, this is an opaque
+ * object that differs between lines that come from different spans.
+ */
+ public readonly object $url;
+
+
+ /**
+ * @param int $number The O-based line number in the source file
+ */
+ public function __construct(
+ public readonly string $text,
+ public readonly int $number,
+ object $url,
+ ) {
+ $this->url = $url;
+ }
+}
diff --git a/vendor/scssphp/source-span/src/SimpleSourceLocation.php b/vendor/scssphp/source-span/src/SimpleSourceLocation.php
new file mode 100644
index 000000000..591f6e4b0
--- /dev/null
+++ b/vendor/scssphp/source-span/src/SimpleSourceLocation.php
@@ -0,0 +1,59 @@
+<?php
+
+namespace SourceSpan;
+
+use League\Uri\Contracts\UriInterface;
+
+final class SimpleSourceLocation extends SourceLocationMixin
+{
+ private readonly int $line;
+ private readonly int $column;
+
+ /**
+ * Creates a new location indicating $offset within $sourceUrl.
+ *
+ * $line and $column default to assuming the source is a single line. This
+ * means that $line defaults to 0 and $column defaults to $offset.
+ */
+ public function __construct(
+ private readonly int $offset,
+ private readonly ?UriInterface $sourceUrl = null,
+ ?int $line = null,
+ ?int $column = null,
+ ) {
+ $this->line = $line ?? 0;
+ $this->column = $column ?? $offset;
+
+ if ($offset < 0) {
+ throw new \OutOfRangeException('Offset may not be negative.');
+ }
+
+ if ($line !== null && $line < 0) {
+ throw new \OutOfRangeException('Line may not be negative.');
+ }
+
+ if ($column !== null && $column < 0) {
+ throw new \OutOfRangeException('Column may not be negative.');
+ }
+ }
+
+ public function getOffset(): int
+ {
+ return $this->offset;
+ }
+
+ public function getLine(): int
+ {
+ return $this->line;
+ }
+
+ public function getColumn(): int
+ {
+ return $this->column;
+ }
+
+ public function getSourceUrl(): ?UriInterface
+ {
+ return $this->sourceUrl;
+ }
+}
diff --git a/vendor/scssphp/source-span/src/SimpleSourceSpan.php b/vendor/scssphp/source-span/src/SimpleSourceSpan.php
new file mode 100644
index 000000000..7c29498bb
--- /dev/null
+++ b/vendor/scssphp/source-span/src/SimpleSourceSpan.php
@@ -0,0 +1,53 @@
+<?php
+
+namespace SourceSpan;
+
+final class SimpleSourceSpan extends SourceSpanMixin
+{
+ public function __construct(
+ private readonly SourceLocation $start,
+ private readonly SourceLocation $end,
+ private readonly string $text,
+ ) {
+ if (!Util::isSameUrl($start->getSourceUrl(), $end->getSourceUrl())) {
+ throw new \InvalidArgumentException("Source URLs \"{$start->getSourceUrl()}\" and \"{$end->getSourceUrl()}\" don't match.");
+ }
+
+ if ($this->end->getOffset() < $this->start->getOffset()) {
+ throw new \InvalidArgumentException('End must come after start.');
+ }
+
+ $distance = $this->start->distance($this->end);
+ if (\strlen($this->text) !== $distance) {
+ throw new \InvalidArgumentException("Text \"$text\" must be $distance characters long.");
+ }
+ }
+
+ public function getStart(): SourceLocation
+ {
+ return $this->start;
+ }
+
+ public function getEnd(): SourceLocation
+ {
+ return $this->end;
+ }
+
+ public function getText(): string
+ {
+ return $this->text;
+ }
+
+ public function subspan(int $start, ?int $end = null): SourceSpan
+ {
+ Util::checkValidRange($start, $end, $this->getLength());
+
+ if ($start === 0 && ($end === null || $end === $this->getLength())) {
+ return $this;
+ }
+
+ $locations = Util::subspanLocations($this, $start, $end);
+
+ return new SimpleSourceSpan($locations[0], $locations[1], Util::substring($this->text, $start, $end));
+ }
+}
diff --git a/vendor/scssphp/source-span/src/SimpleSourceSpanWithContext.php b/vendor/scssphp/source-span/src/SimpleSourceSpanWithContext.php
new file mode 100644
index 000000000..b854a52b0
--- /dev/null
+++ b/vendor/scssphp/source-span/src/SimpleSourceSpanWithContext.php
@@ -0,0 +1,68 @@
+<?php
+
+namespace SourceSpan;
+
+final class SimpleSourceSpanWithContext extends SourceSpanMixin implements SourceSpanWithContext
+{
+ public function __construct(
+ private readonly SourceLocation $start,
+ private readonly SourceLocation $end,
+ private readonly string $text,
+ private readonly string $context
+ ) {
+ if (!Util::isSameUrl($start->getSourceUrl(), $end->getSourceUrl())) {
+ throw new \InvalidArgumentException("Source URLs \"{$start->getSourceUrl()}\" and \"{$end->getSourceUrl()}\" don't match.");
+ }
+
+ if ($this->end->getOffset() < $this->start->getOffset()) {
+ throw new \InvalidArgumentException('End must come after start.');
+ }
+
+ $distance = $this->start->distance($this->end);
+ if (\strlen($this->text) !== $distance) {
+ throw new \InvalidArgumentException("Text \"$text\" must be $distance characters long.");
+ }
+
+ if (!str_contains($this->context, $this->text)) {
+ throw new \InvalidArgumentException("The context line \"$context\" must contain \"$text\".");
+ }
+
+ if (Util::findLineStart($this->context, $this->text, $this->start->getColumn()) === null) {
+ $column = $this->start->getColumn() + 1;
+ throw new \InvalidArgumentException("The span text \"$text\" must start at column $column in a line within \"$context\".");
+ }
+ }
+
+ public function getStart(): SourceLocation
+ {
+ return $this->start;
+ }
+
+ public function getEnd(): SourceLocation
+ {
+ return $this->end;
+ }
+
+ public function getText(): string
+ {
+ return $this->text;
+ }
+
+ public function getContext(): string
+ {
+ return $this->context;
+ }
+
+ public function subspan(int $start, ?int $end = null): SourceSpanWithContext
+ {
+ Util::checkValidRange($start, $end, $this->getLength());
+
+ if ($start === 0 && ($end === null || $end === $this->getLength())) {
+ return $this;
+ }
+
+ $locations = Util::subspanLocations($this, $start, $end);
+
+ return new SimpleSourceSpanWithContext($locations[0], $locations[1], Util::substring($this->text, $start, $end), $this->context);
+ }
+}
diff --git a/vendor/scssphp/source-span/src/SourceFile.php b/vendor/scssphp/source-span/src/SourceFile.php
new file mode 100644
index 000000000..33bc5502c
--- /dev/null
+++ b/vendor/scssphp/source-span/src/SourceFile.php
@@ -0,0 +1,272 @@
+<?php
+
+namespace SourceSpan;
+
+use League\Uri\Contracts\UriInterface;
+
+final class SourceFile
+{
+ private readonly string $string;
+
+ private readonly ?UriInterface $sourceUrl;
+
+ /**
+ * @var list<int>
+ */
+ private readonly array $lineStarts;
+
+ /**
+ * The 0-based last line that was returned by {@see getLine}
+ *
+ * This optimizes computation for successive accesses to
+ * the same line or to the next line.
+ * It is stored as 0-based to correspond to the indices
+ * in {@see $lineStarts}.
+ *
+ * @var int|null
+ */
+ private ?int $cachedLine = null;
+
+ public static function fromString(string $content, ?UriInterface $sourceUrl = null): SourceFile
+ {
+ return new SourceFile($content, $sourceUrl);
+ }
+
+ private function __construct(string $content, ?UriInterface $sourceUrl = null)
+ {
+ $this->string = $content;
+ $this->sourceUrl = $sourceUrl;
+
+ // Extract line starts
+ $lineStarts = [0];
+
+ if ($content === '') {
+ $this->lineStarts = $lineStarts;
+ return;
+ }
+
+ $prev = 0;
+
+ while (true) {
+ $crPos = strpos($content, "\r", $prev);
+ $lfPos = strpos($content, "\n", $prev);
+
+ if ($crPos === false && $lfPos === false) {
+ break;
+ }
+
+ if ($crPos !== false) {
+ // Return not followed by newline is treated as a newline
+ if ($lfPos === false || $lfPos > $crPos + 1) {
+ $lineStarts[] = $crPos + 1;
+ $prev = $crPos + 1;
+ continue;
+ }
+ }
+
+ if ($lfPos !== false) {
+ $lineStarts[] = $lfPos + 1;
+ $prev = $lfPos + 1;
+ }
+ }
+
+ $this->lineStarts = $lineStarts;
+ }
+
+ public function getLength(): int
+ {
+ return \strlen($this->string);
+ }
+
+ /**
+ * The number of lines in the file.
+ */
+ public function getLines(): int
+ {
+ return \count($this->lineStarts);
+ }
+
+ public function span(int $start, ?int $end = null): FileSpan
+ {
+ if ($end === null) {
+ $end = \strlen($this->string);
+ }
+
+ return new ConcreteFileSpan($this, $start, $end);
+ }
+
+ public function location(int $offset): FileLocation
+ {
+ if ($offset < 0) {
+ throw new \OutOfRangeException("Offset may not be negative, was $offset.");
+ }
+
+ if ($offset > \strlen($this->string)) {
+ $fileLength = \strlen($this->string);
+
+ throw new \OutOfRangeException("Offset $offset must not be greater than the number of characters in the file, $fileLength.");
+ }
+
+ return new FileLocation($this, $offset);
+ }
+
+ public function getSourceUrl(): ?UriInterface
+ {
+ return $this->sourceUrl;
+ }
+
+ public function getString(): string
+ {
+ return $this->string;
+ }
+
+ /**
+ * The 0-based line corresponding to that offset.
+ */
+ public function getLine(int $offset): int
+ {
+ if ($offset < 0) {
+ throw new \OutOfRangeException('Position cannot be negative');
+ }
+
+ if ($offset > \strlen($this->string)) {
+ throw new \OutOfRangeException('Position cannot be greater than the number of characters in the string.');
+ }
+
+ if ($offset < $this->lineStarts[0]) {
+ return -1;
+ }
+
+ if ($offset >= Util::listLast($this->lineStarts)) {
+ return \count($this->lineStarts) - 1;
+ }
+
+ if ($this->isNearCacheLine($offset)) {
+ assert($this->cachedLine !== null);
+
+ return $this->cachedLine;
+ }
+
+ $this->cachedLine = $this->binarySearch($offset) - 1;
+
+ return $this->cachedLine;
+ }
+
+ /**
+ * Returns `true` if $offset 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.
+ */
+ private function isNearCacheLine(int $offset): bool
+ {
+ if ($this->cachedLine === null) {
+ return false;
+ }
+
+ if ($offset < $this->lineStarts[$this->cachedLine]) {
+ return false;
+ }
+
+ if (
+ $this->cachedLine >= \count($this->lineStarts) - 1 ||
+ $offset < $this->lineStarts[$this->cachedLine + 1]
+ ) {
+ return true;
+ }
+
+ if (
+ $this->cachedLine >= \count($this->lineStarts) - 2 ||
+ $offset < $this->lineStarts[$this->cachedLine + 2]
+ ) {
+ ++$this->cachedLine;
+
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * Binary search through {@see $lineStarts} to find the line containing $offset.
+ *
+ * Returns the index of the line in {@see $lineStarts}.
+ */
+ private function binarySearch(int $offset): int
+ {
+ $min = 0;
+ $max = \count($this->lineStarts) - 1;
+
+ while ($min < $max) {
+ $half = $min + intdiv($max - $min, 2);
+
+ if ($this->lineStarts[$half] > $offset) {
+ $max = $half;
+ } else {
+ $min = $half + 1;
+ }
+ }
+
+ return $max;
+ }
+
+ /**
+ * The 0-based column of that offset.
+ */
+ public function getColumn(int $offset): int
+ {
+ $line = $this->getLine($offset);
+
+ return $offset - $this->lineStarts[$line];
+ }
+
+ /**
+ * Gets the offset for a line and column.
+ */
+ public function getOffset(int $line, int $column = 0): int
+ {
+ if ($line < 0) {
+ throw new \OutOfRangeException('Line may not be negative.');
+ }
+
+ if ($line >= \count($this->lineStarts)) {
+ throw new \OutOfRangeException('Line must be less than the number of lines in the file.');
+ }
+
+ if ($column < 0) {
+ throw new \OutOfRangeException('Column may not be negative.');
+ }
+
+ $result = $this->lineStarts[$line] + $column;
+
+ if ($result > \strlen($this->string) || ($line + 1 < \count($this->lineStarts) && $result >= $this->lineStarts[$line + 1])) {
+ throw new \OutOfRangeException("Line $line doesn't have $column columns.");
+ }
+
+ return $result;
+ }
+
+ /**
+ * 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) {
+ if ($end < $start) {
+ throw new \InvalidArgumentException("End $end must come after start $start.");
+ }
+
+ if ($end > $this->getLength()) {
+ throw new \OutOfRangeException("End $end not be greater than the number of characters in the file, {$this->getLength()}.");
+ }
+ }
+
+ if ($start < 0) {
+ throw new \OutOfRangeException("Start may not be negative, was $start.");
+ }
+
+ return Util::substring($this->string, $start, $end);
+ }
+}
diff --git a/vendor/scssphp/source-span/src/SourceLocation.php b/vendor/scssphp/source-span/src/SourceLocation.php
new file mode 100644
index 000000000..99792350a
--- /dev/null
+++ b/vendor/scssphp/source-span/src/SourceLocation.php
@@ -0,0 +1,47 @@
+<?php
+
+namespace SourceSpan;
+
+use League\Uri\Contracts\UriInterface;
+
+interface SourceLocation
+{
+ public function getOffset(): int;
+
+ /**
+ * The 0-based line of that location
+ */
+ public function getLine(): int;
+
+ /**
+ * The 0-based column of that location
+ */
+ public function getColumn(): int;
+
+ public function getSourceUrl(): ?UriInterface;
+
+ /**
+ * Returns the distance in characters between $this and $other.
+ *
+ * This always returns a non-negative value.
+ *
+ * @return int<0, max>
+ */
+ public function distance(SourceLocation $other): int;
+
+ /**
+ * Returns a span that covers only a single point: this location.
+ */
+ public function pointSpan(): SourceSpan;
+
+ /**
+ * Compares two locations.
+ *
+ * It returns a negative integer if $this is ordered before $other,
+ * a positive integer if $this is ordered after $other,
+ * and zero if $this and $other are ordered together.
+ *
+ * $other must have the same source URL as $this.
+ */
+ public function compareTo(SourceLocation $other): int;
+}
diff --git a/vendor/scssphp/source-span/src/SourceLocationMixin.php b/vendor/scssphp/source-span/src/SourceLocationMixin.php
new file mode 100644
index 000000000..1649a3d1d
--- /dev/null
+++ b/vendor/scssphp/source-span/src/SourceLocationMixin.php
@@ -0,0 +1,34 @@
+<?php
+
+namespace SourceSpan;
+
+/**
+ * A mixin for easily implementing {@see SourceLocation}.
+ *
+ * @internal
+ */
+abstract class SourceLocationMixin implements SourceLocation
+{
+ public function distance(SourceLocation $other): int
+ {
+ if (!Util::isSameUrl($this->getSourceUrl(), $other->getSourceUrl())) {
+ throw new \InvalidArgumentException("Source URLs \"{$this->getSourceUrl()}\" and \"{$other->getSourceUrl()}\" don't match.");
+ }
+
+ return abs($this->getOffset() - $other->getOffset());
+ }
+
+ public function pointSpan(): SourceSpan
+ {
+ return new SimpleSourceSpan($this, $this, '');
+ }
+
+ public function compareTo(SourceLocation $other): int
+ {
+ if (!Util::isSameUrl($this->getSourceUrl(), $other->getSourceUrl())) {
+ throw new \InvalidArgumentException("Source URLs \"{$this->getSourceUrl()}\" and \"{$other->getSourceUrl()}\" don't match.");
+ }
+
+ return $this->getOffset() - $other->getOffset();
+ }
+}
diff --git a/vendor/scssphp/source-span/src/SourceSpan.php b/vendor/scssphp/source-span/src/SourceSpan.php
new file mode 100644
index 000000000..5badc3adf
--- /dev/null
+++ b/vendor/scssphp/source-span/src/SourceSpan.php
@@ -0,0 +1,110 @@
+<?php
+
+namespace SourceSpan;
+
+use League\Uri\Contracts\UriInterface;
+
+/**
+ * An interface that describes a segment of source text.
+ */
+interface SourceSpan
+{
+ /**
+ * The start location of this span.
+ */
+ public function getStart(): SourceLocation;
+
+ /**
+ * The end location of this span, exclusive.
+ */
+ public function getEnd(): SourceLocation;
+
+ /**
+ * The source text for this span.
+ */
+ public function getText(): string;
+
+ /**
+ * The URL of the source (typically a file) of this span.
+ *
+ * This may be null, indicating that the source URL is unknown or
+ * unavailable.
+ */
+ public function getSourceUrl(): ?UriInterface;
+
+ /**
+ * The length of this span, in bytes.
+ */
+ public function getLength(): int;
+
+ /**
+ * Creates a new span that's the union of $this and $other.
+ *
+ * The two spans must have the same source URL and may not be disjoint.
+ * {@see getText} is computed by combining `$this->getText()` and `$other->getText()`.
+ */
+ public function union(SourceSpan $other): SourceSpan;
+
+ /**
+ * Compares two spans.
+ *
+ * It returns a negative integer if $this is ordered before $other,
+ * a positive integer if $this is ordered after $other,
+ * and zero if $this and $other are ordered together.
+ *
+ * $other must have the same source URL as `this`. This orders spans by
+ * {@see getStart} then {@see getLength}.
+ */
+ public function compareTo(SourceSpan $other): int;
+
+ /**
+ * Formats $message in a human-friendly way associated with this span.
+ *
+ * @param string $message
+ *
+ * @return string
+ */
+ public function message(string $message): string;
+
+ /**
+ * Like {@see message}, but also highlights $secondarySpans to provide
+ * the user with additional context.
+ *
+ * Each span takes a label ($label for this span, and the keys of the
+ * $secondarySpans map for the secondary spans) that's used to indicate to
+ * the user what that particular span represents.
+ *
+ * @throws \InvalidArgumentException if any secondary span has a different source URL than this span.
+ *
+ * @param array<string, SourceSpan> $secondarySpans
+ */
+ public function messageMultiple(string $message, string $label, array $secondarySpans): string;
+
+ /**
+ * Prints the text associated with this span in a user-friendly way.
+ *
+ * This is identical to {@see message}, except that it doesn't print the file
+ * name, line number, column number, or message.
+ */
+ public function highlight(): string;
+
+ /**
+ * Like {@see highlight}, but also highlights $secondarySpans to provide
+ * the user with additional context.
+ *
+ * Each span takes a label ($label for this span, and the keys of the
+ * $secondarySpans map for the secondary spans) that's used to indicate to
+ * the user what that particular span represents.
+ *
+ * @throws \InvalidArgumentException if any secondary span has a different source URL than this span.
+ *
+ * @param array<string, SourceSpan> $secondarySpans
+ */
+ public function highlightMultiple(string $label, array $secondarySpans): string;
+
+ /**
+ * 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): SourceSpan;
+}
diff --git a/vendor/scssphp/source-span/src/SourceSpanMixin.php b/vendor/scssphp/source-span/src/SourceSpanMixin.php
new file mode 100644
index 000000000..b9bbdb01b
--- /dev/null
+++ b/vendor/scssphp/source-span/src/SourceSpanMixin.php
@@ -0,0 +1,132 @@
+<?php
+
+namespace SourceSpan;
+
+use League\Uri\Contracts\UriInterface;
+use SourceSpan\Highlighter\Highlighter;
+
+/**
+ * A mixin for easily implementing {@see SourceSpan}.
+ *
+ * This implements the {@see SourceSpan} methods in terms of {@see getStart}, {@see getEnd}, and
+ * {@see getText}. This assumes that {@see getStart} and {@see getEnd} have the same source URL, that
+ * {@see getStart} comes before {@see getEnd}, and that {@see getText} has a number of characters equal
+ * to the distance between {@see getStart} and {@see getEnd}.
+ *
+ * @internal
+ */
+abstract class SourceSpanMixin implements SourceSpan
+{
+ public function getSourceUrl(): ?UriInterface
+ {
+ return $this->getStart()->getSourceUrl();
+ }
+
+ public function getLength(): int
+ {
+ return $this->getEnd()->getOffset() - $this->getStart()->getOffset();
+ }
+
+ public function union(SourceSpan $other): SourceSpan
+ {
+ if (!Util::isSameUrl($this->getSourceUrl(), $other->getSourceUrl())) {
+ throw new \InvalidArgumentException("Source URLs \"{$this->getSourceUrl()}\" and \"{$other->getSourceUrl()}\" don't match.");
+ }
+
+ if ($this->getStart()->compareTo($other->getStart()) > 0) {
+ $start = $other->getStart();
+ $beginSpan = $other;
+ } else {
+ $start = $this->getStart();
+ $beginSpan = $this;
+ }
+ if ($this->getEnd()->compareTo($other->getEnd()) > 0) {
+ $end = $this->getEnd();
+ $endSpan = $this;
+ } else {
+ $end = $other->getEnd();
+ $endSpan = $other;
+ }
+
+ if ($beginSpan->getEnd()->compareTo($endSpan->getStart()) < 0) {
+ throw new \InvalidArgumentException("Spans are disjoint.");
+ }
+
+ $text = $beginSpan->getText() . substr($endSpan->getText(), $beginSpan->getEnd()->distance($endSpan->getStart()));
+
+ return new SimpleSourceSpan($start, $end, $text);
+ }
+
+ public function compareTo(SourceSpan $other): int
+ {
+ $result = $this->getStart()->compareTo($other->getStart());
+
+ if ($result !== 0) {
+ return $result;
+ }
+
+ return $this->getEnd()->compareTo($other->getEnd());
+ }
+
+ public function message(string $message): string
+ {
+ $startLine = $this->getStart()->getLine() + 1;
+ $startColumn = $this->getStart()->getColumn() + 1;
+ $sourceUrl = $this->getSourceUrl();
+
+ $buffer = "line $startLine, column $startColumn";
+
+ if ($sourceUrl !== null) {
+ $prettyUri = Util::prettyUri($sourceUrl);
+ $buffer .= " of $prettyUri";
+ }
+
+ $buffer .= ": $message";
+
+ $highlight = $this->highlight();
+ if ($highlight !== '') {
+ $buffer .= "\n";
+ $buffer .= $highlight;
+ }
+
+ return $buffer;
+ }
+
+ public function messageMultiple(string $message, string $label, array $secondarySpans): string
+ {
+ $startLine = $this->getStart()->getLine() + 1;
+ $startColumn = $this->getStart()->getColumn() + 1;
+ $sourceUrl = $this->getSourceUrl();
+
+ $buffer = "line $startLine, column $startColumn";
+
+ if ($sourceUrl !== null) {
+ $prettyUri = Util::prettyUri($sourceUrl);
+ $buffer .= " of $prettyUri";
+ }
+
+ $buffer .= ": $message";
+
+ $highlight = $this->highlightMultiple($label, $secondarySpans);
+ if ($highlight !== '') {
+ $buffer .= "\n";
+ $buffer .= $highlight;
+ }
+
+ return $buffer;
+ }
+
+ public function highlight(): string
+ {
+ if (!$this instanceof SourceSpanWithContext && $this->getLength() === 0) {
+ return '';
+ }
+
+ return Highlighter::create($this)->highlight();
+ }
+
+ public function highlightMultiple(string $label, array $secondarySpans): string
+ {
+ return Highlighter::multiple($this, $label, $secondarySpans)->highlight();
+ }
+}
diff --git a/vendor/scssphp/source-span/src/SourceSpanWithContext.php b/vendor/scssphp/source-span/src/SourceSpanWithContext.php
new file mode 100644
index 000000000..383a6c7c2
--- /dev/null
+++ b/vendor/scssphp/source-span/src/SourceSpanWithContext.php
@@ -0,0 +1,16 @@
+<?php
+
+namespace SourceSpan;
+
+/**
+ * An interface that describes a segment of source text with additional context.
+ */
+interface SourceSpanWithContext extends SourceSpan
+{
+ /**
+ * Text around the span, which includes the line containing this span.
+ */
+ public function getContext(): string;
+
+ public function subspan(int $start, ?int $end = null): SourceSpanWithContext;
+}
diff --git a/vendor/scssphp/source-span/src/Util.php b/vendor/scssphp/source-span/src/Util.php
new file mode 100644
index 000000000..ccddb009c
--- /dev/null
+++ b/vendor/scssphp/source-span/src/Util.php
@@ -0,0 +1,358 @@
+<?php
+
+namespace SourceSpan;
+
+use League\Uri\BaseUri;
+use League\Uri\Contracts\UriInterface;
+
+/**
+ * @internal
+ */
+final class Util
+{
+ /**
+ * @param iterable<object> $iter
+ */
+ public static function isAllTheSame(iterable $iter): bool
+ {
+ $previousValue = null;
+
+ foreach ($iter as $value) {
+ if ($previousValue === null) {
+ $previousValue = $value;
+ continue;
+ }
+
+ if (!self::isSame($value, $previousValue)) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * Returns whether 2 objects are the same, considering URIs as the same by equality rather than reference.
+ */
+ public static function isSame(object $object1, object $object2): bool
+ {
+ if ($object1 === $object2) {
+ return true;
+ }
+
+ if ($object1 instanceof UriInterface && $object2 instanceof UriInterface) {
+ return $object1->toString() === $object2->toString();
+ }
+
+ return false;
+ }
+
+ /**
+ * Returns whether $span covers multiple lines.
+ */
+ public static function isMultiline(SourceSpan $span): bool
+ {
+ return $span->getStart()->getLine() !== $span->getEnd()->getLine();
+ }
+
+ /**
+ * Sets the first `null` element of $list to $element.
+ *
+ * @template E
+ * @param list<E|null> $list
+ * @param E $element
+ */
+ public static function replaceFirstNull(array &$list, $element): void
+ {
+ $index = array_search(null, $list, true);
+
+ if ($index === false) {
+ throw new \InvalidArgumentException('The list contains no null elements.');
+ }
+
+ // @phpstan-ignore parameterByRef.type
+ $list[$index] = $element;
+ \assert(array_is_list($list));
+ }
+
+ /**
+ * Sets the element of $list that currently contains $element to `null`.
+ *
+ * @template E
+ * @param list<E|null> $list
+ * @param E $element
+ */
+ public static function replaceWithNull(array &$list, $element): void
+ {
+ $index = array_search($element, $list, true);
+
+ if ($index === false) {
+ throw new \InvalidArgumentException('The list contains no matching elements.');
+ }
+
+ // @phpstan-ignore parameterByRef.type
+ $list[$index] = null;
+ \assert(array_is_list($list));
+ }
+
+ /**
+ * Finds a line in $context containing $text at the specified column.
+ *
+ * Returns the index in $context where that line begins, or null if none
+ * exists.
+ */
+ public static function findLineStart(string $context, string $text, int $column): ?int
+ {
+ // If the text is empty, we just want to find the first line that has at least
+ // $column characters.
+ if ($text === '') {
+ $beginningOfLine = 0;
+
+ while (true) {
+ $index = strpos($context, "\n", $beginningOfLine);
+
+ if ($index === false) {
+ return \strlen($context) - $beginningOfLine >= $column ? $beginningOfLine : null;
+ }
+
+ if ($index - $beginningOfLine >= $column) {
+ return $beginningOfLine;
+ }
+
+ $beginningOfLine = $index + 1;
+ }
+ }
+
+ $index = strpos($context, $text);
+
+ while ($index !== false) {
+ // Start looking before $index in case $text starts with a newline.
+ $lineStart = $index === 0 ? 0 : Util::lastIndexOf($context, "\n", $index - 1) + 1;
+ $textColumn = $index - $lineStart;
+
+ if ($column === $textColumn) {
+ return $lineStart;
+ }
+
+ $index = strpos($context, $text, $index + 1);
+ }
+
+ return null;
+ }
+
+ /**
+ * Returns a two-element list containing the start and end locations of the
+ * span from $start bytes (inclusive) to $end bytes (exclusive)
+ * after the beginning of $span.
+ *
+ * @return array{SourceLocation, SourceLocation}
+ */
+ public static function subspanLocations(SourceSpan $span, int $start, ?int $end = null): array
+ {
+ $text = $span->getText();
+ $startLocation = $span->getStart();
+ $line = $startLocation->getLine();
+ $column = $startLocation->getColumn();
+
+ // Adjust $line and $column as necessary if the character at $i in $text
+ // is a newline.
+ $consumeCodePoint = function (int $i) use ($text, &$line, &$column) {
+ $codeUnit = $text[$i];
+
+ if (
+ $codeUnit === "\n" ||
+ // A carriage return counts as a newline, but only if it's not
+ // followed by a line feed.
+ ($codeUnit === "\r" && ($i + 1 === \strlen($text) || $text[$i + 1] !== "\n"))
+ ) {
+ $line += 1;
+ $column = 0;
+ } else {
+ $column += 1;
+ }
+ };
+
+ for ($i = 0; $i < $start; $i++) {
+ $consumeCodePoint($i);
+ }
+
+ $newStartLocation = new SimpleSourceLocation($startLocation->getOffset() + $start, $span->getSourceUrl(), $line, $column);
+
+ if ($end === null || $end === $span->getLength()) {
+ $newEndLocation = $span->getEnd();
+ } elseif ($end === $start) {
+ $newEndLocation = $newStartLocation;
+ } else {
+ for ($i = $start; $i < $end; $i++) {
+ $consumeCodePoint($i);
+ }
+
+ $newEndLocation = new SimpleSourceLocation($startLocation->getOffset() + $end, $span->getSourceUrl(), $line, $column);
+ }
+
+ return [$newStartLocation, $newEndLocation];
+ }
+
+ /**
+ * The starting position of the last match $needle in this string.
+ *
+ * Finds a match of $needle by searching backward starting at $start.
+ * Returns -1 if $needle could not be found in this string.
+ * If $start is omitted, search starts from the end of the string.
+ */
+ public static function lastIndexOf(string $string, string $needle, ?int $start = null): int
+ {
+ if ($start === null || $start === \strlen($string)) {
+ $position = strrpos($string, $needle);
+ } else {
+ if ($start < 0) {
+ throw new \InvalidArgumentException("Start must be a non-negative integer");
+ }
+
+ if ($start > \strlen($string)) {
+ throw new \InvalidArgumentException("Start must not be greater than the length of the string");
+ }
+
+ $position = strrpos($string, $needle, $start - \strlen($string));
+ }
+
+ return $position === false ? -1 : $position;
+ }
+
+ /**
+ * Returns the text of the string from $start to $end (exclusive).
+ *
+ * If $end isn't passed, it defaults to the end of the string.
+ */
+ public static function substring(string $text, int $start, ?int $end = null): string
+ {
+ if ($end === null) {
+ return substr($text, $start);
+ }
+
+ if ($end < $start) {
+ $length = 0;
+ } else {
+ $length = $end - $start;
+ }
+
+ return substr($text, $start, $length);
+ }
+
+ public static function isSameUrl(?UriInterface $url1, ?UriInterface $url2): bool
+ {
+ if ($url1 === null) {
+ return $url2 === null;
+ }
+
+ if ($url2 === null) {
+ return false;
+ }
+
+ return (string) $url1 === (string) $url2;
+ }
+
+ /**
+ * Finds the first index in the list that satisfies the provided $test.
+ *
+ * @template E
+ *
+ * @param list<E> $list
+ * @param callable(E): bool $test
+ */
+ public static function indexWhere(array $list, callable $test): ?int
+ {
+ foreach ($list as $index => $element) {
+ if ($test($element)) {
+ return $index;
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * 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 ??= '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 ??= 'end';
+ $endNameDisplay = $endName ? " $endName" : '';
+
+ throw new \OutOfRangeException("Invalid value:$endNameDisplay must be between $start and $length: $end.");
+ }
+ }
+ }
+
+ /**
+ * @template T
+ *
+ * @param list<T> $list
+ *
+ * @return T
+ */
+ public static function listLast(array $list)
+ {
+ $count = count($list);
+
+ if ($count === 0) {
+ throw new \LogicException('The list may not be empty.');
+ }
+
+ return $list[$count - 1];
+ }
+
+ /**
+ * Returns a pretty URI for a path
+ */
+ public static function prettyUri(string|UriInterface $path): string
+ {
+ if ($path instanceof UriInterface) {
+ if ($path->getScheme() !== 'file') {
+ return (string) $path;
+ }
+
+ $path = self::pathFromUri($path);
+ }
+
+ $normalizedPath = $path;
+ $normalizedRootDirectory = getcwd() . '/';
+
+ if (\DIRECTORY_SEPARATOR === '\\') {
+ $normalizedRootDirectory = str_replace('\\', '/', $normalizedRootDirectory);
+ $normalizedPath = str_replace('\\', '/', $path);
+ }
+
+ if (str_starts_with($normalizedPath, $normalizedRootDirectory)) {
+ return substr($path, \strlen($normalizedRootDirectory));
+ }
+
+ return $path;
+ }
+
+ private static function pathFromUri(UriInterface $uri): string
+ {
+ if (\DIRECTORY_SEPARATOR === '\\') {
+ return BaseUri::from($uri)->windowsPath() ?? throw new \InvalidArgumentException("Uri $uri must have scheme 'file:'.");
+ }
+
+ return BaseUri::from($uri)->unixPath() ?? throw new \InvalidArgumentException("Uri $uri must have scheme 'file:'.");
+ }
+}