Skip to content

Parse simple expressions #389

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 13 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 31 additions & 2 deletions src/Value/CSSFunction.php
Original file line number Diff line number Diff line change
Expand Up @@ -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, ['=', ' ', ',']);
}

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice refactoring. Could this be put through as a separate PR?

/**
* @param ParserState $oParserState
* @param bool $bIgnoreCase
Expand All @@ -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;
Expand Down
32 changes: 32 additions & 0 deletions src/Value/Expression.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<?php

namespace Sabberworm\CSS\Value;

use Sabberworm\CSS\OutputFormat;
use Sabberworm\CSS\Parsing\ParserState;

/**
* An `Expression` represents a special kind of value that is comprised of multiple components wrapped in parenthesis.
* Examle `height: (vh - 10);`.
*/
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)
{
$oParserState->consume('(');
$aArguments = self::parseArgs($oParserState);
$mResult = new Expression("", $aArguments, ',', $oParserState->currentLine());
$oParserState->consume(')');
return $mResult;
}
}
Copy link
Collaborator

@JakeQZ JakeQZ Feb 15, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There should be a class-specific unit test for this new class. The overall parser tests are good, but we also want to make sure each individual class behaves as it should.

Copy link
Collaborator

@JakeQZ JakeQZ Feb 15, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@raxbg, would you be willing to create a corresponding TestExpression class to test this new class as a standalone?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There should be a class-specific unit test for this new class. The overall parser tests are good, but we also want to make sure each individual class behaves as it should.

Makes sense. How does that look like: 14118a2

13 changes: 12 additions & 1 deletion src/Value/Value.php
Original file line number Diff line number Diff line change
Expand Up @@ -155,8 +155,19 @@ 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);
$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;
Expand Down
31 changes: 29 additions & 2 deletions tests/ParserTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
*/
Expand Down Expand Up @@ -688,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;}';
Expand Down Expand Up @@ -1240,6 +1254,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);}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AFAICT, vh without a number in front is still not valid and I’d prefer it would throw an error in strict mode.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Indeed, it is invalid, however this seems to be outside of the scope of these changes. I guess we need a separate issue for this. Also is it a concern of the parser to validate the semantic meaning of identifiers it encounters?

Copy link
Collaborator

@JakeQZ JakeQZ Feb 15, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also is it a concern of the parser to validate the semantic meaning of identifiers it encounters?

I agree it's not.

Moving forwards, we've agreed to replace strict mode parsing with a logging mechanism. And try to replicate what most browsers do when encountering invalid syntax. A unit with no number in front should therefore probably be parsed as zero of that unit. And, as a bonus, a separate PR would log the lack of a number.

So, for now, we don't need to worry about strict mode, but should create issues for things that could be logged in future.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also is it a concern of the parser to validate the semantic meaning of identifiers it encounters?

Looking again, this is syntax, which is a concern of the parser. A unit without a length is syntactically invalid, and should be discarded (along with the entire property declaration).

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');
Expand Down
11 changes: 11 additions & 0 deletions tests/fixtures/expressions.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
div {
height: (vh - 10);
}

div {
height: (vh - 10) / 2;
}

div {
height: max(5, (vh - 10));
}
12 changes: 12 additions & 0 deletions tests/fixtures/function-arithmetic.css
Original file line number Diff line number Diff line change
@@ -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);
}