Skip to content

Commit

Permalink
Merge pull request #105 from Lullabot/line-number-support-in-css-objects
Browse files Browse the repository at this point in the history
Add line number support in the sabberworm CSS parser

Closes #101 as fixed
  • Loading branch information
sabberworm authored Jun 30, 2016
2 parents 7078b76 + 7132bed commit bfca845
Show file tree
Hide file tree
Showing 28 changed files with 315 additions and 91 deletions.
4 changes: 2 additions & 2 deletions lib/Sabberworm/CSS/CSSList/AtRuleBlockList.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ class AtRuleBlockList extends CSSBlockList implements AtRule {
private $sType;
private $sArgs;

public function __construct($sType, $sArgs = '') {
parent::__construct();
public function __construct($sType, $sArgs = '', $iLineNo = 0) {
parent::__construct($iLineNo);
$this->sType = $sType;
$this->sArgs = $sArgs;
}
Expand Down
4 changes: 4 additions & 0 deletions lib/Sabberworm/CSS/CSSList/CSSBlockList.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@
* Most CSSLists conform to this category but some at-rules (such as @keyframes) do not.
*/
abstract class CSSBlockList extends CSSList {
public function __construct($iLineNo = 0) {
parent::__construct($iLineNo);
}

protected function allDeclarationBlocks(&$aResult) {
foreach ($this->aContents as $mContent) {
if ($mContent instanceof DeclarationBlock) {
Expand Down
17 changes: 12 additions & 5 deletions lib/Sabberworm/CSS/CSSList/CSSList.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,23 +2,30 @@

namespace Sabberworm\CSS\CSSList;

use Sabberworm\CSS\Renderable;
use Sabberworm\CSS\RuleSet\DeclarationBlock;
use Sabberworm\CSS\RuleSet\RuleSet;
use Sabberworm\CSS\Property\Selector;
use Sabberworm\CSS\Rule\Rule;
use Sabberworm\CSS\Value\ValueList;
use Sabberworm\CSS\Value\CSSFunction;

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

protected $aContents;
protected $iLineNo;

public function __construct() {
public function __construct($iLineNo = 0) {
$this->aContents = array();
$this->iLineNo = $iLineNo;
}

/**
* @return int
*/
public function getLineNo() {
return $this->iLineNo;
}

public function append($oItem) {
Expand Down
7 changes: 7 additions & 0 deletions lib/Sabberworm/CSS/CSSList/Document.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,13 @@
* The root CSSList of a parsed file. Contains all top-level css contents, mostly declaration blocks, but also any @-rules encountered.
*/
class Document extends CSSBlockList {
/**
* Document constructor.
* @param int $iLineNo
*/
public function __construct($iLineNo = 0) {
parent::__construct($iLineNo);
}

/**
* Gets all DeclarationBlock objects recursively.
Expand Down
4 changes: 2 additions & 2 deletions lib/Sabberworm/CSS/CSSList/KeyFrame.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ class KeyFrame extends CSSList implements AtRule {
private $vendorKeyFrame;
private $animationName;

public function __construct() {
parent::__construct();
public function __construct($iLineNo = 0) {
parent::__construct($iLineNo);
$this->vendorKeyFrame = null;
$this->animationName = null;
}
Expand Down
74 changes: 45 additions & 29 deletions lib/Sabberworm/CSS/Parser.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
use Sabberworm\CSS\CSSList\CSSList;
use Sabberworm\CSS\CSSList\Document;
use Sabberworm\CSS\CSSList\KeyFrame;
use Sabberworm\CSS\Parsing\SourceException;
use Sabberworm\CSS\Property\AtRule;
use Sabberworm\CSS\Property\Import;
use Sabberworm\CSS\Property\Charset;
Expand Down Expand Up @@ -34,10 +35,20 @@ class Parser {
private $iLength;
private $blockRules;
private $aSizeUnits;
private $iLineNo;

public function __construct($sText, Settings $oParserSettings = null) {
/**
* Parser constructor.
* Note that that iLineNo starts from 1 and not 0
*
* @param $sText
* @param Settings|null $oParserSettings
* @param int $iLineNo
*/
public function __construct($sText, Settings $oParserSettings = null, $iLineNo = 1) {
$this->sText = $sText;
$this->iCurrentPosition = 0;
$this->iLineNo = $iLineNo;
if ($oParserSettings === null) {
$oParserSettings = Settings::create();
}
Expand Down Expand Up @@ -66,7 +77,7 @@ public function getCharset() {

public function parse() {
$this->setCharset($this->oParserSettings->sDefaultCharset);
$oResult = new Document();
$oResult = new Document($this->iLineNo);
$this->parseDocument($oResult);
return $oResult;
}
Expand Down Expand Up @@ -98,7 +109,7 @@ private function parseList(CSSList $oList, $bIsRoot = false) {
$this->consumeWhiteSpace();
}
if (!$bIsRoot) {
throw new \Exception("Unexpected end of document");
throw new SourceException("Unexpected end of document", $this->iLineNo);
}
}

Expand All @@ -107,18 +118,18 @@ private function parseListItem(CSSList $oList, $bIsRoot = false) {
$oAtRule = $this->parseAtRule();
if($oAtRule instanceof Charset) {
if(!$bIsRoot) {
throw new UnexpectedTokenException('@charset may only occur in root document', '', 'custom');
throw new UnexpectedTokenException('@charset may only occur in root document', '', 'custom', $this->iLineNo);
}
if(count($oList->getContents()) > 0) {
throw new UnexpectedTokenException('@charset must be the first parseable token in a document', '', 'custom');
throw new UnexpectedTokenException('@charset must be the first parseable token in a document', '', 'custom', $this->iLineNo);
}
$this->setCharset($oAtRule->getCharset()->getString());
}
return $oAtRule;
} else if ($this->comes('}')) {
$this->consume('}');
if ($bIsRoot) {
throw new \Exception("Unopened {");
throw new SourceException("Unopened {", $this->iLineNo);
} else {
return null;
}
Expand All @@ -130,6 +141,7 @@ private function parseListItem(CSSList $oList, $bIsRoot = false) {
private function parseAtRule() {
$this->consume('@');
$sIdentifier = $this->parseIdentifier();
$iIdentifierLineNum = $this->iLineNo;
$this->consumeWhiteSpace();
if ($sIdentifier === 'import') {
$oLocation = $this->parseURLValue();
Expand All @@ -139,14 +151,14 @@ private function parseAtRule() {
$sMediaQuery = $this->consumeUntil(';');
}
$this->consume(';');
return new Import($oLocation, $sMediaQuery);
return new Import($oLocation, $sMediaQuery, $iIdentifierLineNum);
} else if ($sIdentifier === 'charset') {
$sCharset = $this->parseStringValue();
$this->consumeWhiteSpace();
$this->consume(';');
return new Charset($sCharset);
return new Charset($sCharset, $iIdentifierLineNum);
} else if ($this->identifierIs($sIdentifier, 'keyframes')) {
$oResult = new KeyFrame();
$oResult = new KeyFrame($iIdentifierLineNum);
$oResult->setVendorKeyFrame($sIdentifier);
$oResult->setAnimationName(trim($this->consumeUntil('{', false, true)));
$this->consumeWhiteSpace();
Expand All @@ -161,12 +173,12 @@ private function parseAtRule() {
}
$this->consume(';');
if ($sPrefix !== null && !is_string($sPrefix)) {
throw new UnexpectedTokenException('Wrong namespace prefix', $sPrefix, 'custom');
throw new UnexpectedTokenException('Wrong namespace prefix', $sPrefix, 'custom', $iIdentifierLineNum);
}
if (!($mUrl instanceof CSSString || $mUrl instanceof URL)) {
throw new UnexpectedTokenException('Wrong namespace url of invalid type', $mUrl, 'custom');
throw new UnexpectedTokenException('Wrong namespace url of invalid type', $mUrl, 'custom', $iIdentifierLineNum);
}
return new CSSNamespace($mUrl, $sPrefix);
return new CSSNamespace($mUrl, $sPrefix, $iIdentifierLineNum);
} else {
//Unknown other at rule (font-face or such)
$sArgs = trim($this->consumeUntil('{', false, true));
Expand All @@ -179,10 +191,10 @@ private function parseAtRule() {
}
}
if($bUseRuleSet) {
$oAtRule = new AtRuleSet($sIdentifier, $sArgs);
$oAtRule = new AtRuleSet($sIdentifier, $sArgs, $iIdentifierLineNum);
$this->parseRuleSet($oAtRule);
} else {
$oAtRule = new AtRuleBlockList($sIdentifier, $sArgs);
$oAtRule = new AtRuleBlockList($sIdentifier, $sArgs, $iIdentifierLineNum);
$this->parseList($oAtRule);
}
return $oAtRule;
Expand All @@ -192,7 +204,7 @@ private function parseAtRule() {
private function parseIdentifier($bAllowFunctions = true, $bIgnoreCase = true) {
$sResult = $this->parseCharacter(true);
if ($sResult === null) {
throw new UnexpectedTokenException($sResult, $this->peek(5), 'identifier');
throw new UnexpectedTokenException($sResult, $this->peek(5), 'identifier', $this->iLineNo);
}
$sCharacter = null;
while (($sCharacter = $this->parseCharacter(true)) !== null) {
Expand All @@ -204,7 +216,7 @@ private function parseIdentifier($bAllowFunctions = true, $bIgnoreCase = true) {
if ($bAllowFunctions && $this->comes('(')) {
$this->consume('(');
$aArguments = $this->parseValue(array('=', ' ', ','));
$sResult = new CSSFunction($sResult, $aArguments);
$sResult = new CSSFunction($sResult, $aArguments, ',', $this->iLineNo);
$this->consume(')');
}
return $sResult;
Expand Down Expand Up @@ -232,13 +244,13 @@ private function parseStringValue() {
while (!$this->comes($sQuote)) {
$sContent = $this->parseCharacter(false);
if ($sContent === null) {
throw new \Exception("Non-well-formed quoted string {$this->peek(3)}");
throw new SourceException("Non-well-formed quoted string {$this->peek(3)}", $this->iLineNo);
}
$sResult .= $sContent;
}
$this->consume($sQuote);
}
return new CSSString($sResult);
return new CSSString($sResult, $this->iLineNo);
}

private function parseCharacter($bIsForIdentifier) {
Expand Down Expand Up @@ -287,7 +299,7 @@ private function parseCharacter($bIsForIdentifier) {
}

private function parseSelector() {
$oResult = new DeclarationBlock();
$oResult = new DeclarationBlock($this->iLineNo);
$oResult->setSelector($this->consumeUntil('{', false, true));
$this->consumeWhiteSpace();
$this->parseRuleSet($oResult);
Expand Down Expand Up @@ -333,7 +345,7 @@ private function parseRuleSet($oRuleSet) {
}

private function parseRule() {
$oRule = new Rule($this->parseIdentifier());
$oRule = new Rule($this->parseIdentifier(), $this->iLineNo);
$this->consumeWhiteSpace();
$this->consume(':');
$oValue = $this->parseValue(self::listDelimiterForRule($oRule->getRule()));
Expand Down Expand Up @@ -387,7 +399,7 @@ private function parseValue($aListDelimiters) {
break;
}
}
$oList = new RuleValueList($sDelimiter);
$oList = new RuleValueList($sDelimiter, $this->iLineNo);
for ($i = $iStartPosition - 1; $i - $iStartPosition + 1 < $iLength * 2; $i+=2) {
$oList->addListComponent($aStack[$i]);
}
Expand Down Expand Up @@ -445,7 +457,7 @@ private function parseNumericValue($bForColor = false) {
}
}
}
return new Size(floatval($sSize), $sUnit, $bForColor);
return new Size(floatval($sSize), $sUnit, $bForColor, $this->iLineNo);
}

private function parseColorValue() {
Expand All @@ -456,7 +468,7 @@ private function parseColorValue() {
if ($this->strlen($sValue) === 3) {
$sValue = $sValue[0] . $sValue[0] . $sValue[1] . $sValue[1] . $sValue[2] . $sValue[2];
}
$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));
$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));
} else {
$sColorMode = $this->parseIdentifier(false);
$this->consumeWhiteSpace();
Expand All @@ -472,7 +484,7 @@ private function parseColorValue() {
}
$this->consume(')');
}
return new Color($aColor);
return new Color($aColor, $this->iLineNo);
}

private function parseURLValue() {
Expand All @@ -483,7 +495,7 @@ private function parseURLValue() {
$this->consume('(');
}
$this->consumeWhiteSpace();
$oResult = new URL($this->parseStringValue());
$oResult = new URL($this->parseStringValue(), $this->iLineNo);
if ($bUseUrl) {
$this->consumeWhiteSpace();
$this->consume(')');
Expand Down Expand Up @@ -516,17 +528,21 @@ private function peek($iLength = 1, $iOffset = 0) {

private function consume($mValue = 1) {
if (is_string($mValue)) {
$iLineCount = substr_count($mValue, "\n");
$iLength = $this->strlen($mValue);
if (!$this->streql($this->substr($this->iCurrentPosition, $iLength), $mValue)) {
throw new UnexpectedTokenException($mValue, $this->peek(max($iLength, 5)));
throw new UnexpectedTokenException($mValue, $this->peek(max($iLength, 5)), $this->iLineNo);
}
$this->iLineNo += $iLineCount;
$this->iCurrentPosition += $this->strlen($mValue);
return $mValue;
} else {
if ($this->iCurrentPosition + $mValue > $this->iLength) {
throw new UnexpectedTokenException($mValue, $this->peek(5), 'count');
throw new UnexpectedTokenException($mValue, $this->peek(5), 'count', $this->iLineNo);
}
$sResult = $this->substr($this->iCurrentPosition, $mValue);
$iLineCount = substr_count($sResult, "\n");
$this->iLineNo += $iLineCount;
$this->iCurrentPosition += $mValue;
return $sResult;
}
Expand All @@ -537,7 +553,7 @@ private function consumeExpression($mExpression) {
if (preg_match($mExpression, $this->inputLeft(), $aMatches, PREG_OFFSET_CAPTURE) === 1) {
return $this->consume($aMatches[0][0]);
}
throw new UnexpectedTokenException($mExpression, $this->peek(5), 'expression');
throw new UnexpectedTokenException($mExpression, $this->peek(5), 'expression', $this->iLineNo);
}

private function consumeWhiteSpace() {
Expand Down Expand Up @@ -595,7 +611,7 @@ private function consumeUntil($aEnd, $bIncludeEnd = false, $consumeEnd = false)
}

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

private function inputLeft() {
Expand Down
5 changes: 4 additions & 1 deletion lib/Sabberworm/CSS/Parsing/OutputException.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,8 @@
/**
* Thrown if the CSS parsers attempts to print something invalid
*/
class OutputException extends \Exception {
class OutputException extends SourceException {
public function __construct($sMessage, $iLineNo = 0) {
parent::__construct($sMessage, $iLineNo);
}
}
18 changes: 18 additions & 0 deletions lib/Sabberworm/CSS/Parsing/SourceException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<?php

namespace Sabberworm\CSS\Parsing;

class SourceException extends \Exception {
private $iLineNo;
public function __construct($sMessage, $iLineNo = 0) {
$this->iLineNo = $iLineNo;
if (!empty($iLineNo)) {
$sMessage .= " [line no: $iLineNo]";
}
parent::__construct($sMessage);
}

public function getLineNo() {
return $this->iLineNo;
}
}
Loading

0 comments on commit bfca845

Please sign in to comment.