Skip to content

Commit 71cb230

Browse files
committed
Throw Exception when @charset appears where it shouldn’t
UnexpectedTokenException
1 parent 76af69e commit 71cb230

File tree

3 files changed

+74
-23
lines changed

3 files changed

+74
-23
lines changed

lib/Sabberworm/CSS/Parser.php

Lines changed: 39 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -78,30 +78,54 @@ private function parseDocument(Document $oDocument) {
7878

7979
private function parseList(CSSList $oList, $bIsRoot = false) {
8080
while (!$this->isEnd()) {
81-
if ($this->comes('@')) {
82-
$oList->append($this->parseAtRule());
83-
} else if ($this->comes('}')) {
84-
$this->consume('}');
85-
if ($bIsRoot) {
86-
throw new \Exception("Unopened {");
87-
} else {
88-
return;
81+
$oListItem = null;
82+
if($this->oParserSettings->bLenientParsing) {
83+
try {
84+
$oListItem = $this->parseListItem($oList, $bIsRoot);
85+
} catch (UnexpectedTokenException $e) {
86+
$oListItem = false;
8987
}
9088
} else {
91-
if($this->oParserSettings->bLenientParsing) {
92-
try {
93-
$oList->append($this->parseSelector());
94-
} catch (UnexpectedTokenException $e) {}
95-
} else {
96-
$oList->append($this->parseSelector());
97-
}
89+
$oListItem = $this->parseListItem($oList, $bIsRoot);
90+
}
91+
if($oListItem === null) {
92+
// List parsing finished
93+
return;
94+
}
95+
if($oListItem) {
96+
$oList->append($oListItem);
9897
}
9998
$this->consumeWhiteSpace();
10099
}
101100
if (!$bIsRoot) {
102101
throw new \Exception("Unexpected end of document");
103102
}
104103
}
104+
105+
private function parseListItem(CSSList $oList, $bIsRoot = false) {
106+
if ($this->comes('@')) {
107+
$oAtRule = $this->parseAtRule();
108+
if($oAtRule instanceof Charset) {
109+
if(!$bIsRoot) {
110+
throw new UnexpectedTokenException('@charset may only occur in root document', '', 'custom');
111+
}
112+
if(count($oList->getContents()) > 0) {
113+
throw new UnexpectedTokenException('@charset must be the first parseable token in a document', '', 'custom');
114+
}
115+
$this->setCharset($oAtRule->getCharset()->getString());
116+
}
117+
return $oAtRule;
118+
} else if ($this->comes('}')) {
119+
$this->consume('}');
120+
if ($bIsRoot) {
121+
throw new \Exception("Unopened {");
122+
} else {
123+
return null;
124+
}
125+
} else {
126+
return $this->parseSelector();
127+
}
128+
}
105129

106130
private function parseAtRule() {
107131
$this->consume('@');
@@ -120,7 +144,6 @@ private function parseAtRule() {
120144
$sCharset = $this->parseStringValue();
121145
$this->consumeWhiteSpace();
122146
$this->consume(';');
123-
$this->setCharset($sCharset->getString());
124147
return new Charset($sCharset);
125148
} else if ($this->identifierIs($sIdentifier, 'keyframes')) {
126149
$oResult = new KeyFrame();

lib/Sabberworm/CSS/Parsing/UnexpectedTokenException.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ public function __construct($sExpected, $sFound, $sMatchType = 'literal') {
2222
$sMessage = "Next token was expected to have {$sExpected} chars. Context: “{$sFound}”.";
2323
} else if($this->sMatchType === 'identifier') {
2424
$sMessage = "Identifier expected. Got “{$sFound}";
25+
} else if($this->sMatchType === 'custom') {
26+
$sMessage = trim("$sExpected $sFound");
2527
}
2628
parent::__construct($sMessage);
2729
}

tests/Sabberworm/CSS/ParserTest.php

Lines changed: 33 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -350,7 +350,7 @@ function testListValueRemoval() {
350350
$this->assertSame('@media screen {html {some: -test(val2);}}
351351
#unrelated {other: yes;}', $oDoc->render());
352352
}
353-
353+
354354
/**
355355
* @expectedException Sabberworm\CSS\Parsing\OutputException
356356
*/
@@ -377,17 +377,43 @@ function testComments() {
377377
$this->assertSame($sExpected, $oDoc->render());
378378
}
379379

380-
function testEmptyFile() {
381-
$oDoc = $this->parsedStructureForFile('-empty', Settings::create()->withMultibyteSupport(true));
380+
function testEmptyFile() {
381+
$oDoc = $this->parsedStructureForFile('-empty', Settings::create()->withMultibyteSupport(true));
382382
$sExpected = '';
383383
$this->assertSame($sExpected, $oDoc->render());
384-
}
384+
}
385385

386-
function testEmptyFileMbOff() {
387-
$oDoc = $this->parsedStructureForFile('-empty', Settings::create()->withMultibyteSupport(false));
386+
function testEmptyFileMbOff() {
387+
$oDoc = $this->parsedStructureForFile('-empty', Settings::create()->withMultibyteSupport(false));
388388
$sExpected = '';
389389
$this->assertSame($sExpected, $oDoc->render());
390-
}
390+
}
391+
392+
function testCharsetLenient1() {
393+
$oDoc = $this->parsedStructureForFile('-charset-after-rule', Settings::create()->withLenientParsing(true));
394+
$sExpected = '#id {prop: var(--val);}';
395+
$this->assertSame($sExpected, $oDoc->render());
396+
}
397+
398+
function testCharsetLenient2() {
399+
$oDoc = $this->parsedStructureForFile('-charset-in-block', Settings::create()->withLenientParsing(true));
400+
$sExpected = '@media print {}';
401+
$this->assertSame($sExpected, $oDoc->render());
402+
}
403+
404+
/**
405+
* @expectedException Sabberworm\CSS\Parsing\UnexpectedTokenException
406+
*/
407+
function testCharsetFailure1() {
408+
$this->parsedStructureForFile('-charset-after-rule', Settings::create()->withLenientParsing(false));
409+
}
410+
411+
/**
412+
* @expectedException Sabberworm\CSS\Parsing\UnexpectedTokenException
413+
*/
414+
function testCharsetFailure2() {
415+
$this->parsedStructureForFile('-charset-in-block', Settings::create()->withLenientParsing(false));
416+
}
391417

392418
function parsedStructureForFile($sFileName, $oSettings = null) {
393419
$sFile = dirname(__FILE__) . '/../../files' . DIRECTORY_SEPARATOR . "$sFileName.css";

0 commit comments

Comments
 (0)