Skip to content

Commit 49168f3

Browse files
committed
Merge branch 'fix/invalid_calc_parsing' into mergable_master
2 parents 1a874ce + ee1f8fe commit 49168f3

File tree

7 files changed

+147
-29
lines changed

7 files changed

+147
-29
lines changed

lib/Sabberworm/CSS/Parsing/ParserState.php

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,15 @@ class ParserState
1818
private $sCharset;
1919
private $iLength;
2020
private $iLineNo;
21+
private $iAnchor;
2122

2223
public function __construct($sText, Settings $oParserSettings, $iLineNo = 1)
2324
{
2425
$this->oParserSettings = $oParserSettings;
2526
$this->sText = $sText;
2627
$this->iCurrentPosition = 0;
2728
$this->iLineNo = $iLineNo;
29+
$this->iAnchor = null;
2830
$this->setCharset($this->oParserSettings->sDefaultCharset);
2931
}
3032

@@ -58,14 +60,29 @@ public function getSettings()
5860
return $this->oParserSettings;
5961
}
6062

63+
public function setAnchor()
64+
{
65+
$this->iAnchor = $this->iCurrentPosition;
66+
}
67+
68+
public function backtrackToAnchor()
69+
{
70+
if ($this->iAnchor !== null) {
71+
$this->iCurrentPosition = $this->iAnchor;
72+
}
73+
}
74+
6175
public function parseIdentifier($bIgnoreCase = true)
6276
{
77+
if ($this->isEnd()) {
78+
throw new UnexpectedEOFException('', '', 'identifier', $this->iLineNo);
79+
}
6380
$sResult = $this->parseCharacter(true);
6481
if ($sResult === null) {
6582
throw new UnexpectedTokenException($sResult, $this->peek(5), 'identifier', $this->iLineNo);
6683
}
6784
$sCharacter = null;
68-
while (($sCharacter = $this->parseCharacter(true)) !== null) {
85+
while (!$this->isEnd() && ($sCharacter = $this->parseCharacter(true)) !== null) {
6986
if (preg_match('/[a-zA-Z0-9\x{00A0}-\x{FFFF}_-]/Sux', $sCharacter)) {
7087
$sResult .= $sCharacter;
7188
} else {

lib/Sabberworm/CSS/Value/CalcFunction.php

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,21 @@ class CalcFunction extends CSSFunction
1313
public static function parse(ParserState $oParserState)
1414
{
1515
$aOperators = ['+', '-', '*', '/'];
16-
$sFunction = trim($oParserState->consumeUntil('(', false, true));
16+
$sFunction = $oParserState->parseIdentifier();
17+
if ($oParserState->peek() != '(') {
18+
// Found ; or end of line before an opening bracket
19+
throw new UnexpectedTokenException('(', $oParserState->peek(), 'literal', $oParserState->currentLine());
20+
} else if (!in_array($sFunction, array('calc', '-moz-calc', '-webkit-calc'))) {
21+
// Found invalid calc definition. Example calc (...
22+
throw new UnexpectedTokenException('calc', $sFunction, 'literal', $oParserState->currentLine());
23+
}
24+
$oParserState->consume('(');
1725
$oCalcList = new CalcRuleValueList($oParserState->currentLine());
1826
$oList = new RuleValueList(',', $oParserState->currentLine());
1927
$iNestingLevel = 0;
2028
$iLastComponentType = null;
2129
while (!$oParserState->comes(')') || $iNestingLevel > 0) {
30+
if ($oParserState->isEnd() && $iNestingLevel === 0) break;
2231
$oParserState->consumeWhiteSpace();
2332
if ($oParserState->comes('(')) {
2433
$iNestingLevel++;
@@ -65,7 +74,9 @@ public static function parse(ParserState $oParserState)
6574
$oParserState->consumeWhiteSpace();
6675
}
6776
$oList->addListComponent($oCalcList);
68-
$oParserState->consume(')');
77+
if (!$oParserState->isEnd()) {
78+
$oParserState->consume(')');
79+
}
6980
return new CalcFunction($sFunction, $oList, ',', $oParserState->currentLine());
7081
}
7182
}

lib/Sabberworm/CSS/Value/URL.php

Lines changed: 23 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -17,19 +17,29 @@ public function __construct(CSSString $oURL, $iLineNo = 0)
1717

1818
public static function parse(ParserState $oParserState)
1919
{
20-
$bUseUrl = $oParserState->comes('url', true);
21-
if ($bUseUrl) {
22-
$oParserState->consume('url');
23-
$oParserState->consumeWhiteSpace();
24-
$oParserState->consume('(');
25-
}
26-
$oParserState->consumeWhiteSpace();
27-
$oResult = new URL(CSSString::parse($oParserState), $oParserState->currentLine());
28-
if ($bUseUrl) {
29-
$oParserState->consumeWhiteSpace();
30-
$oParserState->consume(')');
31-
}
32-
return $oResult;
20+
$oParserState->setAnchor();
21+
$sIdentifier = '';
22+
for ($i = 0; $i < 3; $i++) {
23+
$sChar = $oParserState->parseCharacter(true);
24+
if ($sChar === null) {
25+
break;
26+
}
27+
$sIdentifier .= $sChar;
28+
}
29+
$bUseUrl = $oParserState->streql($sIdentifier, 'url');
30+
if ($bUseUrl) {
31+
$oParserState->consumeWhiteSpace();
32+
$oParserState->consume('(');
33+
} else {
34+
$oParserState->backtrackToAnchor();
35+
}
36+
$oParserState->consumeWhiteSpace();
37+
$oResult = new URL(CSSString::parse($oParserState), $oParserState->currentLine());
38+
if ($bUseUrl) {
39+
$oParserState->consumeWhiteSpace();
40+
$oParserState->consume(')');
41+
}
42+
return $oResult;
3343
}
3444

3545

lib/Sabberworm/CSS/Value/Value.php

Lines changed: 15 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -75,16 +75,25 @@ public static function parseValue(ParserState $oParserState, $aListDelimiters =
7575

7676
public static function parseIdentifierOrFunction(ParserState $oParserState, $bIgnoreCase = false)
7777
{
78-
$sResult = $oParserState->parseIdentifier($bIgnoreCase);
78+
$oParserState->setAnchor();
79+
$mResult = $oParserState->parseIdentifier($bIgnoreCase);
7980

8081
if ($oParserState->comes('(')) {
81-
$oParserState->consume('(');
82-
$aArguments = Value::parseValue($oParserState, ['=', ' ', ',']);
83-
$sResult = new CSSFunction($sResult, $aArguments, ',', $oParserState->currentLine());
84-
$oParserState->consume(')');
82+
if ($oParserState->streql('url', $mResult)) {
83+
$oParserState->backtrackToAnchor();
84+
$mResult = URL::parse($oParserState);
85+
} else if ($oParserState->streql('calc', $mResult) || $oParserState->streql('-webkit-calc', $mResult) || $oParserState->streql('-moz-calc', $mResult)) {
86+
$oParserState->backtrackToAnchor();
87+
$mResult = CalcFunction::parse($oParserState);
88+
} else {
89+
$oParserState->consume('(');
90+
$aArguments = Value::parseValue($oParserState, array('=', ' ', ','));
91+
$mResult = new CSSFunction($mResult, $aArguments, ',', $oParserState->currentLine());
92+
$oParserState->consume(')');
93+
}
8594
}
8695

87-
return $sResult;
96+
return $mResult;
8897
}
8998

9099
public static function parsePrimitiveValue(ParserState $oParserState)
@@ -98,13 +107,6 @@ public static function parsePrimitiveValue(ParserState $oParserState)
98107
$oValue = Size::parse($oParserState);
99108
} elseif ($oParserState->comes('#') || $oParserState->comes('rgb', true) || $oParserState->comes('hsl', true)) {
100109
$oValue = Color::parse($oParserState);
101-
} elseif ($oParserState->comes('url', true)) {
102-
$oValue = URL::parse($oParserState);
103-
} elseif (
104-
$oParserState->comes('calc', true) || $oParserState->comes('-webkit-calc', true)
105-
|| $oParserState->comes('-moz-calc', true)
106-
) {
107-
$oValue = CalcFunction::parse($oParserState);
108110
} elseif ($oParserState->comes("'") || $oParserState->comes('"')) {
109111
$oValue = CSSString::parse($oParserState);
110112
} elseif ($oParserState->comes("progid:") && $oParserState->getSettings()->bLenientParsing) {

tests/Sabberworm/CSS/ParserTest.php

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -456,6 +456,44 @@ public function testCalcNestedInFile()
456456
$this->assertSame($sExpected, $oDoc->render());
457457
}
458458

459+
public function testInvalidCalcInFile()
460+
{
461+
$oDoc = $this->parsedStructureForFile('calc-invalid', Settings::create()->withMultibyteSupport(true));
462+
$sExpected = 'div {}
463+
div {}
464+
div {}
465+
div {height: -moz-calc;}
466+
div {height: calc;}';
467+
$this->assertSame($sExpected, $oDoc->render());
468+
}
469+
470+
public function testInvalidCalc()
471+
{
472+
$parser = new Parser('div { height: calc(100px');
473+
$oDoc = $parser->parse();
474+
$this->assertSame('div {height: calc(100px);}', $oDoc->render());
475+
476+
$parser = new Parser('div { height: calc(100px)');
477+
$oDoc = $parser->parse();
478+
$this->assertSame('div {height: calc(100px);}', $oDoc->render());
479+
480+
$parser = new Parser('div { height: calc(100px);');
481+
$oDoc = $parser->parse();
482+
$this->assertSame('div {height: calc(100px);}', $oDoc->render());
483+
484+
$parser = new Parser('div { height: calc(100px}');
485+
$oDoc = $parser->parse();
486+
$this->assertSame('div {}', $oDoc->render());
487+
488+
$parser = new Parser('div { height: calc(100px;');
489+
$oDoc = $parser->parse();
490+
$this->assertSame('div {}', $oDoc->render());
491+
492+
$parser = new Parser('div { height: calc(100px;}');
493+
$oDoc = $parser->parse();
494+
$this->assertSame('div {}', $oDoc->render());
495+
}
496+
459497
public function testGridLineNameInFile()
460498
{
461499
$oDoc = $this->parsedStructureForFile('grid-linename', Settings::create()->withMultibyteSupport(true));
@@ -853,4 +891,15 @@ public function testLonelyImport()
853891
$sExpected = "@import url(\"example.css\") only screen and (max-width: 600px);";
854892
$this->assertSame($sExpected, $oDoc->render());
855893
}
894+
895+
public function testEscapedSpecialCaseTokens()
896+
{
897+
$oDoc = $this->parsedStructureForFile('escaped-tokens');
898+
$contents = $oDoc->getContents();
899+
$rules = $contents[0]->getRules();
900+
$urlRule = $rules[0];
901+
$calcRule = $rules[1];
902+
$this->assertEquals(true, is_a($urlRule->getValue(), '\Sabberworm\CSS\Value\URL'));
903+
$this->assertEquals(true, is_a($calcRule->getValue(), '\Sabberworm\CSS\Value\CalcFunction'));
904+
}
856905
}

tests/files/calc-invalid.css

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
div{
2+
height: calc (25% - 1em);
3+
}
4+
5+
div{
6+
height: calc
7+
(25% - 1em);
8+
}
9+
10+
div{
11+
height: calc
12+
width: 100px
13+
}
14+
15+
div{
16+
height: -moz-calc;
17+
}
18+
19+
div{
20+
height: calc
21+
;
22+
}

tests/files/escaped-tokens.css

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
/**
2+
* 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
3+
*/
4+
body {
5+
background: u\rl("//example.org/picture.jpg");
6+
height: ca\lc(100% - 1px);
7+
}

0 commit comments

Comments
 (0)