From 908bc46a688a9f99e12b5f271d5f8a1b2abc5e67 Mon Sep 17 00:00:00 2001 From: Ivailo Hristov Date: Mon, 19 Sep 2022 12:33:02 +0300 Subject: [PATCH 1/9] 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 00000000..a8f8916f --- /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 fe773c74..88f64e23 100644 --- a/src/Value/Value.php +++ b/src/Value/Value.php @@ -152,6 +152,8 @@ public static function parsePrimitiveValue(ParserState $oParserState) $oValue = LineName::parse($oParserState); } elseif ($oParserState->comes("U+")) { $oValue = self::parseUnicodeRangeValue($oParserState); + } elseif ($oParserState->comes("(")) { + $oValue = Expression::parse($oParserState); } else { $oValue = self::parseIdentifierOrFunction($oParserState); } diff --git a/tests/ParserTest.php b/tests/ParserTest.php index 5e2a3101..efb8883c 100644 --- a/tests/ParserTest.php +++ b/tests/ParserTest.php @@ -512,6 +512,20 @@ public function expandShorthands() self::assertSame($sExpected, $oDoc->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 00000000..50df58c9 --- /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 093c527264f81bd730880145f76392541e8d3332 Mon Sep 17 00:00:00 2001 From: Ivailo Hristov Date: Mon, 19 Sep 2022 17:47:22 +0300 Subject: [PATCH 2/9] Support arithmetic ops (+,-,*,/) in functions --- src/Value/CSSFunction.php | 33 +++++++++++++++++++++++++++++++-- 1 file changed, 31 insertions(+), 2 deletions(-) diff --git a/src/Value/CSSFunction.php b/src/Value/CSSFunction.php index 300dc3ec..19e7d33b 100644 --- a/src/Value/CSSFunction.php +++ b/src/Value/CSSFunction.php @@ -33,6 +33,35 @@ public function __construct($sName, $aArguments, $sSeparator = ',', $iLineNo = 0 parent::__construct($aArguments, $sSeparator, $iLineNo); } + /** + * @param ParserState $oParserState + * @param bool $bIgnoreCase + * + * @return string + * + * @throws SourceException + * @throws UnexpectedEOFException + * @throws UnexpectedTokenException + */ + public static function parseName(ParserState $oParserState, $bIgnoreCase = false) + { + return $oParserState->parseIdentifier($bIgnoreCase); + } + + /** + * @param ParserState $oParserState + * + * @return array + * + * @throws SourceException + * @throws UnexpectedEOFException + * @throws UnexpectedTokenException + */ + public static function parseArgs(ParserState $oParserState) + { + return Value::parseValue($oParserState, ['=', ' ', ',', '/', '*', '+', '-']); + } + /** * @param ParserState $oParserState * @param bool $bIgnoreCase @@ -45,9 +74,9 @@ public function __construct($sName, $aArguments, $sSeparator = ',', $iLineNo = 0 */ public static function parse(ParserState $oParserState, $bIgnoreCase = false) { - $mResult = $oParserState->parseIdentifier($bIgnoreCase); + $mResult = self::parseName($oParserState, $bIgnoreCase); $oParserState->consume('('); - $aArguments = Value::parseValue($oParserState, ['=', ' ', ',']); + $aArguments = self::parseArgs($oParserState); $mResult = new CSSFunction($mResult, $aArguments, ',', $oParserState->currentLine()); $oParserState->consume(')'); return $mResult; From f734920a8f698e6009b57cc8ca0dfb8b20f1d5ec Mon Sep 17 00:00:00 2001 From: Ivailo Hristov Date: Mon, 19 Sep 2022 17:56:22 +0300 Subject: [PATCH 3/9] 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 b6197700..0777af7e 100644 --- a/tests/ParserTest.php +++ b/tests/ParserTest.php @@ -1225,6 +1225,19 @@ public function lonelyImport() self::assertSame($sExpected, $oDoc->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() { $oDoc = $this->parsedStructureForFile('escaped-tokens'); diff --git a/tests/fixtures/function-arithmetic.css b/tests/fixtures/function-arithmetic.css new file mode 100644 index 00000000..07a2c318 --- /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 8627ef6895fafb1792ad72bff06dd7c9208992e1 Mon Sep 17 00:00:00 2001 From: Ivailo Hristov Date: Mon, 19 Sep 2022 18:05:02 +0300 Subject: [PATCH 4/9] 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 a8f8916f..dc3442fa 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 184b610dad161cbf72caf4464c22f102ee70e8dd Mon Sep 17 00:00:00 2001 From: Ivailo Hristov Date: Mon, 19 Sep 2022 19:00:08 +0300 Subject: [PATCH 5/9] Handle arithmetic operators in Value --- src/Value/CSSFunction.php | 2 +- src/Value/Value.php | 11 ++++++++++- tests/ParserTest.php | 8 ++++---- 3 files changed, 15 insertions(+), 6 deletions(-) diff --git a/src/Value/CSSFunction.php b/src/Value/CSSFunction.php index 19e7d33b..570dc0fc 100644 --- a/src/Value/CSSFunction.php +++ b/src/Value/CSSFunction.php @@ -59,7 +59,7 @@ public static function parseName(ParserState $oParserState, $bIgnoreCase = false */ public static function parseArgs(ParserState $oParserState) { - return Value::parseValue($oParserState, ['=', ' ', ',', '/', '*', '+', '-']); + return Value::parseValue($oParserState, ['=', ' ', ',']); } /** diff --git a/src/Value/Value.php b/src/Value/Value.php index a920396b..8efa5195 100644 --- a/src/Value/Value.php +++ b/src/Value/Value.php @@ -156,7 +156,16 @@ public static function parsePrimitiveValue(ParserState $oParserState) } elseif ($oParserState->comes("U+")) { $oValue = self::parseUnicodeRangeValue($oParserState); } else { - $oValue = self::parseIdentifierOrFunction($oParserState); + $sNextChar = $oParserState->peek(1); + try { + $oValue = self::parseIdentifierOrFunction($oParserState); + } catch (UnexpectedTokenException $e) { + if (in_array($sNextChar, ['+', '-', '*', '/'])) { + $oValue = $oParserState->consume(1); + } else { + throw $e; + } + } } $oParserState->consumeWhiteSpace(); return $oValue; diff --git a/tests/ParserTest.php b/tests/ParserTest.php index 0777af7e..791942fe 100644 --- a/tests/ParserTest.php +++ b/tests/ParserTest.php @@ -1231,10 +1231,10 @@ public function lonelyImport() 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 d9f89ccbb5904ef1d822f3b94e1e7aac884de165 Mon Sep 17 00:00:00 2001 From: Ivailo Hristov Date: Mon, 19 Sep 2022 19:06:29 +0300 Subject: [PATCH 6/9] Fix tests --- tests/ParserTest.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/ParserTest.php b/tests/ParserTest.php index 05789a5b..4fdcbd6c 100644 --- a/tests/ParserTest.php +++ b/tests/ParserTest.php @@ -702,8 +702,8 @@ public function calcNestedInFile() public function invalidCalcInFile() { $oDoc = self::parsedStructureForFile('calc-invalid', Settings::create()->withMultibyteSupport(true)); - $sExpected = 'div {} -div {} + $sExpected = 'div {height: calc (25% - 1em);} +div {height: calc (25% - 1em);} div {} div {height: -moz-calc;} div {height: calc;}'; From ca89a713d28b4b19d021bf40e8bc196b3d5eaa4b Mon Sep 17 00:00:00 2001 From: Ivailo Hristov Date: Tue, 20 Sep 2022 07:20:14 +0300 Subject: [PATCH 7/9] Make the check for arithmetic operators strict --- src/Value/Value.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Value/Value.php b/src/Value/Value.php index aa30e783..bb4c96c4 100644 --- a/src/Value/Value.php +++ b/src/Value/Value.php @@ -162,7 +162,7 @@ public static function parsePrimitiveValue(ParserState $oParserState) try { $oValue = self::parseIdentifierOrFunction($oParserState); } catch (UnexpectedTokenException $e) { - if (in_array($sNextChar, ['+', '-', '*', '/'])) { + if (in_array($sNextChar, ['+', '-', '*', '/'], true)) { $oValue = $oParserState->consume(1); } else { throw $e; From abbcc8f797ab5a37b5944b65b2f7d16a769a1306 Mon Sep 17 00:00:00 2001 From: raxbg Date: Wed, 10 Jul 2024 13:49:55 +0300 Subject: [PATCH 8/9] 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 dc3442fa..ac48cc25 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 14118a20cee6273a33087b305f026aefcea77989 Mon Sep 17 00:00:00 2001 From: raxbg Date: Wed, 10 Jul 2024 15:09:43 +0300 Subject: [PATCH 9/9] Add class-specific tests for CSS\Value\Expression --- tests/Value/ExpressionTest.php | 63 ++++++++++++++++++++++++++++++++++ 1 file changed, 63 insertions(+) create mode 100644 tests/Value/ExpressionTest.php diff --git a/tests/Value/ExpressionTest.php b/tests/Value/ExpressionTest.php new file mode 100644 index 00000000..4e2ad5f0 --- /dev/null +++ b/tests/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); + } +}