Skip to content

Commit 6ac546d

Browse files
committed
Merge branch 'master' into fix/invalid_calc_parsing
2 parents 5ed4383 + a80a97d commit 6ac546d

File tree

11 files changed

+116
-25
lines changed

11 files changed

+116
-25
lines changed

lib/Sabberworm/CSS/CSSList/CSSList.php

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
use Sabberworm\CSS\Parsing\ParserState;
77
use Sabberworm\CSS\Parsing\SourceException;
88
use Sabberworm\CSS\Parsing\UnexpectedTokenException;
9+
use Sabberworm\CSS\Parsing\UnexpectedEOFException;
910
use Sabberworm\CSS\Property\AtRule;
1011
use Sabberworm\CSS\Property\Charset;
1112
use Sabberworm\CSS\Property\CSSNamespace;
@@ -97,7 +98,7 @@ private static function parseListItem(ParserState $oParserState, CSSList $oList)
9798
}
9899
}
99100
} else {
100-
return DeclarationBlock::parse($oParserState);
101+
return DeclarationBlock::parse($oParserState, $oList);
101102
}
102103
}
103104

@@ -111,14 +112,14 @@ private static function parseAtRule(ParserState $oParserState) {
111112
$oParserState->consumeWhiteSpace();
112113
$sMediaQuery = null;
113114
if (!$oParserState->comes(';')) {
114-
$sMediaQuery = $oParserState->consumeUntil(';');
115+
$sMediaQuery = trim($oParserState->consumeUntil(array(';', ParserState::EOF)));
115116
}
116-
$oParserState->consume(';');
117-
return new Import($oLocation, $sMediaQuery, $iIdentifierLineNum);
117+
$oParserState->consumeUntil(array(';', ParserState::EOF), true, true);
118+
return new Import($oLocation, $sMediaQuery ? $sMediaQuery : null, $iIdentifierLineNum);
118119
} else if ($sIdentifier === 'charset') {
119120
$sCharset = CSSString::parse($oParserState);
120121
$oParserState->consumeWhiteSpace();
121-
$oParserState->consume(';');
122+
$oParserState->consumeUntil(array(';', ParserState::EOF), true, true);
122123
return new Charset($sCharset, $iIdentifierLineNum);
123124
} else if (self::identifierIs($sIdentifier, 'keyframes')) {
124125
$oResult = new KeyFrame($iIdentifierLineNum);
@@ -136,7 +137,7 @@ private static function parseAtRule(ParserState $oParserState) {
136137
$sPrefix = $mUrl;
137138
$mUrl = Value::parsePrimitiveValue($oParserState);
138139
}
139-
$oParserState->consume(';');
140+
$oParserState->consumeUntil(array(';', ParserState::EOF), true, true);
140141
if ($sPrefix !== null && !is_string($sPrefix)) {
141142
throw new UnexpectedTokenException('Wrong namespace prefix', $sPrefix, 'custom', $iIdentifierLineNum);
142143
}
@@ -238,10 +239,14 @@ public function remove($oItemToRemove) {
238239
* Replaces an item from the CSS list.
239240
* @param RuleSet|Import|Charset|CSSList $oItemToRemove May be a RuleSet (most likely a DeclarationBlock), a Import, a Charset or another CSSList (most likely a MediaQuery)
240241
*/
241-
public function replace($oOldItem, $oNewItem) {
242+
public function replace($oOldItem, $mNewItem) {
242243
$iKey = array_search($oOldItem, $this->aContents, true);
243244
if ($iKey !== false) {
244-
array_splice($this->aContents, $iKey, 1, $oNewItem);
245+
if (is_array($mNewItem)) {
246+
array_splice($this->aContents, $iKey, 1, $mNewItem);
247+
} else {
248+
array_splice($this->aContents, $iKey, 1, array($mNewItem));
249+
}
245250
return true;
246251
}
247252
return false;

lib/Sabberworm/CSS/Parsing/ParserState.php

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,12 @@
33

44
use Sabberworm\CSS\Comment\Comment;
55
use Sabberworm\CSS\Parsing\UnexpectedTokenException;
6+
use Sabberworm\CSS\Parsing\UnexpectedEOFException;
67
use Sabberworm\CSS\Settings;
78

89
class ParserState {
10+
const EOF = null;
11+
912
private $oParserSettings;
1013

1114
private $sText;
@@ -27,7 +30,9 @@ public function __construct($sText, Settings $oParserSettings, $iLineNo = 1) {
2730
public function setCharset($sCharset) {
2831
$this->sCharset = $sCharset;
2932
$this->aText = $this->strsplit($this->sText);
30-
$this->iLength = count($this->aText);
33+
if( is_array($this->aText) ) {
34+
$this->iLength = count($this->aText);
35+
}
3136
}
3237

3338
public function getCharset() {
@@ -116,8 +121,7 @@ public function consumeWhiteSpace() {
116121
if($this->oParserSettings->bLenientParsing) {
117122
try {
118123
$oComment = $this->consumeComment();
119-
} catch(UnexpectedTokenException $e) {
120-
// When we can’t find the end of a comment, we assume the document is finished.
124+
} catch(UnexpectedEOFException $e) {
121125
$this->iCurrentPosition = $this->iLength;
122126
return;
123127
}
@@ -158,7 +162,7 @@ public function consume($mValue = 1) {
158162
return $mValue;
159163
} else {
160164
if ($this->iCurrentPosition + $mValue > $this->iLength) {
161-
throw new UnexpectedTokenException($mValue, $this->peek(5), 'count', $this->iLineNo);
165+
throw new UnexpectedEOFException($mValue, $this->peek(5), 'count', $this->iLineNo);
162166
}
163167
$sResult = $this->substr($this->iCurrentPosition, $mValue);
164168
$iLineCount = substr_count($sResult, "\n");
@@ -212,7 +216,8 @@ public function consumeUntil($aEnd, $bIncludeEnd = false, $consumeEnd = false, a
212216
$out = '';
213217
$start = $this->iCurrentPosition;
214218

215-
while (($char = $this->consume(1)) !== '') {
219+
while (!$this->isEnd()) {
220+
$char = $this->consume(1);
216221
if (in_array($char, $aEnd)) {
217222
if ($bIncludeEnd) {
218223
$out .= $char;
@@ -227,8 +232,12 @@ public function consumeUntil($aEnd, $bIncludeEnd = false, $consumeEnd = false, a
227232
}
228233
}
229234

235+
if (in_array(self::EOF, $aEnd)) {
236+
return $out;
237+
}
238+
230239
$this->iCurrentPosition = $start;
231-
throw new UnexpectedTokenException('One of ("'.implode('","', $aEnd).'")', $this->peek(5), 'search', $this->iLineNo);
240+
throw new UnexpectedEOFException('One of ("'.implode('","', $aEnd).'")', $this->peek(5), 'search', $this->iLineNo);
232241
}
233242

234243
private function inputLeft() {
@@ -307,4 +316,4 @@ private function strpos($sString, $sNeedle, $iOffset) {
307316
return strpos($sString, $sNeedle, $iOffset);
308317
}
309318
}
310-
}
319+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
<?php
2+
3+
namespace Sabberworm\CSS\Parsing;
4+
5+
/**
6+
* Thrown if the CSS parsers encounters end of file it did not expect
7+
* Extends UnexpectedTokenException in order to preserve backwards compatibility
8+
*/
9+
class UnexpectedEOFException extends UnexpectedTokenException {}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
<?php
2+
3+
namespace Sabberworm\CSS\Property;
4+
5+
use Sabberworm\CSS\Parsing\UnexpectedTokenException;
6+
7+
class KeyframeSelector extends Selector {
8+
9+
//Regexes for specificity calculations
10+
11+
const SELECTOR_VALIDATION_RX = '/
12+
^(
13+
(?:
14+
[a-zA-Z0-9\x{00A0}-\x{FFFF}_^$|*="\'~\[\]()\-\s\.:#+>]* # any sequence of valid unescaped characters
15+
(?:\\\\.)? # a single escaped character
16+
(?:([\'"]).*?(?<!\\\\)\2)? # a quoted text like [id="example"]
17+
)*
18+
)|
19+
(\d+%) # keyframe animation progress percentage (e.g. 50%)
20+
$
21+
/ux';
22+
23+
}

lib/Sabberworm/CSS/Property/Selector.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ class Selector {
5151
private $iSpecificity;
5252

5353
public static function isValid($sSelector) {
54-
return preg_match(self::SELECTOR_VALIDATION_RX, $sSelector);
54+
return preg_match(static::SELECTOR_VALIDATION_RX, $sSelector);
5555
}
5656

5757
public function __construct($sSelector, $bCalculateSpecificity = false) {

lib/Sabberworm/CSS/RuleSet/DeclarationBlock.php

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,14 @@
66
use Sabberworm\CSS\Parsing\OutputException;
77
use Sabberworm\CSS\Parsing\UnexpectedTokenException;
88
use Sabberworm\CSS\Property\Selector;
9+
use Sabberworm\CSS\Property\KeyframeSelector;
910
use Sabberworm\CSS\Rule\Rule;
1011
use Sabberworm\CSS\Value\RuleValueList;
1112
use Sabberworm\CSS\Value\Value;
1213
use Sabberworm\CSS\Value\Size;
1314
use Sabberworm\CSS\Value\Color;
1415
use Sabberworm\CSS\Value\URL;
16+
use Sabberworm\CSS\CSSList\KeyFrame;
1517

1618
/**
1719
* Declaration blocks are the parts of a css file which denote the rules belonging to a selector.
@@ -26,7 +28,7 @@ public function __construct($iLineNo = 0) {
2628
$this->aSelectors = array();
2729
}
2830

29-
public static function parse(ParserState $oParserState) {
31+
public static function parse(ParserState $oParserState, $oList = NULL) {
3032
$aComments = array();
3133
$oResult = new DeclarationBlock($oParserState->currentLine());
3234
try {
@@ -42,7 +44,7 @@ public static function parse(ParserState $oParserState) {
4244
}
4345
}
4446
} while (!in_array($oParserState->peek(), array('{', '}')) || $sStringWrapperChar !== false);
45-
$oResult->setSelector(implode('', $aSelectorParts));
47+
$oResult->setSelector(implode('', $aSelectorParts), $oList);
4648
if ($oParserState->comes('{')) {
4749
$oParserState->consume(1);
4850
}
@@ -62,18 +64,25 @@ public static function parse(ParserState $oParserState) {
6264
}
6365

6466

65-
public function setSelectors($mSelector) {
67+
public function setSelectors($mSelector, $oList = NULL) {
6668
if (is_array($mSelector)) {
6769
$this->aSelectors = $mSelector;
6870
} else {
6971
$this->aSelectors = explode(',', $mSelector);
7072
}
7173
foreach ($this->aSelectors as $iKey => $mSelector) {
7274
if (!($mSelector instanceof Selector)) {
73-
if (!Selector::isValid($mSelector)) {
74-
throw new UnexpectedTokenException("Selector did not match '" . Selector::SELECTOR_VALIDATION_RX . "'.", $mSelector, "custom");
75+
if ($oList === NULL || !($oList instanceof KeyFrame)) {
76+
if (!Selector::isValid($mSelector)) {
77+
throw new UnexpectedTokenException("Selector did not match '" . Selector::SELECTOR_VALIDATION_RX . "'.", $mSelector, "custom");
78+
}
79+
$this->aSelectors[$iKey] = new Selector($mSelector);
80+
} else {
81+
if (!KeyframeSelector::isValid($mSelector)) {
82+
throw new UnexpectedTokenException("Selector did not match '" . KeyframeSelector::SELECTOR_VALIDATION_RX . "'.", $mSelector, "custom");
83+
}
84+
$this->aSelectors[$iKey] = new KeyframeSelector($mSelector);
7585
}
76-
$this->aSelectors[$iKey] = new Selector($mSelector);
7786
}
7887
}
7988
}
@@ -102,8 +111,8 @@ public function getSelector() {
102111
/**
103112
* @deprecated use setSelectors()
104113
*/
105-
public function setSelector($mSelector) {
106-
$this->setSelectors($mSelector);
114+
public function setSelector($mSelector, $oList = NULL) {
115+
$this->setSelectors($mSelector, $oList);
107116
}
108117

109118
/**

lib/Sabberworm/CSS/Value/Size.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,8 @@ public function __toString() {
115115
public function render(\Sabberworm\CSS\OutputFormat $oOutputFormat) {
116116
$l = localeconv();
117117
$sPoint = preg_quote($l['decimal_point'], '/');
118-
return preg_replace(array("/$sPoint/", "/^(-?)0\./"), array('.', '$1.'), $this->fSize) . ($this->sUnit === null ? '' : $this->sUnit);
118+
$sSize = preg_match("/[\d\.]+e[+-]?\d+/i", (string)$this->fSize) ? preg_replace("/$sPoint?0+$/", "", sprintf("%f", $this->fSize)) : $this->fSize;
119+
return preg_replace(array("/$sPoint/", "/^(-?)0\./"), array('.', '$1.'), $sSize) . ($this->sUnit === null ? '' : $this->sUnit);
119120
}
120121

121122
}

tests/Sabberworm/CSS/ParserTest.php

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -484,6 +484,14 @@ function testSelectorIgnoresInFile() {
484484
$this->assertSame($sExpected, $oDoc->render());
485485
}
486486

487+
function testKeyframeSelectors() {
488+
$oDoc = $this->parsedStructureForFile('keyframe-selector-validation', Settings::create()->withMultibyteSupport(true));
489+
$sExpected = '@-webkit-keyframes zoom {0% {-webkit-transform: scale(1,1);}
490+
50% {-webkit-transform: scale(1.2,1.2);}
491+
100% {-webkit-transform: scale(1,1);}}';
492+
$this->assertSame($sExpected, $oDoc->render());
493+
}
494+
487495
/**
488496
* @expectedException Sabberworm\CSS\Parsing\UnexpectedTokenException
489497
*/
@@ -756,4 +764,16 @@ function testMicrosoftFilterParsing() {
756764
$sExpected = ".test {filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=\"#80000000\",endColorstr=\"#00000000\",GradientType=1);}";
757765
$this->assertSame($sExpected, $oDoc->render());
758766
}
767+
768+
function testLargeSizeValuesInFile() {
769+
$oDoc = $this->parsedStructureForFile('large-z-index', Settings::create()->withMultibyteSupport(false));
770+
$sExpected = '.overlay {z-index: 10000000000000000000000;}';
771+
$this->assertSame($sExpected, $oDoc->render());
772+
}
773+
774+
function testLonelyImport() {
775+
$oDoc = $this->parsedStructureForFile('lonely-import');
776+
$sExpected = "@import url(\"example.css\") only screen and (max-width: 600px);";
777+
$this->assertSame($sExpected, $oDoc->render());
778+
}
759779
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
@-webkit-keyframes zoom {
2+
0% {
3+
-webkit-transform: scale(1,1);
4+
}
5+
50% {
6+
-webkit-transform: scale(1.2,1.2);
7+
}
8+
100% {
9+
-webkit-transform: scale(1,1);
10+
}
11+
}

tests/files/large-z-index.css

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
.overlay {
2+
z-index: 9999999999999999999999;
3+
}

tests/files/lonely-import.css

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
@import "example.css" only screen and (max-width: 600px)

0 commit comments

Comments
 (0)