Skip to content

Fix/invalid calc parsing #169

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

Merged
merged 15 commits into from
Sep 20, 2022
Merged
Show file tree
Hide file tree
Changes from all 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
34 changes: 34 additions & 0 deletions src/Parsing/Anchor.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<?php

namespace Sabberworm\CSS\Parsing;

class Anchor
{
/**
* @var int
*/
private $iPosition;

/**
* @var \Sabberworm\CSS\Parsing\ParserState
*/
private $oParserState;

/**
* @param int $iPosition
* @param \Sabberworm\CSS\Parsing\ParserState $oParserState
*/
public function __construct($iPosition, ParserState $oParserState)
{
$this->iPosition = $iPosition;
$this->oParserState = $oParserState;
}

/**
* @return void
*/
public function backtrack()
{
$this->oParserState->setPosition($this->iPosition);
}
}
23 changes: 22 additions & 1 deletion src/Parsing/ParserState.php
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,24 @@ public function getSettings()
return $this->oParserSettings;
}

/**
* @return \Sabberworm\CSS\Parsing\Anchor
*/
public function anchor()
{
return new Anchor($this->iCurrentPosition, $this);
}

/**
* @param int $iPosition
*
* @return void
*/
public function setPosition($iPosition)
{
$this->iCurrentPosition = $iPosition;
}

