Skip to content

Commit bbece12

Browse files
authored
Merge pull request #169 from raxbg/fix/invalid_calc_parsing
2 parents 81897e7 + 0678cff commit bbece12

File tree

10 files changed

+214
-21
lines changed

10 files changed

+214
-21
lines changed

src/Parsing/Anchor.php

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
<?php
2+
3+
namespace Sabberworm\CSS\Parsing;
4+
5+
class Anchor
6+
{
7+
/**
8+
* @var int
9+
*/
10+
private $iPosition;
11+
12+
/**
13+
* @var \Sabberworm\CSS\Parsing\ParserState
14+
*/
15+
private $oParserState;
16+
17+
/**
18+
* @param int $iPosition
19+
* @param \Sabberworm\CSS\Parsing\ParserState $oParserState
20+
*/
21+
public function __construct($iPosition, ParserState $oParserState)
22+
{
23+
$this->iPosition = $iPosition;
24+
$this->oParserState = $oParserState;
25+
}
26+
27+
/**
28+
* @return void
29+
*/
30+
public function backtrack()
31+
{
32+
$this->oParserState->setPosition($this->iPosition);
33+
}
34+
}

src/Parsing/ParserState.php

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,24 @@ public function getSettings()
112112
return $this->oParserSettings;
113113
}
114114

