diff --git a/src/Parsing/ParserState.php b/src/Parsing/ParserState.php index 2427c5e1..099dab13 100644 --- a/src/Parsing/ParserState.php +++ b/src/Parsing/ParserState.php @@ -142,12 +142,36 @@ public function setPosition($iPosition) * * @throws UnexpectedTokenException */ - public function parseIdentifier($bIgnoreCase = true) + public function parseIdentifier($bIgnoreCase = true, $bNameStartCodePoint = true) { if ($this->isEnd()) { throw new UnexpectedEOFException('', '', 'identifier', $this->iLineNo); } - $sResult = $this->parseCharacter(true); + + $sResult = null; + $bCanParseCharacter = true; + + if ($bNameStartCodePoint) { + // Check if 3 code points would start an identifier. + // See . + $sNameStartCodePoint = '[a-zA-Z_]|[\x80-\xFF]'; + $sEscapeCode = '\\[^\r\n\f]'; + + if ( + ! ( + preg_match("/^-([-{$sNameStartCodePoint}]|{$sEscapeCode})/isSu", $this->peek(3)) || + preg_match("/^{$sNameStartCodePoint}/isSu", $this->peek()) || + preg_match("/^{$sEscapeCode}/isS", $this->peek(2)) + ) + ) { + $bCanParseCharacter = false; + } + } + + if ($bCanParseCharacter) { + $sResult = $this->parseCharacter(true); + } + if ($sResult === null) { throw new UnexpectedTokenException($sResult, $this->peek(5), 'identifier', $this->iLineNo); } @@ -211,14 +235,15 @@ public function parseCharacter($bIsForIdentifier) } if ($bIsForIdentifier) { $peek = ord($this->peek()); - // Ranges: a-z A-Z 0-9 - _ + $peek = ord($this->peek()); + // Matches a name code point. See . if ( - ($peek >= 97 && $peek <= 122) - || ($peek >= 65 && $peek <= 90) - || ($peek >= 48 && $peek <= 57) - || ($peek === 45) - || ($peek === 95) - || ($peek > 0xa1) + ($peek >= 97 && $peek <= 122) || + ($peek >= 65 && $peek <= 90) || + ($peek >= 48 && $peek <= 57) || + ($peek === 45) || + ($peek === 95) || + ($peek > 0x81) ) { return $this->consume(1); } diff --git a/src/Value/Color.php b/src/Value/Color.php index a002760b..26180c62 100644 --- a/src/Value/Color.php +++ b/src/Value/Color.php @@ -36,7 +36,7 @@ public static function parse(ParserState $oParserState, $bIgnoreCase = false) $aColor = []; if ($oParserState->comes('#')) { $oParserState->consume('#'); - $sValue = $oParserState->parseIdentifier(false); + $sValue = $oParserState->parseIdentifier(false, false); if ($oParserState->strlen($sValue) === 3) { $sValue = $sValue[0] . $sValue[0] . $sValue[1] . $sValue[1] . $sValue[2] . $sValue[2]; } elseif ($oParserState->strlen($sValue) === 4) { diff --git a/tests/ParserTest.php b/tests/ParserTest.php index c8e8ac76..1ec634f7 100644 --- a/tests/ParserTest.php +++ b/tests/ParserTest.php @@ -1261,6 +1261,29 @@ public function lonelyImport() self::assertSame($sExpected, $oDoc->render()); } + public function getInvalidIdentifiers() + { + return [ + ['body { -0-transition: all .3s ease-in-out; }'], + ['body { 4-o-transition: all .3s ease-in-out; }'], + ]; + } + + /** + * @dataProvider getInvalidIdentifiers + * + * @param string $css CSS text. + * @test + */ + public function invalidIdentifier($css) + { + $this->expectException(\Sabberworm\CSS\Parsing\UnexpectedTokenException::class); + + $oSettings = Settings::create()->withLenientParsing(false); + $oParser = new Parser($css, $oSettings); + $oParser->parse(); + } + public function escapedSpecialCaseTokens() { $oDoc = $this->parsedStructureForFile('escaped-tokens');