Skip to content

Commit bfca845

Browse files
authored
Merge pull request #105 from Lullabot/line-number-support-in-css-objects
Add line number support in the sabberworm CSS parser Closes #101 as fixed
2 parents 7078b76 + 7132bed commit bfca845

28 files changed

+315
-91
lines changed

lib/Sabberworm/CSS/CSSList/AtRuleBlockList.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,8 @@ class AtRuleBlockList extends CSSBlockList implements AtRule {
1212
private $sType;
1313
private $sArgs;
1414

15-
public function __construct($sType, $sArgs = '') {
16-
parent::__construct();
15+
public function __construct($sType, $sArgs = '', $iLineNo = 0) {
16+
parent::__construct($iLineNo);
1717
$this->sType = $sType;
1818
$this->sArgs = $sArgs;
1919
}

lib/Sabberworm/CSS/CSSList/CSSBlockList.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,10 @@
1414
* Most CSSLists conform to this category but some at-rules (such as @keyframes) do not.
1515
*/
1616
abstract class CSSBlockList extends CSSList {
17+
public function __construct($iLineNo = 0) {
18+
parent::__construct($iLineNo);
19+
}
20+
1721
protected function allDeclarationBlocks(&$aResult) {
1822
foreach ($this->aContents as $mContent) {
1923
if ($mContent instanceof DeclarationBlock) {

lib/Sabberworm/CSS/CSSList/CSSList.php

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,23 +2,30 @@
22

33
namespace Sabberworm\CSS\CSSList;
44

5+
use Sabberworm\CSS\Renderable;
56
use Sabberworm\CSS\RuleSet\DeclarationBlock;
67
use Sabberworm\CSS\RuleSet\RuleSet;
78
use Sabberworm\CSS\Property\Selector;
8-
use Sabberworm\CSS\Rule\Rule;
9-
use Sabberworm\CSS\Value\ValueList;
10-
use Sabberworm\CSS\Value\CSSFunction;
119

1210
/**
1311
* A CSSList is the most generic container available. Its contents include RuleSet as well as other CSSList objects.
1412
* Also, it may contain Import and Charset objects stemming from @-rules.
1513
*/
16-
abstract class CSSList {
14+
abstract class CSSList implements Renderable {
1715

1816
protected $aContents;
17+
protected $iLineNo;
1918

20-
public function __construct() {
19+
public function __construct($iLineNo = 0) {
2120
$this->aContents = array();
21+
$this->iLineNo = $iLineNo;
22+
}
23+
24+
/**
25+
* @return int
26+
*/
27+
public function getLineNo() {
28+
return $this->iLineNo;
2229
}
2330

2431
public function append($oItem) {

lib/Sabberworm/CSS/CSSList/Document.php

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,13 @@
66
* The root CSSList of a parsed file. Contains all top-level css contents, mostly declaration blocks, but also any @-rules encountered.
77
*/
88
class Document extends CSSBlockList {
9+
/**
10+
* Document constructor.
11+
* @param int $iLineNo
12+
*/
13+
public function __construct($iLineNo = 0) {
14+
parent::__construct($iLineNo);
15+
}
916

1017
/**
1118
* Gets all DeclarationBlock objects recursively.

lib/Sabberworm/CSS/CSSList/KeyFrame.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,8 @@ class KeyFrame extends CSSList implements AtRule {
99
private $vendorKeyFrame;
1010
private $animationName;
1111

12-
public function __construct() {
13-
parent::__construct();
12+
public function __construct($iLineNo = 0) {
13+
parent::__construct($iLineNo);
1414
$this->vendorKeyFrame = null;
1515
$this->animationName = null;
1616
}

lib/Sabberworm/CSS/Parser.php

Lines changed: 45 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
use Sabberworm\CSS\CSSList\CSSList;
66
use Sabberworm\CSS\CSSList\Document;
77
use Sabberworm\CSS\CSSList\KeyFrame;
8+
use Sabberworm\CSS\Parsing\SourceException;
89
use Sabberworm\CSS\Property\AtRule;
910
use Sabberworm\CSS\Property\Import;
1011
use Sabberworm\CSS\Property\Charset;
@@ -34,10 +35,20 @@ class Parser {
3435
private $iLength;
3536
private $blockRules;
3637
private $aSizeUnits;
38+
private $iLineNo;
3739

38-
public function __construct($sText, Settings $oParserSettings = null) {
40+
/**
41+
* Parser constructor.
42+
* Note that that iLineNo starts from 1 and not 0
43+
*
44+
* @param $sText
45+
* @param Settings|null $oParserSettings
46+
* @param int $iLineNo
47+
*/
48+
public function __construct($sText, Settings $oParserSettings = null, $iLineNo = 1) {
3949
$this->sText = $sText;
4050
$this->iCurrentPosition = 0;
51+
$this->iLineNo = $iLineNo;
4152
if ($oParserSettings === null) {
4253
$oParserSettings = Settings::create();
4354
}
@@ -66,7 +77,7 @@ public function getCharset() {
6677

6778
public function parse() {
6879
$this->setCharset($this->oParserSettings->sDefaultCharset);
69-
$oResult = new Document();
80+
$oResult = new Document($this->iLineNo);
7081
$this->parseDocument($oResult);
7182
return $oResult;
7283
}
@@ -98,7 +109,7 @@ private function parseList(CSSList $oList, $bIsRoot = false) {
98109
$this->consumeWhiteSpace();
99110
}
100111
if (!$bIsRoot) {
101-
throw new \Exception("Unexpected end of document");
112+
throw new SourceException("Unexpected end of document", $this->iLineNo);
102113
}
103114
}
104115

@@ -107,18 +118,18 @@ private function parseListItem(CSSList $oList, $bIsRoot = false) {
107118
$oAtRule = $this->parseAtRule();
108119
if($oAtRule instanceof Charset) {
109120
if(!$bIsRoot) {
110-
throw new UnexpectedTokenException('@charset may only occur in root document', '', 'custom');
121+
throw new UnexpectedTokenException('@charset may only occur in root document', '', 'custom', $this->iLineNo);
111122
}
112123
if(count($oList->getContents()) > 0) {
113-
throw new UnexpectedTokenException('@charset must be the first parseable token in a document', '', 'custom');
124+
throw new UnexpectedTokenException('@charset must be the first parseable token in a document', '', 'custom', $this->iLineNo);
114125
}
115126
$this->setCharset($oAtRule->getCharset()->getString());
116127
}
117128
return $oAtRule;
118129
} else if ($this->comes('}')) {
119130
$this->consume('}');
120131
if ($bIsRoot) {
121-
throw new \Exception("Unopened {");
132+
throw new SourceException("Unopened {", $this->iLineNo);
122133
} else {
123134
return null;
124135
}
@@ -130,6 +141,7 @@ private function parseListItem(CSSList $oList, $bIsRoot = false) {
130141
private function parseAtRule() {
131142
$this->consume('@');
132143
$sIdentifier = $this->parseIdentifier();
144+
$iIdentifierLineNum = $this->iLineNo;
133145
$this->consumeWhiteSpace();
134146
if ($sIdentifier === 'import') {
135147
$oLocation = $this->parseURLValue();
@@ -139,14 +151,14 @@ private function parseAtRule() {
139151
$sMediaQuery = $this->consumeUntil(';');
140152
}
141153
$this->consume(';');
142-
return new Import($oLocation, $sMediaQuery);
154+
return new Import($oLocation, $sMediaQuery, $iIdentifierLineNum);
143155
} else if ($sIdentifier === 'charset') {
144156
$sCharset = $this->parseStringValue();
145157
$this->consumeWhiteSpace();
146158
$this->consume(';');
147-
return new Charset($sCharset);
159+
return new Charset($sCharset, $iIdentifierLineNum);
148160
} else if ($this->identifierIs($sIdentifier, 'keyframes')) {
149-
$oResult = new KeyFrame();
161+
$oResult = new KeyFrame($iIdentifierLineNum);
150162
$oResult->setVendorKeyFrame($sIdentifier);
151163
$oResult->setAnimationName(trim($this->consumeUntil('{', false, true)));
152164
$this->consumeWhiteSpace();
@@ -161,12 +173,12 @@ private function parseAtRule() {
161173
}
162174
$this->consume(';');
163175
if ($sPrefix !== null && !is_string($sPrefix)) {
164-
throw new UnexpectedTokenException('Wrong namespace prefix', $sPrefix, 'custom');
176+
throw new UnexpectedTokenException('Wrong namespace prefix', $sPrefix, 'custom', $iIdentifierLineNum);
165177
}
166178
if (!($mUrl instanceof CSSString || $mUrl instanceof URL)) {
167-
throw new UnexpectedTokenException('Wrong namespace url of invalid type', $mUrl, 'custom');
179+
throw new UnexpectedTokenException('Wrong namespace url of invalid type', $mUrl, 'custom', $iIdentifierLineNum);
168180
}
169-
return new CSSNamespace($mUrl, $sPrefix);
181+
return new CSSNamespace($mUrl, $sPrefix, $iIdentifierLineNum);
170182
} else {
171183
//Unknown other at rule (font-face or such)
172184
$sArgs = trim($this->consumeUntil('{', false, true));
@@ -179,10 +191,10 @@ private function parseAtRule() {
179191
}
180192
}
181193
if($bUseRuleSet) {
182-
$oAtRule = new AtRuleSet($sIdentifier, $sArgs);
194+
$oAtRule = new AtRuleSet($sIdentifier, $sArgs, $iIdentifierLineNum);
183195
$this->parseRuleSet($oAtRule);
184196
} else {
185-
$oAtRule = new AtRuleBlockList($sIdentifier, $sArgs);
197+
$oAtRule = new AtRuleBlockList($sIdentifier, $sArgs, $iIdentifierLineNum);
186198
$this->parseList($oAtRule);
187199
}
188200
return $oAtRule;
@@ -192,7 +204,7 @@ private function parseAtRule() {
192204
private function parseIdentifier($bAllowFunctions = true, $bIgnoreCase = true) {
193205
$sResult = $this->parseCharacter(true);
194206
if ($sResult === null) {
195-
throw new UnexpectedTokenException($sResult, $this->peek(5), 'identifier');
207+
throw new UnexpectedTokenException($sResult, $this->peek(5), 'identifier', $this->iLineNo);
196208
}
197209
$sCharacter = null;
198210
while (($sCharacter = $this->parseCharacter(true)) !== null) {
@@ -204,7 +216,7 @@ private function parseIdentifier($bAllowFunctions = true, $bIgnoreCase = true) {
204216
if ($bAllowFunctions && $this->comes('(')) {
205217
$this->consume('(');
206218
$aArguments = $this->parseValue(array('=', ' ', ','));
207-
$sResult = new CSSFunction($sResult, $aArguments);
219+
$sResult = new CSSFunction($sResult, $aArguments, ',', $this->iLineNo);
208220
$this->consume(')');
209221
}
210222
return $sResult;
@@ -232,13 +244,13 @@ private function parseStringValue() {
232244
while (!$this->comes($sQuote)) {
233245
$sContent = $this->parseCharacter(false);
234246
if ($sContent === null) {
235-
throw new \Exception("Non-well-formed quoted string {$this->peek(3)}");
247+
throw new SourceException("Non-well-formed quoted string {$this->peek(3)}", $this->iLineNo);
236248
}
237249
$sResult .= $sContent;
238250
}
239251
$this->consume($sQuote);
240252
}
241-
return new CSSString($sResult);
253+
return new CSSString($sResult, $this->iLineNo);
242254
}
243255

244256
private function parseCharacter($bIsForIdentifier) {
@@ -287,7 +299,7 @@ private function parseCharacter($bIsForIdentifier) {
287299
}
288300

289301
private function parseSelector() {
290-
$oResult = new DeclarationBlock();
302+
$oResult = new DeclarationBlock($this->iLineNo);
291303
$oResult->setSelector($this->consumeUntil('{', false, true));
292304
$this->consumeWhiteSpace();
293305
$this->parseRuleSet($oResult);
@@ -333,7 +345,7 @@ private function parseRuleSet($oRuleSet) {
333345
}
334346

335347
private function parseRule() {
336-
$oRule = new Rule($this->parseIdentifier());
348+
$oRule = new Rule($this->parseIdentifier(), $this->iLineNo);
337349
$this->consumeWhiteSpace();
338350
$this->consume(':');
339351
$oValue = $this->parseValue(self::listDelimiterForRule($oRule->getRule()));
@@ -387,7 +399,7 @@ private function parseValue($aListDelimiters) {
387399
break;
388400
}
389401
}
390-
$oList = new RuleValueList($sDelimiter);
402+
$oList = new RuleValueList($sDelimiter, $this->iLineNo);
391403
for ($i = $iStartPosition - 1; $i - $iStartPosition + 1 < $iLength * 2; $i+=2) {
392404
$oList->addListComponent($aStack[$i]);
393405
}
@@ -445,7 +457,7 @@ private function parseNumericValue($bForColor = false) {
445457
}
446458
}
447459
}
448-
return new Size(floatval($sSize), $sUnit, $bForColor);
460+
return new Size(floatval($sSize), $sUnit, $bForColor, $this->iLineNo);
449461
}
450462

451463
private function parseColorValue() {
@@ -456,7 +468,7 @@ private function parseColorValue() {
456468
if ($this->strlen($sValue) === 3) {
457469
$sValue = $sValue[0] . $sValue[0] . $sValue[1] . $sValue[1] . $sValue[2] . $sValue[2];
458470
}
459-
$aColor = array('r' => new Size(intval($sValue[0] . $sValue[1], 16), null, true), 'g' => new Size(intval($sValue[2] . $sValue[3], 16), null, true), 'b' => new Size(intval($sValue[4] . $sValue[5], 16), null, true));
471+
$aColor = array('r' => new Size(intval($sValue[0] . $sValue[1], 16), null, true, $this->iLineNo), 'g' => new Size(intval($sValue[2] . $sValue[3], 16), null, true, $this->iLineNo), 'b' => new Size(intval($sValue[4] . $sValue[5], 16), null, true, $this->iLineNo));
460472
} else {
461473
$sColorMode = $this->parseIdentifier(false);
462474
$this->consumeWhiteSpace();
@@ -472,7 +484,7 @@ private function parseColorValue() {
472484
}
473485
$this->consume(')');
474486
}
475-
return new Color($aColor);
487+
return new Color($aColor, $this->iLineNo);
476488
}
477489

478490
private function parseURLValue() {
@@ -483,7 +495,7 @@ private function parseURLValue() {
483495
$this->consume('(');
484496
}
485497
$this->consumeWhiteSpace();
486-
$oResult = new URL($this->parseStringValue());
498+
$oResult = new URL($this->parseStringValue(), $this->iLineNo);
487499
if ($bUseUrl) {
488500
$this->consumeWhiteSpace();
489501
$this->consume(')');
@@ -516,17 +528,21 @@ private function peek($iLength = 1, $iOffset = 0) {
516528

517529
private function consume($mValue = 1) {
518530
if (is_string($mValue)) {
531+
$iLineCount = substr_count($mValue, "\n");
519532
$iLength = $this->strlen($mValue);
520533
if (!$this->streql($this->substr($this->iCurrentPosition, $iLength), $mValue)) {
521-
throw new UnexpectedTokenException($mValue, $this->peek(max($iLength, 5)));
534+
throw new UnexpectedTokenException($mValue, $this->peek(max($iLength, 5)), $this->iLineNo);
522535
}
536+
$this->iLineNo += $iLineCount;
523537
$this->iCurrentPosition += $this->strlen($mValue);
524538
return $mValue;
525539
} else {
526540
if ($this->iCurrentPosition + $mValue > $this->iLength) {
527-
throw new UnexpectedTokenException($mValue, $this->peek(5), 'count');
541+
throw new UnexpectedTokenException($mValue, $this->peek(5), 'count', $this->iLineNo);
528542
}
529543
$sResult = $this->substr($this->iCurrentPosition, $mValue);
544+
$iLineCount = substr_count($sResult, "\n");
545+
$this->iLineNo += $iLineCount;
530546
$this->iCurrentPosition += $mValue;
531547
return $sResult;
532548
}
@@ -537,7 +553,7 @@ private function consumeExpression($mExpression) {
537553
if (preg_match($mExpression, $this->inputLeft(), $aMatches, PREG_OFFSET_CAPTURE) === 1) {
538554
return $this->consume($aMatches[0][0]);
539555
}
540-
throw new UnexpectedTokenException($mExpression, $this->peek(5), 'expression');
556+
throw new UnexpectedTokenException($mExpression, $this->peek(5), 'expression', $this->iLineNo);
541557
}
542558

543559
private function consumeWhiteSpace() {
@@ -595,7 +611,7 @@ private function consumeUntil($aEnd, $bIncludeEnd = false, $consumeEnd = false)
595611
}
596612

597613
$this->iCurrentPosition = $start;
598-
throw new UnexpectedTokenException('One of ("'.implode('","', $aEnd).'")', $this->peek(5), 'search');
614+
throw new UnexpectedTokenException('One of ("'.implode('","', $aEnd).'")', $this->peek(5), 'search', $this->iLineNo);
599615
}
600616

601617
private function inputLeft() {

lib/Sabberworm/CSS/Parsing/OutputException.php

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,5 +5,8 @@
55
/**
66
* Thrown if the CSS parsers attempts to print something invalid
77
*/
8-
class OutputException extends \Exception {
8+
class OutputException extends SourceException {
9+
public function __construct($sMessage, $iLineNo = 0) {
10+
parent::__construct($sMessage, $iLineNo);
11+
}
912
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
<?php
2+
3+
namespace Sabberworm\CSS\Parsing;
4+
5+
class SourceException extends \Exception {
6+
private $iLineNo;
7+
public function __construct($sMessage, $iLineNo = 0) {
8+
$this->iLineNo = $iLineNo;
9+
if (!empty($iLineNo)) {
10+
$sMessage .= " [line no: $iLineNo]";
11+
}
12+
parent::__construct($sMessage);
13+
}
14+
15+
public function getLineNo() {
16+
return $this->iLineNo;
17+
}
18+
}

0 commit comments

Comments
 (0)