From cd291373f496f9f4395565536c318516d85b973c Mon Sep 17 00:00:00 2001 From: Ivailo Hristov Date: Mon, 19 Sep 2022 12:33:02 +0300 Subject: [PATCH 01/11] [FEATURE] Parse simple expressions --- src/Value/Expression.php | 32 ++++++++++++++++++++++++++++++++ src/Value/Value.php | 2 ++ tests/ParserTest.php | 14 ++++++++++++++ tests/fixtures/expressions.css | 11 +++++++++++ 4 files changed, 59 insertions(+) create mode 100644 src/Value/Expression.php create mode 100644 tests/fixtures/expressions.css diff --git a/src/Value/Expression.php b/src/Value/Expression.php new file mode 100644 index 000000000..a8f8916fa --- /dev/null +++ b/src/Value/Expression.php @@ -0,0 +1,32 @@ +consume('('); + $aArguments = Value::parseValue($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..cf0cb0d03 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..6aa04fec1 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() + { + $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 */ 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)); +} From 0304df78f8452988e3616360ee8734e2642a088c Mon Sep 17 00:00:00 2001 From: Ivailo Hristov Date: Mon, 19 Sep 2022 17:56:22 +0300 Subject: [PATCH 02/11] [TASK] Add tests for arithmetic in functions --- tests/ParserTest.php | 13 +++++++++++++ tests/fixtures/function-arithmetic.css | 12 ++++++++++++ 2 files changed, 25 insertions(+) create mode 100644 tests/fixtures/function-arithmetic.css diff --git a/tests/ParserTest.php b/tests/ParserTest.php index 6aa04fec1..f917c72aa 100644 --- a/tests/ParserTest.php +++ b/tests/ParserTest.php @@ -1191,6 +1191,19 @@ public function lonelyImport(): void self::assertSame($expected, $document->render()); } + /** + * @test + */ + public function functionArithmeticInFile() + { + $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/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); +} From cb6dbddf1974f3082a0b25314fb82a38602d8267 Mon Sep 17 00:00:00 2001 From: Ivailo Hristov Date: Mon, 19 Sep 2022 18:05:02 +0300 Subject: [PATCH 03/11] Use CSSFunction::parseArgs when parsing expression arguments --- src/Value/Expression.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Value/Expression.php b/src/Value/Expression.php index a8f8916fa..dc3442fa8 100644 --- a/src/Value/Expression.php +++ b/src/Value/Expression.php @@ -24,7 +24,7 @@ class Expression extends CSSFunction public static function parse(ParserState $oParserState, $bIgnoreCase = false) { $oParserState->consume('('); - $aArguments = Value::parseValue($oParserState, ['=', ' ', ',']); + $aArguments = self::parseArgs($oParserState); $mResult = new Expression("", $aArguments, ',', $oParserState->currentLine()); $oParserState->consume(')'); return $mResult; From 8d7147ff92264b162bfcaeecfa08efcaf23f4802 Mon Sep 17 00:00:00 2001 From: Ivailo Hristov Date: Mon, 19 Sep 2022 19:00:08 +0300 Subject: [PATCH 04/11] [FEATURE] Handle arithmetic operators in Value --- tests/ParserTest.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/ParserTest.php b/tests/ParserTest.php index f917c72aa..1ca0156d0 100644 --- a/tests/ParserTest.php +++ b/tests/ParserTest.php @@ -1197,10 +1197,10 @@ public function lonelyImport(): void public function functionArithmeticInFile() { $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);}'; + $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()); } From 55b06f50d08cca0d92e72926e6d3749566327099 Mon Sep 17 00:00:00 2001 From: Ivailo Hristov Date: Mon, 19 Sep 2022 19:06:29 +0300 Subject: [PATCH 05/11] [TASK] Fix tests --- tests/ParserTest.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/ParserTest.php b/tests/ParserTest.php index 1ca0156d0..1d1c3b071 100644 --- a/tests/ParserTest.php +++ b/tests/ParserTest.php @@ -607,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;}'; From 08e63e7f44fa7ff2cc9824a5ef43539d0caba23f Mon Sep 17 00:00:00 2001 From: raxbg Date: Wed, 10 Jul 2024 13:49:55 +0300 Subject: [PATCH 06/11] Catch up with upstream/main --- src/Value/Expression.php | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/src/Value/Expression.php b/src/Value/Expression.php index dc3442fa8..ac48cc254 100644 --- a/src/Value/Expression.php +++ b/src/Value/Expression.php @@ -12,19 +12,14 @@ class Expression extends CSSFunction { /** - * @param ParserState $oParserState - * @param bool $bIgnoreCase - * - * @return Expression - * * @throws SourceException * @throws UnexpectedEOFException * @throws UnexpectedTokenException */ - public static function parse(ParserState $oParserState, $bIgnoreCase = false) + public static function parse(ParserState $oParserState, bool $bIgnoreCase = false): Expression { $oParserState->consume('('); - $aArguments = self::parseArgs($oParserState); + $aArguments = parent::parseArguments($oParserState); $mResult = new Expression("", $aArguments, ',', $oParserState->currentLine()); $oParserState->consume(')'); return $mResult; From abb4c474ccc8e308ba356463b881e03141fc9683 Mon Sep 17 00:00:00 2001 From: raxbg Date: Wed, 10 Jul 2024 15:09:43 +0300 Subject: [PATCH 07/11] [TASK] Add class-specific tests for CSS\Value\Expression --- tests/Unit/Value/ExpressionTest.php | 63 +++++++++++++++++++++++++++++ 1 file changed, 63 insertions(+) create mode 100644 tests/Unit/Value/ExpressionTest.php diff --git a/tests/Unit/Value/ExpressionTest.php b/tests/Unit/Value/ExpressionTest.php new file mode 100644 index 000000000..4e2ad5f0c --- /dev/null +++ b/tests/Unit/Value/ExpressionTest.php @@ -0,0 +1,63 @@ + + */ + 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, (string) $val); + } + + private function getDelimiters(string $rule): array + { + $closure = function($rule) { + return self::listDelimiterForRule($rule); + }; + + $getter = $closure->bindTo(null, Rule::class); + return $getter($rule); + } +} From 2d81c9e4f064f2822500b37821b7601245d46d5e Mon Sep 17 00:00:00 2001 From: Jake Hotson Date: Thu, 18 Dec 2025 01:31:18 +0000 Subject: [PATCH 08/11] Make `CSSFunction::parseArguments` protected (missed by rebase) --- src/Value/CSSFunction.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Value/CSSFunction.php b/src/Value/CSSFunction.php index 86b56d9b1..919f2ce1c 100644 --- a/src/Value/CSSFunction.php +++ b/src/Value/CSSFunction.php @@ -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, ['=', ' ', ',']); } From 14e6bee3d9c1ca9eee631b08b1b5ccf0f077e97d Mon Sep 17 00:00:00 2001 From: Jake Hotson Date: Thu, 18 Dec 2025 01:39:09 +0000 Subject: [PATCH 09/11] Fix tests not to use implicit cast to string --- tests/Unit/Value/ExpressionTest.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/Unit/Value/ExpressionTest.php b/tests/Unit/Value/ExpressionTest.php index 4e2ad5f0c..54adc4620 100644 --- a/tests/Unit/Value/ExpressionTest.php +++ b/tests/Unit/Value/ExpressionTest.php @@ -4,6 +4,7 @@ use PHPUnit\Framework\TestCase; use Sabberworm\CSS\Parsing\ParserState; +use Sabberworm\CSS\OutputFormat; use Sabberworm\CSS\Settings; use Sabberworm\CSS\Value\Value; use Sabberworm\CSS\Value\ValueList; @@ -48,7 +49,7 @@ public function parseExpressions(string $input, string $expected, int $expressio self::assertInstanceOf(ValueList::class, $val); self::assertInstanceOf(Expression::class, $val->getListComponents()[$expression_index]); - self::assertSame($expected, (string) $val); + self::assertSame($expected, $val->render(OutputFormat::createCompact())); } private function getDelimiters(string $rule): array From ad9cb11fcc826fdaa9dfe968a1b345763b404fba Mon Sep 17 00:00:00 2001 From: Jake Hotson Date: Thu, 18 Dec 2025 01:43:52 +0000 Subject: [PATCH 10/11] Fixing code issues reported by the tools --- src/Value/Expression.php | 5 +++-- src/Value/Value.php | 2 +- tests/ParserTest.php | 4 ++-- tests/Unit/Value/ExpressionTest.php | 6 ++++-- 4 files changed, 10 insertions(+), 7 deletions(-) diff --git a/src/Value/Expression.php b/src/Value/Expression.php index ac48cc254..8fc72765c 100644 --- a/src/Value/Expression.php +++ b/src/Value/Expression.php @@ -1,8 +1,9 @@ consume('('); $aArguments = parent::parseArguments($oParserState); - $mResult = new Expression("", $aArguments, ',', $oParserState->currentLine()); + $mResult = new Expression('', $aArguments, ',', $oParserState->currentLine()); $oParserState->consume(')'); return $mResult; } diff --git a/src/Value/Value.php b/src/Value/Value.php index cf0cb0d03..be4c2f4db 100644 --- a/src/Value/Value.php +++ b/src/Value/Value.php @@ -164,7 +164,7 @@ public static function parsePrimitiveValue(ParserState $parserState) $value = LineName::parse($parserState); } elseif ($parserState->comes('U+')) { $value = self::parseUnicodeRangeValue($parserState); - } elseif ($parserState->comes("(")) { + } elseif ($parserState->comes('(')) { $value = Expression::parse($parserState); } else { $nextCharacter = $parserState->peek(1); diff --git a/tests/ParserTest.php b/tests/ParserTest.php index 1d1c3b071..5a37c5dcd 100644 --- a/tests/ParserTest.php +++ b/tests/ParserTest.php @@ -438,7 +438,7 @@ public function functionSyntax(): void /** * @test */ - public function parseExpressions() + public function parseExpressions(): void { $oDoc = self::parsedStructureForFile('expressions'); $sExpected = 'div {height: (vh - 10);}' @@ -1194,7 +1194,7 @@ public function lonelyImport(): void /** * @test */ - public function functionArithmeticInFile() + public function functionArithmeticInFile(): void { $oDoc = self::parsedStructureForFile('function-arithmetic', Settings::create()->withMultibyteSupport(true)); $sExpected = 'div {height: max(300,vh + 10);} diff --git a/tests/Unit/Value/ExpressionTest.php b/tests/Unit/Value/ExpressionTest.php index 54adc4620..80f286d14 100644 --- a/tests/Unit/Value/ExpressionTest.php +++ b/tests/Unit/Value/ExpressionTest.php @@ -1,5 +1,7 @@ 'max(5, (vh - 10))', 'expected_output' => 'max(5,(vh - 10))', - 'expression_index' => 1 + 'expression_index' => 1, ], ]; } @@ -54,7 +56,7 @@ public function parseExpressions(string $input, string $expected, int $expressio private function getDelimiters(string $rule): array { - $closure = function($rule) { + $closure = function ($rule) { return self::listDelimiterForRule($rule); }; From bf16950539183e4675900b48ebcd4d3fd08c4993 Mon Sep 17 00:00:00 2001 From: Jake Hotson Date: Thu, 18 Dec 2025 02:06:20 +0000 Subject: [PATCH 11/11] Resolve PHPStan warnings --- config/phpstan-baseline.neon | 12 ++++++++++++ src/Value/CSSFunction.php | 4 ++-- src/Value/Expression.php | 5 ++--- tests/Unit/Value/ExpressionTest.php | 5 ++++- 4 files changed, 20 insertions(+), 6 deletions(-) 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 919f2ce1c..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 diff --git a/src/Value/Expression.php b/src/Value/Expression.php index 8fc72765c..fb565e63c 100644 --- a/src/Value/Expression.php +++ b/src/Value/Expression.php @@ -5,6 +5,7 @@ namespace Sabberworm\CSS\Value; use Sabberworm\CSS\Parsing\ParserState; +use Sabberworm\CSS\Parsing\SourceException; /** * An `Expression` represents a special kind of value that is comprised of multiple components wrapped in parenthesis. @@ -14,10 +15,8 @@ class Expression extends CSSFunction { /** * @throws SourceException - * @throws UnexpectedEOFException - * @throws UnexpectedTokenException */ - public static function parse(ParserState $oParserState, bool $bIgnoreCase = false): Expression + public static function parse(ParserState $oParserState, bool $bIgnoreCase = false): CSSFunction { $oParserState->consume('('); $aArguments = parent::parseArguments($oParserState); diff --git a/tests/Unit/Value/ExpressionTest.php b/tests/Unit/Value/ExpressionTest.php index 80f286d14..05aeece55 100644 --- a/tests/Unit/Value/ExpressionTest.php +++ b/tests/Unit/Value/ExpressionTest.php @@ -19,7 +19,7 @@ final class ExpressionTest extends TestCase { /** - * @return array<0, array{string: string}> + * @return list */ public static function provideExpressions(): array { @@ -54,6 +54,9 @@ public function parseExpressions(string $input, string $expected, int $expressio self::assertSame($expected, $val->render(OutputFormat::createCompact())); } + /** + * @return list + */ private function getDelimiters(string $rule): array { $closure = function ($rule) {