diff --git a/config/phpstan-baseline.neon b/config/phpstan-baseline.neon index 4c4add4da..28ce1706c 100644 --- a/config/phpstan-baseline.neon +++ b/config/phpstan-baseline.neon @@ -48,6 +48,12 @@ parameters: count: 3 path: ../src/Value/Color.php + - + message: '#^Parameter \#2 \$arguments of class Sabberworm\\CSS\\Value\\Expression constructor expects array\\|Sabberworm\\CSS\\Value\\RuleValueList, Sabberworm\\CSS\\Value\\Value\|string given\.$#' + identifier: argument.type + count: 1 + path: ../src/Value/Expression.php + - message: '#^Parameters should have "float" types as the only types passed to this method$#' identifier: typePerfect.narrowPublicClassMethodParamType @@ -107,3 +113,9 @@ parameters: identifier: argument.type count: 2 path: ../tests/Unit/CSSList/CSSListTest.php + + - + message: '#^Call to an undefined static method Sabberworm\\CSS\\Tests\\Value\\ExpressionTest\:\:listDelimiterForRule\(\)\.$#' + identifier: staticMethod.notFound + count: 1 + path: ../tests/Unit/Value/ExpressionTest.php diff --git a/src/Value/CSSFunction.php b/src/Value/CSSFunction.php index 86b56d9b1..32338da35 100644 --- a/src/Value/CSSFunction.php +++ b/src/Value/CSSFunction.php @@ -17,14 +17,14 @@ class CSSFunction extends ValueList { /** - * @var non-empty-string + * @var string * * @internal since 8.8.0 */ protected $name; /** - * @param non-empty-string $name + * @param string $name * @param RuleValueList|array $arguments * @param non-empty-string $separator * @param int<1, max>|null $lineNumber @@ -76,7 +76,7 @@ private static function parseName(ParserState $parserState, bool $ignoreCase = f * @throws UnexpectedEOFException * @throws UnexpectedTokenException */ - private static function parseArguments(ParserState $parserState) + protected static function parseArguments(ParserState $parserState) { return Value::parseValue($parserState, ['=', ' ', ',']); } diff --git a/src/Value/Expression.php b/src/Value/Expression.php new file mode 100644 index 000000000..fb565e63c --- /dev/null +++ b/src/Value/Expression.php @@ -0,0 +1,27 @@ +consume('('); + $aArguments = parent::parseArguments($oParserState); + $mResult = new Expression('', $aArguments, ',', $oParserState->currentLine()); + $oParserState->consume(')'); + return $mResult; + } +} diff --git a/src/Value/Value.php b/src/Value/Value.php index 263c420da..be4c2f4db 100644 --- a/src/Value/Value.php +++ b/src/Value/Value.php @@ -164,6 +164,8 @@ public static function parsePrimitiveValue(ParserState $parserState) $value = LineName::parse($parserState); } elseif ($parserState->comes('U+')) { $value = self::parseUnicodeRangeValue($parserState); + } elseif ($parserState->comes('(')) { + $value = Expression::parse($parserState); } else { $nextCharacter = $parserState->peek(1); try { diff --git a/tests/ParserTest.php b/tests/ParserTest.php index 97e0d09b5..5a37c5dcd 100644 --- a/tests/ParserTest.php +++ b/tests/ParserTest.php @@ -435,6 +435,20 @@ public function functionSyntax(): void self::assertSame($expected, $document->render()); } + /** + * @test + */ + public function parseExpressions(): void + { + $oDoc = self::parsedStructureForFile('expressions'); + $sExpected = 'div {height: (vh - 10);}' + . "\n" + . 'div {height: (vh - 10)/2;}' + . "\n" + . 'div {height: max(5,(vh - 10));}'; + self::assertSame($sExpected, $oDoc->render()); + } + /** * @test */ @@ -593,8 +607,8 @@ public function calcNestedInFile(): void public function invalidCalcInFile(): void { $document = self::parsedStructureForFile('calc-invalid', Settings::create()->withMultibyteSupport(true)); - $expected = 'div {} -div {} + $expected = 'div {height: calc (25% - 1em);} +div {height: calc (25% - 1em);} div {} div {height: -moz-calc;} div {height: calc;}'; @@ -1177,6 +1191,19 @@ public function lonelyImport(): void self::assertSame($expected, $document->render()); } + /** + * @test + */ + public function functionArithmeticInFile(): void + { + $oDoc = self::parsedStructureForFile('function-arithmetic', Settings::create()->withMultibyteSupport(true)); + $sExpected = 'div {height: max(300,vh + 10);} +div {height: max(300,vh - 10);} +div {height: max(300,vh * 10);} +div {height: max(300,vh / 10);}'; + self::assertSame($sExpected, $oDoc->render()); + } + public function escapedSpecialCaseTokens(): void { $document = self::parsedStructureForFile('escaped-tokens'); diff --git a/tests/Unit/Value/ExpressionTest.php b/tests/Unit/Value/ExpressionTest.php new file mode 100644 index 000000000..05aeece55 --- /dev/null +++ b/tests/Unit/Value/ExpressionTest.php @@ -0,0 +1,69 @@ + + */ + public static function provideExpressions(): array + { + return [ + [ + 'input' => '(vh - 10) / 2', + 'expected_output' => '(vh - 10)/2', + 'expression_index' => 0, + ], + [ + 'input' => 'max(5, (vh - 10))', + 'expected_output' => 'max(5,(vh - 10))', + 'expression_index' => 1, + ], + ]; + } + + /** + * @test + * + * @dataProvider provideExpressions + */ + public function parseExpressions(string $input, string $expected, int $expression_index): void + { + $val = Value::parseValue( + new ParserState($input, Settings::create()), + $this->getDelimiters('height') + ); + + self::assertInstanceOf(ValueList::class, $val); + self::assertInstanceOf(Expression::class, $val->getListComponents()[$expression_index]); + self::assertSame($expected, $val->render(OutputFormat::createCompact())); + } + + /** + * @return list + */ + private function getDelimiters(string $rule): array + { + $closure = function ($rule) { + return self::listDelimiterForRule($rule); + }; + + $getter = $closure->bindTo(null, Rule::class); + return $getter($rule); + } +} diff --git a/tests/fixtures/expressions.css b/tests/fixtures/expressions.css new file mode 100644 index 000000000..50df58c96 --- /dev/null +++ b/tests/fixtures/expressions.css @@ -0,0 +1,11 @@ +div { + height: (vh - 10); +} + +div { + height: (vh - 10) / 2; +} + +div { + height: max(5, (vh - 10)); +} diff --git a/tests/fixtures/function-arithmetic.css b/tests/fixtures/function-arithmetic.css new file mode 100644 index 000000000..07a2c318f --- /dev/null +++ b/tests/fixtures/function-arithmetic.css @@ -0,0 +1,12 @@ +div { + height: max(300, vh + 10); +} +div { + height: max(300, vh - 10); +} +div { + height: max(300, vh * 10); +} +div { + height: max(300, vh / 10); +}