/**
* @param bool $bIgnoreCase
*
Expand All @@ -121,12 +139,15 @@ public function getSettings()
*/
public function parseIdentifier($bIgnoreCase = true)
{
if ($this->isEnd()) {
throw new UnexpectedEOFException('', '', 'identifier', $this->iLineNo);
}
$sResult = $this->parseCharacter(true);
if ($sResult === null) {
throw new UnexpectedTokenException($sResult, $this->peek(5), 'identifier', $this->iLineNo);
}
$sCharacter = null;
while (($sCharacter = $this->parseCharacter(true)) !== null) {
while (!$this->isEnd() && ($sCharacter = $this->parseCharacter(true)) !== null) {
if (preg_match('/[a-zA-Z0-9\x{00A0}-\x{FFFF}_-]/Sux', $sCharacter)) {
$sResult .= $sCharacter;
} else {
Expand Down
21 changes: 21 additions & 0 deletions src/Value/CSSFunction.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
namespace Sabberworm\CSS\Value;

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

/**
* A `CSSFunction` represents a special kind of value that also contains a function name and where the values are the
Expand Down Expand Up @@ -32,6 +33,26 @@ public function __construct($sName, $aArguments, $sSeparator = ',', $iLineNo = 0
parent::__construct($aArguments, $sSeparator, $iLineNo);
}

/**
* @param ParserState $oParserState
* @param bool $bIgnoreCase
*
* @return CSSFunction
*
* @throws SourceException
* @throws UnexpectedEOFException
* @throws UnexpectedTokenException
*/
public static function parse(ParserState $oParserState, $bIgnoreCase = false)
{
$mResult = $oParserState->parseIdentifier($bIgnoreCase);
$oParserState->consume('(');
$aArguments = Value::parseValue($oParserState, ['=', ' ', ',']);
$mResult = new CSSFunction($mResult, $aArguments, ',', $oParserState->currentLine());
$oParserState->consume(')');
return $mResult;
}

/**
* @return string
*/
Expand Down
23 changes: 20 additions & 3 deletions src/Value/CalcFunction.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,20 +19,35 @@ class CalcFunction extends CSSFunction
const T_OPERATOR = 2;

/**
* @param ParserState $oParserState
* @param bool $bIgnoreCase
*
* @return CalcFunction
*
* @throws UnexpectedTokenException
* @throws UnexpectedEOFException
*/
public static function parse(ParserState $oParserState)
public static function parse(ParserState $oParserState, $bIgnoreCase = false)
{
$aOperators = ['+', '-', '*', '/'];
$sFunction = trim($oParserState->consumeUntil('(', false, true));
$sFunction = $oParserState->parseIdentifier();
if ($oParserState->peek() != '(') {
// Found ; or end of line before an opening bracket
throw new UnexpectedTokenException('(', $oParserState->peek(), 'literal', $oParserState->currentLine());
} elseif (!in_array($sFunction, ['calc', '-moz-calc', '-webkit-calc'])) {
// Found invalid calc definition. Example calc (...
throw new UnexpectedTokenException('calc', $sFunction, 'literal', $oParserState->currentLine());
}
$oParserState->consume('(');
$oCalcList = new CalcRuleValueList($oParserState->currentLine());
$oList = new RuleValueList(',', $oParserState->currentLine());
$iNestingLevel = 0;
$iLastComponentType = null;
while (!$oParserState->comes(')') || $iNestingLevel > 0) {
if ($oParserState->isEnd() && $iNestingLevel === 0) {
break;
}

$oParserState->consumeWhiteSpace();
if ($oParserState->comes('(')) {
$iNestingLevel++;
Expand Down Expand Up @@ -83,7 +98,9 @@ public static function parse(ParserState $oParserState)
$oParserState->consumeWhiteSpace();
}
$oList->addListComponent($oCalcList);
$oParserState->consume(')');
if (!$oParserState->isEnd()) {
$oParserState->consume(')');
}
return new CalcFunction($sFunction, $oList, ',', $oParserState->currentLine());
}
}
5 changes: 4 additions & 1 deletion src/Value/Color.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,15 @@ public function __construct(array $aColor, $iLineNo = 0)
}

/**
* @param ParserState $oParserState
* @param bool $bIgnoreCase
*
* @return Color|CSSFunction
*
* @throws UnexpectedEOFException
* @throws UnexpectedTokenException
*/
public static function parse(ParserState $oParserState)
public static function parse(ParserState $oParserState, $bIgnoreCase = false)
{
$aColor = [];
if ($oParserState->comes('#')) {
Expand Down
14 changes: 12 additions & 2 deletions src/Value/URL.php
Original file line number Diff line number Diff line change
Expand Up @@ -36,11 +36,21 @@ public function __construct(CSSString $oURL, $iLineNo = 0)
*/
public static function parse(ParserState $oParserState)
{
$bUseUrl = $oParserState->comes('url', true);
$oAnchor = $oParserState->anchor();
$sIdentifier = '';
for ($i = 0; $i < 3; $i++) {
$sChar = $oParserState->parseCharacter(true);
if ($sChar === null) {
break;
}
$sIdentifier .= $sChar;
}
$bUseUrl = $oParserState->streql($sIdentifier, 'url');
if ($bUseUrl) {
$oParserState->consume('url');
$oParserState->consumeWhiteSpace();
$oParserState->consume('(');
} else {
$oAnchor->backtrack();
}
$oParserState->consumeWhiteSpace();
$oResult = new URL(CSSString::parse($oParserState), $oParserState->currentLine());
Expand Down
31 changes: 17 additions & 14 deletions src/Value/Value.php
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,8 @@ public static function parseValue(ParserState $oParserState, array $aListDelimit
while (
!($oParserState->comes('}') || $oParserState->comes(';') || $oParserState->comes('!')
|| $oParserState->comes(')')
|| $oParserState->comes('\\'))
|| $oParserState->comes('\\')
|| $oParserState->isEnd())
) {
if (count($aStack) > 0) {
$bFoundDelimiter = false;
Expand Down Expand Up @@ -105,16 +106,25 @@ public static function parseValue(ParserState $oParserState, array $aListDelimit
*/
public static function parseIdentifierOrFunction(ParserState $oParserState, $bIgnoreCase = false)
{
$sResult = $oParserState->parseIdentifier($bIgnoreCase);
$oAnchor = $oParserState->anchor();
$mResult = $oParserState->parseIdentifier($bIgnoreCase);

if ($oParserState->comes('(')) {
$oParserState->consume('(');
$aArguments = Value::parseValue($oParserState, ['=', ' ', ',']);
$sResult = new CSSFunction($sResult, $aArguments, ',', $oParserState->currentLine());
$oParserState->consume(')');
$oAnchor->backtrack();
if ($oParserState->streql('url', $mResult)) {
$mResult = URL::parse($oParserState);
} elseif (
$oParserState->streql('calc', $mResult)
|| $oParserState->streql('-webkit-calc', $mResult)
|| $oParserState->streql('-moz-calc', $mResult)
) {
$mResult = CalcFunction::parse($oParserState);
} else {
$mResult = CSSFunction::parse($oParserState, $bIgnoreCase);
}
}

return $sResult;
return $mResult;
}

/**
Expand All @@ -137,13 +147,6 @@ public static function parsePrimitiveValue(ParserState $oParserState)
$oValue = Size::parse($oParserState);
} elseif ($oParserState->comes('#') || $oParserState->comes('rgb', true) || $oParserState->comes('hsl', true)) {
$oValue = Color::parse($oParserState);
} elseif ($oParserState->comes('url', true)) {
$oValue = URL::parse($oParserState);
} elseif (
$oParserState->comes('calc', true) || $oParserState->comes('-webkit-calc', true)
|| $oParserState->comes('-moz-calc', true)
) {
$oValue = CalcFunction::parse($oParserState);
} elseif ($oParserState->comes("'") || $oParserState->comes('"')) {
$oValue = CSSString::parse($oParserState);
} elseif ($oParserState->comes("progid:") && $oParserState->getSettings()->bLenientParsing) {
Expand Down
55 changes: 55 additions & 0 deletions tests/ParserTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -682,6 +682,50 @@ public function calcNestedInFile()
self::assertSame($sExpected, $oDoc->render());
}

/**
* @test
*/
public function invalidCalcInFile()
{
$oDoc = self::parsedStructureForFile('calc-invalid', Settings::create()->withMultibyteSupport(true));
$sExpected = 'div {}
div {}
div {}
div {height: -moz-calc;}
div {height: calc;}';
self::assertSame($sExpected, $oDoc->render());
}

/**
* @test
*/
public function invalidCalc()
{
$parser = new Parser('div { height: calc(100px');
$oDoc = $parser->parse();
self::assertSame('div {height: calc(100px);}', $oDoc->render());

$parser = new Parser('div { height: calc(100px)');
$oDoc = $parser->parse();
self::assertSame('div {height: calc(100px);}', $oDoc->render());

$parser = new Parser('div { height: calc(100px);');
$oDoc = $parser->parse();
self::assertSame('div {height: calc(100px);}', $oDoc->render());

$parser = new Parser('div { height: calc(100px}');
$oDoc = $parser->parse();
self::assertSame('div {}', $oDoc->render());

$parser = new Parser('div { height: calc(100px;');
$oDoc = $parser->parse();
self::assertSame('div {}', $oDoc->render());

$parser = new Parser('div { height: calc(100px;}');
$oDoc = $parser->parse();
self::assertSame('div {}', $oDoc->render());
}

/**
* @test
*/
Expand Down Expand Up @@ -1180,4 +1224,15 @@ public function lonelyImport()
$sExpected = "@import url(\"example.css\") only screen and (max-width: 600px);";
self::assertSame($sExpected, $oDoc->render());
}

public function escapedSpecialCaseTokens()
{
$oDoc = $this->parsedStructureForFile('escaped-tokens');
$contents = $oDoc->getContents();
$rules = $contents[0]->getRules();
$urlRule = $rules[0];
$calcRule = $rules[1];
self::assertTrue(is_a($urlRule->getValue(), '\Sabberworm\CSS\Value\URL'));
self::assertTrue(is_a($calcRule->getValue(), '\Sabberworm\CSS\Value\CalcFunction'));
}
}
22 changes: 22 additions & 0 deletions tests/fixtures/calc-invalid.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
div{
height: calc (25% - 1em);
}

div{
height: calc
(25% - 1em);
}

div{
height: calc
width: 100px
}

div{
height: -moz-calc;
}

div{
height: calc
;
}
7 changes: 7 additions & 0 deletions tests/fixtures/escaped-tokens.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
/**
* Special case function-like tokens, with an escape backslash followed by a non-newline and non-hex digit character, should be parsed as the appropriate \Sabberworm\CSS\Value\ type
*/
body {
background: u\rl("//example.org/picture.jpg");
height: ca\lc(100% - 1px);
}