Skip to content

Commit 180786a

Browse files
committed
Do case-insensitive parsing
…as long as identifiers are concerned. > All CSS syntax is case-insensitive within the ASCII range (i.e., [a-z] and [A-Z] are equivalent) (http://www.w3.org/TR/CSS21/syndata.html#characters)
1 parent f4a4813 commit 180786a

File tree

3 files changed

+66
-36
lines changed

3 files changed

+66
-36
lines changed

lib/Sabberworm/CSS/Parser.php

Lines changed: 45 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ private function parseAtRule() {
8787
$this->consume('@');
8888
$sIdentifier = $this->parseIdentifier();
8989
$this->consumeWhiteSpace();
90-
if ($sIdentifier === 'import') {
90+
if ($this->streql($sIdentifier, 'import')) {
9191
$oLocation = $this->parseURLValue();
9292
$this->consumeWhiteSpace();
9393
$sMediaQuery = null;
@@ -96,7 +96,7 @@ private function parseAtRule() {
9696
}
9797
$this->consume(';');
9898
return new Import($oLocation, $sMediaQuery);
99-
} else if ($sIdentifier === 'charset') {
99+
} else if ($this->streql($sIdentifier, 'charset')) {
100100
$sCharset = $this->parseStringValue();
101101
$this->consumeWhiteSpace();
102102
$this->consume(';');
@@ -110,7 +110,7 @@ private function parseAtRule() {
110110
$this->consumeWhiteSpace();
111111
$this->parseList($oResult);
112112
return $oResult;
113-
} else if ($sIdentifier === 'namespace') {
113+
} else if ($this->streql($sIdentifier, 'namespace')) {
114114
$sPrefix = null;
115115
$mUrl = $this->parsePrimitiveValue();
116116
if (!$this->comes(';')) {
@@ -148,7 +148,7 @@ private function parseAtRule() {
148148
}
149149
}
150150

151-
private function parseIdentifier($bAllowFunctions = true) {
151+
private function parseIdentifier($bAllowFunctions = true, $bIgnoreCase = true) {
152152
$sResult = $this->parseCharacter(true);
153153
if ($sResult === null) {
154154
throw new UnexpectedTokenException($sResult, $this->peek(5), 'identifier');
@@ -157,6 +157,9 @@ private function parseIdentifier($bAllowFunctions = true) {
157157
while (($sCharacter = $this->parseCharacter(true)) !== null) {
158158
$sResult .= $sCharacter;
159159
}
160+
if ($bIgnoreCase) {
161+
$sResult = $this->strtolower($sResult);
162+
}
160163
if ($bAllowFunctions && $this->comes('(')) {
161164
$this->consume('(');
162165
$aArguments = $this->parseValue(array('=', ' ', ','));
@@ -287,10 +290,7 @@ private function parseRule() {
287290
if ($this->comes('!')) {
288291
$this->consume('!');
289292
$this->consumeWhiteSpace();
290-
$sImportantMarker = $this->consume(strlen('important'));
291-
if (mb_convert_case($sImportantMarker, MB_CASE_LOWER, $this->sCharset) !== 'important') {
292-
throw new \Exception("! was followed by “" . $sImportantMarker . "”. Expected “important”");
293-
}
293+
$this->consume('important');
294294
$oRule->setIsImportant(true);
295295
}
296296
while ($this->comes(';')) {
@@ -366,7 +366,7 @@ private function parsePrimitiveValue() {
366366
} else if ($this->comes("'") || $this->comes('"')) {
367367
$oValue = $this->parseStringValue();
368368
} else {
369-
$oValue = $this->parseIdentifier();
369+
$oValue = $this->parseIdentifier(true, false);
370370
}
371371
$this->consumeWhiteSpace();
372372
return $oValue;
@@ -385,22 +385,14 @@ private function parseNumericValue($bForColor = false) {
385385
}
386386
}
387387
$fSize = floatval($sSize);
388-
389388
$sUnit = null;
390-
$units = array(
391-
'%', 'em', 'ex', 'px', 'deg', 's', 'cm', 'pt', 'in', 'pc', 'cm',
392-
'mm',
393-
// These are non "size" values, but they are still numeric
394-
'deg', 'grad', 'rad', 'turns', 's', 'ms', 'Hz', 'kHz'
395-
);
396-
397-
foreach ($units as $val) {
398-
if ($this->comes($val)) {
399-
$sUnit = $this->consume($val);
389+
foreach(explode('/', Size::ABSOLUTE_SIZE_UNITS.'/'.Size::RELATIVE_SIZE_UNITS.'/'.Size::NON_SIZE_UNITS) as $sDefinedUnit) {
390+
if ($this->comes($sDefinedUnit, 0, true)) {
391+
$sUnit = $sDefinedUnit;
392+
$this->consume($sDefinedUnit);
400393
break;
401394
}
402395
}
403-
404396
return new Size($fSize, $sUnit, $bForColor);
405397
}
406398

@@ -450,15 +442,16 @@ private function parseURLValue() {
450442
/**
451443
* Tests an identifier for a given value. Since identifiers are all keywords, they can be vendor-prefixed. We need to check for these versions too.
452444
*/
453-
private static function identifierIs($sIdentifier, $sMatch) {
454-
return preg_match("/^(-\\w+-)?$sMatch$/", $sIdentifier) === 1;
445+
private static function identifierIs($sIdentifier, $sMatch, $bCaseInsensitive = true) {
446+
return preg_match("/^(-\\w+-)?$sMatch$/".($bCaseInsensitive ? 'i' : ''), $sIdentifier) === 1;
455447
}
456448

457-
private function comes($sString, $iOffset = 0) {
449+
private function comes($sString, $iOffset = 0, $bCaseInsensitive = true) {
458450
if ($this->isEnd()) {
459451
return false;
460452
}
461-
return $this->peek($sString, $iOffset) == $sString;
453+
$sPeek = $this->peek($sString, $iOffset);
454+
return $this->streql($sPeek, $sString, $bCaseInsensitive);
462455
}
463456

464457
private function peek($iLength = 1, $iOffset = 0) {
@@ -482,7 +475,7 @@ private function peek($iLength = 1, $iOffset = 0) {
482475
private function consume($mValue = 1) {
483476
if (is_string($mValue)) {
484477
$iLength = $this->strlen($mValue);
485-
if ($this->substr($this->sText, $this->iCurrentPosition, $iLength) !== $mValue) {
478+
if (!$this->streql($this->substr($this->sText, $this->iCurrentPosition, $iLength), $mValue)) {
486479
throw new UnexpectedTokenException($mValue, $this->peek(max($iLength, 5)));
487480
}
488481
$this->iCurrentPosition += $this->strlen($mValue);
@@ -530,7 +523,7 @@ private function consumeUntil($aEnd, $bIncludeEnd = false) {
530523
$aEnd = is_array($aEnd) ? $aEnd : array($aEnd);
531524
$iEndPos = null;
532525
foreach ($aEnd as $sEnd) {
533-
$iCurrentEndPos = $this->strpos($this->sText, $sEnd, $this->iCurrentPosition, $this->sCharset);
526+
$iCurrentEndPos = $this->strpos($this->sText, $sEnd, $this->iCurrentPosition);
534527
if($iCurrentEndPos === false) {
535528
continue;
536529
}
@@ -548,27 +541,43 @@ private function inputLeft() {
548541
return $this->substr($this->sText, $this->iCurrentPosition, -1);
549542
}
550543

551-
private function substr($string, $start, $length) {
544+
private function substr($sString, $iStart, $iLength) {
552545
if ($this->oParserSettings->bMultibyteSupport) {
553-
return mb_substr($string, $start, $length, $this->sCharset);
546+
return mb_substr($sString, $iStart, $iLength, $this->sCharset);
547+
} else {
548+
return substr($sString, $iStart, $iLength);
549+
}
550+
}
551+
552+
private function strlen($sString) {
553+
if ($this->oParserSettings->bMultibyteSupport) {
554+
return mb_strlen($sString, $this->sCharset);
555+
} else {
556+
return strlen($sString);
557+
}
558+
}
559+
560+
private function streql($sString1, $sString2, $bCaseInsensitive = true) {
561+
if($bCaseInsensitive) {
562+
return $this->strtolower($sString1) === $this->strtolower($sString2);
554563
} else {
555-
return substr($string, $start, $length);
564+
return $sString1 === $sString2;
556565
}
557566
}
558567

559-
private function strlen($text) {
568+
private function strtolower($sString) {
560569
if ($this->oParserSettings->bMultibyteSupport) {
561-
return mb_strlen($text, $this->sCharset);
570+
return mb_strtolower($sString, $this->sCharset);
562571
} else {
563-
return strlen($text);
572+
return strtolower($sString);
564573
}
565574
}
566575

567-
private function strpos($text, $needle, $offset, $charset) {
576+
private function strpos($sString, $sNeedle, $iOffset) {
568577
if ($this->oParserSettings->bMultibyteSupport) {
569-
return mb_strpos($text, $needle, $offset, $charset);
578+
return mb_strpos($sString, $sNeedle, $iOffset, $this->sCharset);
570579
} else {
571-
return strpos($text, $needle, $offset);
580+
return strpos($sString, $sNeedle, $iOffset);
572581
}
573582
}
574583

tests/Sabberworm/CSS/RuleSet/LenientParsingTest.php

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,4 +23,11 @@ public function testFaultToleranceOn() {
2323
$this->assertSame('.test1 {}'."\n".'.test2 {hello: 2;}'."\n", $oResult->__toString());
2424
}
2525

26+
public function testCaseInsensitivity() {
27+
$sFile = dirname(__FILE__) . '/../../../files' . DIRECTORY_SEPARATOR . "case-insensitivity.css";
28+
$oParser = new Parser(file_get_contents($sFile));
29+
$oResult = $oParser->parse();
30+
$this->assertSame('@charset "utf-8";@import url("test.css");@media screen {}#myid {case: insensitive !important;frequency: 30Hz;color: #ff0;color: hsl(40,40%,30%);font-family: Arial;}'."\n", $oResult->__toString());
31+
}
32+
2633
}

tests/files/case-insensitivity.css

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
@CharSet "utf-8";
2+
@IMPORT uRL(test.css);
3+
4+
@MEDIA screen {
5+
6+
}
7+
8+
#myid {
9+
CaSe: insensitive !imPORTANT;
10+
frequency: 30hz;
11+
color: RGB(255, 255, 0);
12+
color: hSL(40, 40%, 30%);
13+
font-Family: Arial; /* The value needs to remain capitalized */
14+
}

0 commit comments

Comments
 (0)