115+
/**
116+
* @return \Sabberworm\CSS\Parsing\Anchor
117+
*/
118+
public function anchor()
119+
{
120+
return new Anchor($this->iCurrentPosition, $this);
121+
}
122+
123+
/**
124+
* @param int $iPosition
125+
*
126+
* @return void
127+
*/
128+
public function setPosition($iPosition)
129+
{
130+
$this->iCurrentPosition = $iPosition;
131+
}
132+
115133
/**
116134
* @param bool $bIgnoreCase
117135
*
@@ -121,12 +139,15 @@ public function getSettings()
121139
*/
122140
public function parseIdentifier($bIgnoreCase = true)
123141
{
142+
if ($this->isEnd()) {
143+
throw new UnexpectedEOFException('', '', 'identifier', $this->iLineNo);
144+
}
124145
$sResult = $this->parseCharacter(true);
125146
if ($sResult === null) {
126147
throw new UnexpectedTokenException($sResult, $this->peek(5), 'identifier', $this->iLineNo);
127148
}
128149
$sCharacter = null;
129-
while (($sCharacter = $this->parseCharacter(true)) !== null) {
150+
while (!$this->isEnd() && ($sCharacter = $this->parseCharacter(true)) !== null) {
130151
if (preg_match('/[a-zA-Z0-9\x{00A0}-\x{FFFF}_-]/Sux', $sCharacter)) {
131152
$sResult .= $sCharacter;
132153
} else {

src/Value/CSSFunction.php

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
namespace Sabberworm\CSS\Value;
44

55
use Sabberworm\CSS\OutputFormat;
6+
use Sabberworm\CSS\Parsing\ParserState;
67

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

36+
/**
37+
* @param ParserState $oParserState
38+
* @param bool $bIgnoreCase
39+
*
40+
* @return CSSFunction
41+
*
42+
* @throws SourceException
43+
* @throws UnexpectedEOFException
44+
* @throws UnexpectedTokenException
45+
*/
46+
public static function parse(ParserState $oParserState, $bIgnoreCase = false)
47+
{
48+
$mResult = $oParserState->parseIdentifier($bIgnoreCase);
49+
$oParserState->consume('(');
50+
$aArguments = Value::parseValue($oParserState, ['=', ' ', ',']);
51+
$mResult = new CSSFunction($mResult, $aArguments, ',', $oParserState->currentLine());
52+
$oParserState->consume(')');
53+
return $mResult;
54+
}
55+
3556
/**
3657
* @return string
3758
*/

src/Value/CalcFunction.php

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,20 +19,35 @@ class CalcFunction extends CSSFunction
1919
const T_OPERATOR = 2;
2020

2121
/**
22+
* @param ParserState $oParserState
23+
* @param bool $bIgnoreCase
24+
*
2225
* @return CalcFunction
2326
*
2427
* @throws UnexpectedTokenException
2528
* @throws UnexpectedEOFException
2629
*/
27-
public static function parse(ParserState $oParserState)
30+
public static function parse(ParserState $oParserState, $bIgnoreCase = false)
2831
{
2932
$aOperators = ['+', '-', '*', '/'];
30-
$sFunction = trim($oParserState->consumeUntil('(', false, true));
33+
$sFunction = $oParserState->parseIdentifier();
34+
if ($oParserState->peek() != '(') {
35+
// Found ; or end of line before an opening bracket
36+
throw new UnexpectedTokenException('(', $oParserState->peek(), 'literal', $oParserState->currentLine());
37+
} elseif (!in_array($sFunction, ['calc', '-moz-calc', '-webkit-calc'])) {
38+
// Found invalid calc definition. Example calc (...
39+
throw new UnexpectedTokenException('calc', $sFunction, 'literal', $oParserState->currentLine());
40+
}
41+
$oParserState->consume('(');
3142
$oCalcList = new CalcRuleValueList($oParserState->currentLine());
3243
$oList = new RuleValueList(',', $oParserState->currentLine());
3344
$iNestingLevel = 0;
3445
$iLastComponentType = null;
3546
while (!$oParserState->comes(')') || $iNestingLevel > 0) {
47+
if ($oParserState->isEnd() && $iNestingLevel === 0) {
48+
break;
49+
}
50+
3651
$oParserState->consumeWhiteSpace();
3752
if ($oParserState->comes('(')) {
3853
$iNestingLevel++;
@@ -83,7 +98,9 @@ public static function parse(ParserState $oParserState)
8398
$oParserState->consumeWhiteSpace();
8499
}
85100
$oList->addListComponent($oCalcList);
86-
$oParserState->consume(')');
101+
if (!$oParserState->isEnd()) {
102+
$oParserState->consume(')');
103+
}
87104
return new CalcFunction($sFunction, $oList, ',', $oParserState->currentLine());
88105
}
89106
}

src/Value/Color.php

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,12 +23,15 @@ public function __construct(array $aColor, $iLineNo = 0)
2323
}
2424

2525
/**
26+
* @param ParserState $oParserState
27+
* @param bool $bIgnoreCase
28+
*
2629
* @return Color|CSSFunction
2730
*
2831
* @throws UnexpectedEOFException
2932
* @throws UnexpectedTokenException
3033
*/
31-
public static function parse(ParserState $oParserState)
34+
public static function parse(ParserState $oParserState, $bIgnoreCase = false)
3235
{
3336
$aColor = [];
3437
if ($oParserState->comes('#')) {

src/Value/URL.php

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,11 +36,21 @@ public function __construct(CSSString $oURL, $iLineNo = 0)
3636
*/
3737
public static function parse(ParserState $oParserState)
3838
{
39-
$bUseUrl = $oParserState->comes('url', true);
39+
$oAnchor = $oParserState->anchor();
40+
$sIdentifier = '';
41+
for ($i = 0; $i < 3; $i++) {
42+
$sChar = $oParserState->parseCharacter(true);
43+
if ($sChar === null) {
44+
break;
45+
}
46+
$sIdentifier .= $sChar;
47+
}
48+
$bUseUrl = $oParserState->streql($sIdentifier, 'url');
4049
if ($bUseUrl) {
41-
$oParserState->consume('url');
4250
$oParserState->consumeWhiteSpace();
4351
$oParserState->consume('(');
52+
} else {
53+
$oAnchor->backtrack();
4454
}
4555
$oParserState->consumeWhiteSpace();
4656
$oResult = new URL(CSSString::parse($oParserState), $oParserState->currentLine());

src/Value/Value.php

Lines changed: 17 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,8 @@ public static function parseValue(ParserState $oParserState, array $aListDelimit
4444
while (
4545
!($oParserState->comes('}') || $oParserState->comes(';') || $oParserState->comes('!')
4646
|| $oParserState->comes(')')
47-
|| $oParserState->comes('\\'))
47+
|| $oParserState->comes('\\')
48+
|| $oParserState->isEnd())
4849
) {
4950
if (count($aStack) > 0) {
5051
$bFoundDelimiter = false;
@@ -105,16 +106,25 @@ public static function parseValue(ParserState $oParserState, array $aListDelimit
105106
*/
106107
public static function parseIdentifierOrFunction(ParserState $oParserState, $bIgnoreCase = false)
107108
{
108-
$sResult = $oParserState->parseIdentifier($bIgnoreCase);
109+
$oAnchor = $oParserState->anchor();
110+
$mResult = $oParserState->parseIdentifier($bIgnoreCase);
109111

110112
if ($oParserState->comes('(')) {
111-
$oParserState->consume('(');
112-
$aArguments = Value::parseValue($oParserState, ['=', ' ', ',']);
113-
$sResult = new CSSFunction($sResult, $aArguments, ',', $oParserState->currentLine());
114-
$oParserState->consume(')');
113+
$oAnchor->backtrack();
114+
if ($oParserState->streql('url', $mResult)) {
115+
$mResult = URL::parse($oParserState);
116+
} elseif (
117+
$oParserState->streql('calc', $mResult)
118+
|| $oParserState->streql('-webkit-calc', $mResult)
119+
|| $oParserState->streql('-moz-calc', $mResult)
120+
) {
121+
$mResult = CalcFunction::parse($oParserState);
122+
} else {
123+
$mResult = CSSFunction::parse($oParserState, $bIgnoreCase);
124+
}
115125
}
116126

117-
return $sResult;
127+
return $mResult;
118128
}
119129

120130
/**
@@ -137,13 +147,6 @@ public static function parsePrimitiveValue(ParserState $oParserState)
137147
$oValue = Size::parse($oParserState);
138148
} elseif ($oParserState->comes('#') || $oParserState->comes('rgb', true) || $oParserState->comes('hsl', true)) {
139149
$oValue = Color::parse($oParserState);
140-
} elseif ($oParserState->comes('url', true)) {
141-
$oValue = URL::parse($oParserState);
142-
} elseif (
143-
$oParserState->comes('calc', true) || $oParserState->comes('-webkit-calc', true)
144-
|| $oParserState->comes('-moz-calc', true)
145-
) {
146-
$oValue = CalcFunction::parse($oParserState);
147150
} elseif ($oParserState->comes("'") || $oParserState->comes('"')) {
148151
$oValue = CSSString::parse($oParserState);
149152
} elseif ($oParserState->comes("progid:") && $oParserState->getSettings()->bLenientParsing) {

tests/ParserTest.php

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -682,6 +682,50 @@ public function calcNestedInFile()
682682
self::assertSame($sExpected, $oDoc->render());
683683
}
684684

685+
/**
686+
* @test
687+
*/
688+
public function invalidCalcInFile()
689+
{
690+
$oDoc = self::parsedStructureForFile('calc-invalid', Settings::create()->withMultibyteSupport(true));
691+
$sExpected = 'div {}
692+
div {}
693+
div {}
694+
div {height: -moz-calc;}
695+
div {height: calc;}';
696+
self::assertSame($sExpected, $oDoc->render());
697+
}
698+
699+
/**
700+
* @test
701+
*/
702+
public function invalidCalc()
703+
{
704+
$parser = new Parser('div { height: calc(100px');
705+
$oDoc = $parser->parse();
706+
self::assertSame('div {height: calc(100px);}', $oDoc->render());
707+
708+
$parser = new Parser('div { height: calc(100px)');
709+
$oDoc = $parser->parse();
710+
self::assertSame('div {height: calc(100px);}', $oDoc->render());
711+
712+
$parser = new Parser('div { height: calc(100px);');
713+
$oDoc = $parser->parse();
714+
self::assertSame('div {height: calc(100px);}', $oDoc->render());
715+
716+
$parser = new Parser('div { height: calc(100px}');
717+
$oDoc = $parser->parse();
718+
self::assertSame('div {}', $oDoc->render());
719+
720+
$parser = new Parser('div { height: calc(100px;');
721+
$oDoc = $parser->parse();
722+
self::assertSame('div {}', $oDoc->render());
723+
724+
$parser = new Parser('div { height: calc(100px;}');
725+
$oDoc = $parser->parse();
726+
self::assertSame('div {}', $oDoc->render());
727+
}
728+
685729
/**
686730
* @test
687731
*/
@@ -1195,4 +1239,15 @@ public function lonelyImport()
11951239
$sExpected = "@import url(\"example.css\") only screen and (max-width: 600px);";
11961240
self::assertSame($sExpected, $oDoc->render());
11971241
}
1242+
1243+
public function escapedSpecialCaseTokens()
1244+
{
1245+
$oDoc = $this->parsedStructureForFile('escaped-tokens');
1246+
$contents = $oDoc->getContents();
1247+
$rules = $contents[0]->getRules();
1248+
$urlRule = $rules[0];
1249+
$calcRule = $rules[1];
1250+
self::assertTrue(is_a($urlRule->getValue(), '\Sabberworm\CSS\Value\URL'));
1251+
self::assertTrue(is_a($calcRule->getValue(), '\Sabberworm\CSS\Value\CalcFunction'));
1252+
}
11981253
}

tests/fixtures/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/fixtures/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)