Skip to content

Commit 56b274d

Browse files
committed
Move parser settings to external settings class
1 parent 60cdc05 commit 56b274d

File tree

4 files changed

+163
-27
lines changed

4 files changed

+163
-27
lines changed

lib/Sabberworm/CSS/Parser.php

Lines changed: 28 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
use Sabberworm\CSS\Value\URL;
1919
use Sabberworm\CSS\Value\String;
2020
use Sabberworm\CSS\Rule\Rule;
21+
use Sabberworm\CSS\Parsing\UnexpectedTokenException;
2122

2223
/**
2324
* Parser class parses CSS from text into a data structure.
@@ -26,19 +27,17 @@ class Parser {
2627

2728
private $sText;
2829
private $iCurrentPosition;
30+
private $oParserSettings;
31+
private $sCharset;
2932
private $iLength;
3033

31-
/**
32-
* Should we use mb_* string functions
33-
*
34-
* @var bool
35-
*/
36-
private $bUseMbFunctions = TRUE;
37-
38-
public function __construct($sText, $sDefaultCharset = 'utf-8') {
34+
public function __construct($sText, Settings $oParserSettings = null) {
3935
$this->sText = $sText;
4036
$this->iCurrentPosition = 0;
41-
$this->setCharset($sDefaultCharset);
37+
if ($oParserSettings === null) {
38+
$oParserSettings = Settings::create();
39+
}
40+
$this->oParserSettings = $oParserSettings;
4241
}
4342

4443
public function setCharset($sCharset) {
@@ -51,15 +50,12 @@ public function getCharset() {
5150
}
5251

5352
public function parse() {
53+
$this->setCharset($this->oParserSettings->sDefaultCharset);
5454
$oResult = new Document();
5555
$this->parseDocument($oResult);
5656
return $oResult;
5757
}
5858

59-
public function setUseMbFlag($bFlag) {
60-
$this->bUseMbFunctions = (bool) $bFlag;
61-
}
62-
6359
private function parseDocument(Document $oDocument) {
6460
$this->consumeWhiteSpace();
6561
$this->parseList($oDocument, true);
@@ -112,7 +108,7 @@ private function parseAtRule() {
112108
$this->consume(';');
113109
$this->setCharset($sCharset->getString());
114110
return new Charset($sCharset);
115-
} else if(preg_match('/^(-\\w+-)?keyframes$/', $sIdentifier) === 1) {
111+
} else if (preg_match('/^(-\\w+-)?keyframes$/', $sIdentifier) === 1) {
116112
$oResult = new KeyFrame();
117113
$oResult->setVendorKeyFrame($sIdentifier);
118114
$oResult->setAnimationName(trim($this->consumeUntil('{')));
@@ -123,15 +119,15 @@ private function parseAtRule() {
123119
} else if ($sIdentifier === 'namespace') {
124120
$sPrefix = null;
125121
$mUrl = $this->parsePrimitiveValue();
126-
if(!$this->comes(';')) {
122+
if (!$this->comes(';')) {
127123
$sPrefix = $mUrl;
128124
$mUrl = $this->parsePrimitiveValue();
129125
}
130126
$this->consume(';');
131-
if($sPrefix !== null && !is_string($sPrefix)) {
127+
if ($sPrefix !== null && !is_string($sPrefix)) {
132128
throw new \Exception('Wrong namespace prefix '.$sPrefix);
133129
}
134-
if(!($mUrl instanceof String || $mUrl instanceof URL)) {
130+
if (!($mUrl instanceof String || $mUrl instanceof URL)) {
135131
throw new \Exception('Wrong namespace url of invalid type '.$mUrl);
136132
}
137133
return new CSSNamespace($mUrl, $sPrefix);
@@ -148,9 +144,9 @@ private function parseAtRule() {
148144
private function parseIdentifier($bAllowFunctions = true) {
149145
$sResult = $this->parseCharacter(true);
150146
if ($sResult === null) {
151-
throw new \Exception("Identifier expected, got {$this->peek(5)}");
147+
throw new UnexpectedTokenException($sResult, $this->peek(5), 'identifier');
152148
}
153-
$sCharacter;
149+
$sCharacter = null;
154150
while (($sCharacter = $this->parseCharacter(true)) !== null) {
155151
$sResult .= $sCharacter;
156152
}
@@ -265,7 +261,7 @@ private function parseRule() {
265261
$this->consume('!');
266262
$this->consumeWhiteSpace();
267263
$sImportantMarker = $this->consume(strlen('important'));
268-
if (mb_convert_case($sImportantMarker, MB_CASE_LOWER) !== 'important') {
264+
if (mb_convert_case($sImportantMarker, MB_CASE_LOWER, $this->sCharset) !== 'important') {
269265
throw new \Exception("! was followed by “" . $sImportantMarker . "”. Expected “important”");
270266
}
271267
$oRule->setIsImportant(true);
@@ -451,20 +447,25 @@ private function peek($iLength = 1, $iOffset = 0) {
451447
if (is_string($iOffset)) {
452448
$iOffset = $this->strlen($iOffset);
453449
}
454-
return $this->substr($this->sText, $this->iCurrentPosition + $iOffset, $iLength);
450+
$iOffset = $this->iCurrentPosition + $iOffset;
451+
if ($iOffset >= $this->iLength) {
452+
return '';
453+
}
454+
$iLength = min($iLength, $this->iLength-$iOffset);
455+
return $this->substr($this->sText, $iOffset, $iLength);
455456
}
456457

457458
private function consume($mValue = 1) {
458459
if (is_string($mValue)) {
459460
$iLength = $this->strlen($mValue);
460461
if ($this->substr($this->sText, $this->iCurrentPosition, $iLength) !== $mValue) {
461-
throw new \Exception("Expected $mValue, got " . $this->peek(5));
462+
throw new UnexpectedTokenException($mValue, $this->peek(max($iLength, 5)));
462463
}
463464
$this->iCurrentPosition += $this->strlen($mValue);
464465
return $mValue;
465466
} else {
466467
if ($this->iCurrentPosition + $mValue > $this->iLength) {
467-
throw new \Exception("Tried to consume $mValue chars, exceeded file end");
468+
throw new UnexpectedTokenException($mValue, $this->peek(5), 'count');
468469
}
469470
$sResult = $this->substr($this->sText, $this->iCurrentPosition, $mValue);
470471
$this->iCurrentPosition += $mValue;
@@ -477,7 +478,7 @@ private function consumeExpression($mExpression) {
477478
if (preg_match($mExpression, $this->inputLeft(), $aMatches, PREG_OFFSET_CAPTURE) === 1) {
478479
return $this->consume($aMatches[0][0]);
479480
}
480-
throw new \Exception("Expected pattern $mExpression not found, got: {$this->peek(5)}");
481+
throw new UnexpectedTokenException($mExpression, $this->peek(5), 'expression');
481482
}
482483

483484
private function consumeWhiteSpace() {
@@ -504,7 +505,7 @@ private function isEnd() {
504505
private function consumeUntil($sEnd) {
505506
$iEndPos = mb_strpos($this->sText, $sEnd, $this->iCurrentPosition, $this->sCharset);
506507
if ($iEndPos === false) {
507-
throw new \Exception("Required $sEnd not found, got {$this->peek(5)}");
508+
throw new UnexpectedTokenException($sEnd, $this->peek(5), 'search');
508509
}
509510
return $this->consume($iEndPos - $this->iCurrentPosition);
510511
}
@@ -514,15 +515,15 @@ private function inputLeft() {
514515
}
515516

516517
private function substr($string, $start, $length) {
517-
if ($this->bUseMbFunctions) {
518+
if ($this->oParserSettings->bMultibyteSupport) {
518519
return mb_substr($string, $start, $length, $this->sCharset);
519520
} else {
520521
return substr($string, $start, $length);
521522
}
522523
}
523524

524525
private function strlen($text) {
525-
if ($this->bUseMbFunctions) {
526+
if ($this->oParserSettings->bMultibyteSupport) {
526527
return mb_strlen($text, $this->sCharset);
527528
} else {
528529
return strlen($text);
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
<?php
2+
3+
namespace Sabberworm\CSS;
4+
5+
class OutputFormat {
6+
public $sStringQuotingType = '"';
7+
public $bNewlinesAfterRule = false;
8+
public $bNewlinesStartingRuleSets = false;
9+
public $sIndentationLevel = 0;
10+
public $sIndentation = "\t";
11+
12+
private $oFormatter = null;
13+
14+
public function __construct() {
15+
}
16+
17+
public function nextLevel() {
18+
$result = clone $this;
19+
$result->sIndentationLevel++;
20+
$result->oFormatter = null;
21+
return $result;
22+
}
23+
24+
public static function create() {
25+
return new OutputFormatter();
26+
}
27+
28+
public function getFormatter() {
29+
if($this->oFormatter === null) {
30+
$this->oFormatter = new OutputFormatter($this);
31+
}
32+
return $this->oFormatter;
33+
}
34+
}
35+
36+
class OutputFormatter {
37+
private $oFormat;
38+
39+
public function __construct(OutputFormat $oFormat) {
40+
$this->oFormat = $oFormat;
41+
}
42+
43+
public function newline($bOutput = false) {
44+
if($bOutput) {
45+
return $this->indent()."\n";
46+
}
47+
return '';
48+
}
49+
50+
public function indent() {
51+
return str_repeat($this->sIndentation, $this->sIndentationLevel);
52+
}
53+
54+
55+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
<?php
2+
3+
namespace Sabberworm\CSS\Parsing;
4+
5+
/**
6+
* Thrown if the CSS parsers encounters a token it did not expect
7+
*/
8+
class UnexpectedTokenException extends \Exception {
9+
private $sExpected;
10+
private $sFound;
11+
// Possible values: literal, identifier, count, expression, search
12+
private $sMatchType;
13+
14+
public function __construct($sExpected, $sFound, $sMatchType = 'literal') {
15+
$this->sExpected = $sExpected;
16+
$this->sFound = $sFound;
17+
$this->sMatchType = $sMatchType;
18+
$sMessage = "Token “{$sExpected}” ({$sMatchType}) not found. Got “{$sFound}”.";
19+
if($this->sMatchType === 'search') {
20+
$sMessage = "Search for “{$sExpected}” returned no results. Context: “{$sFound}”.";
21+
} else if($this->sMatchType === 'count') {
22+
$sMessage = "Next token was expected to have {$sExpected} chars. Context: “{$sFound}”.";
23+
} else if($this->sMatchType === 'identifier') {
24+
$sMessage = "Identifier expected. Got “{$sFound}";
25+
}
26+
parent::__construct($sMessage);
27+
}
28+
}

lib/Sabberworm/CSS/Settings.php

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
<?php
2+
3+
namespace Sabberworm\CSS;
4+
5+
use Sabberworm\CSS\Rule\Rule;
6+
7+
/**
8+
* Parser settings class.
9+
*
10+
* Configure parser behaviour here.
11+
*/
12+
class Settings {
13+
/**
14+
* Multi-byte string support. If true (default), will use (slower) mb_strlen, mb_convert_case, mb_substr and mb_strpos functions. Otherwise, the normal (ASCII-Only) functions will be used.
15+
*/
16+
public $bMultibyteSupport = true;
17+
18+
/**
19+
* The default charset for the CSS if no `@charset` rule is found. Defaults to utf-8.
20+
*/
21+
public $sDefaultCharset = 'utf-8';
22+
23+
/**
24+
* Lenient parsing. When used, the parser will not choke on unexpected tokens but simply ignore them.
25+
*/
26+
public $bLenientParsing = true;
27+
28+
private function __construct() {}
29+
30+
public static function create() {
31+
return new Settings();
32+
}
33+
34+
public function withMultibyteSupport($bMultibyteSupport = true) {
35+
$this->bMultibyteSupport = $bMultibyteSupport;
36+
return $this;
37+
}
38+
39+
public function withDefaultCharset($sDefaultCharset) {
40+
$this->sDefaultCharset = $sDefaultCharset;
41+
return $this;
42+
}
43+
44+
public function withLenientParsing($bLenientParsing = true) {
45+
$this->bLenientParsing = $bLenientParsing;
46+
return $this;
47+
}
48+
49+
public function beStrict() {
50+
return $this->withLenientParsing(false);
51+
}
52+
}

0 commit comments

Comments
 (0)