Skip to content

Refactor parsing logic #146

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Oct 21, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,4 @@ matrix:
dist: precise
sudo: false
before_script: rm composer.lock && composer install
script: ./vendor/phpunit/phpunit/phpunit
script: ./vendor/bin/phpunit
149 changes: 147 additions & 2 deletions lib/Sabberworm/CSS/CSSList/CSSList.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,22 @@

namespace Sabberworm\CSS\CSSList;

use Sabberworm\CSS\Comment\Commentable;
use Sabberworm\CSS\Parsing\ParserState;
use Sabberworm\CSS\Parsing\SourceException;
use Sabberworm\CSS\Parsing\UnexpectedTokenException;
use Sabberworm\CSS\Property\AtRule;
use Sabberworm\CSS\Property\Charset;
use Sabberworm\CSS\Property\CSSNamespace;
use Sabberworm\CSS\Property\Import;
use Sabberworm\CSS\Property\Selector;
use Sabberworm\CSS\Renderable;
use Sabberworm\CSS\RuleSet\AtRuleSet;
use Sabberworm\CSS\RuleSet\DeclarationBlock;
use Sabberworm\CSS\RuleSet\RuleSet;
use Sabberworm\CSS\Property\Selector;
use Sabberworm\CSS\Comment\Commentable;
use Sabberworm\CSS\Value\CSSString;
use Sabberworm\CSS\Value\URL;
use Sabberworm\CSS\Value\Value;

/**
* A CSSList is the most generic container available. Its contents include RuleSet as well as other CSSList objects.
Expand All @@ -24,6 +35,140 @@ public function __construct($iLineNo = 0) {
$this->iLineNo = $iLineNo;
}

public static function parseList(ParserState $oParserState, CSSList $oList) {
$bIsRoot = $oList instanceof Document;
if(is_string($oParserState)) {
$oParserState = new ParserState($oParserState);
}
$bLenientParsing = $oParserState->getSettings()->bLenientParsing;
while(!$oParserState->isEnd()) {
$comments = $oParserState->consumeWhiteSpace();
$oListItem = null;
if($bLenientParsing) {
try {
$oListItem = self::parseListItem($oParserState, $oList);
} catch (UnexpectedTokenException $e) {
$oListItem = false;
}
} else {
$oListItem = self::parseListItem($oParserState, $oList);
}
if($oListItem === null) {
// List parsing finished
return;
}
if($oListItem) {
$oListItem->setComments($comments);
$oList->append($oListItem);
}
$oParserState->consumeWhiteSpace();
}
if(!$bIsRoot && !$bLenientParsing) {
throw new SourceException("Unexpected end of document", $oParserState->currentLine());
}
}

private static function parseListItem(ParserState $oParserState, CSSList $oList) {
$bIsRoot = $oList instanceof Document;
if ($oParserState->comes('@')) {
$oAtRule = self::parseAtRule($oParserState);
if($oAtRule instanceof Charset) {
if(!$bIsRoot) {
throw new UnexpectedTokenException('@charset may only occur in root document', '', 'custom', $oParserState->currentLine());
}
if(count($oList->getContents()) > 0) {
throw new UnexpectedTokenException('@charset must be the first parseable token in a document', '', 'custom', $oParserState->currentLine());
}
$oParserState->setCharset($oAtRule->getCharset()->getString());
}
return $oAtRule;
} else if ($oParserState->comes('}')) {
$oParserState->consume('}');
if ($bIsRoot) {
if ($oParserState->getSettings()->bLenientParsing) {
while ($oParserState->comes('}')) $oParserState->consume('}');
return DeclarationBlock::parse($oParserState);
} else {
throw new SourceException("Unopened {", $oParserState->currentLine());
}
} else {
return null;
}
} else {
return DeclarationBlock::parse($oParserState);
}
}

private static function parseAtRule(ParserState $oParserState) {
$oParserState->consume('@');
$sIdentifier = $oParserState->parseIdentifier();
$iIdentifierLineNum = $oParserState->currentLine();
$oParserState->consumeWhiteSpace();
if ($sIdentifier === 'import') {
$oLocation = URL::parse($oParserState);
$oParserState->consumeWhiteSpace();
$sMediaQuery = null;
if (!$oParserState->comes(';')) {
$sMediaQuery = $oParserState->consumeUntil(';');
}
$oParserState->consume(';');
return new Import($oLocation, $sMediaQuery, $iIdentifierLineNum);
} else if ($sIdentifier === 'charset') {
$sCharset = CSSString::parse($oParserState);
$oParserState->consumeWhiteSpace();
$oParserState->consume(';');
return new Charset($sCharset, $iIdentifierLineNum);
} else if (self::identifierIs($sIdentifier, 'keyframes')) {
$oResult = new KeyFrame($iIdentifierLineNum);
$oResult->setVendorKeyFrame($sIdentifier);
$oResult->setAnimationName(trim($oParserState->consumeUntil('{', false, true)));
CSSList::parseList($oParserState, $oResult);
return $oResult;
} else if ($sIdentifier === 'namespace') {
$sPrefix = null;
$mUrl = Value::parsePrimitiveValue($oParserState);
if (!$oParserState->comes(';')) {
$sPrefix = $mUrl;
$mUrl = Value::parsePrimitiveValue($oParserState);
}
$oParserState->consume(';');
if ($sPrefix !== null && !is_string($sPrefix)) {
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', $iIdentifierLineNum);
}
return new CSSNamespace($mUrl, $sPrefix, $iIdentifierLineNum);
} else {
//Unknown other at rule (font-face or such)
$sArgs = trim($oParserState->consumeUntil('{', false, true));
$bUseRuleSet = true;
foreach(explode('/', AtRule::BLOCK_RULES) as $sBlockRuleName) {
if(self::identifierIs($sIdentifier, $sBlockRuleName)) {
$bUseRuleSet = false;
break;
}
}
if($bUseRuleSet) {
$oAtRule = new AtRuleSet($sIdentifier, $sArgs, $iIdentifierLineNum);
RuleSet::parseRuleSet($oParserState, $oAtRule);
} else {
$oAtRule = new AtRuleBlockList($sIdentifier, $sArgs, $iIdentifierLineNum);
CSSList::parseList($oParserState, $oAtRule);
}
return $oAtRule;
}
}

/**
* Tests an identifier for a given value. Since identifiers are all keywords, they can be vendor-prefixed. We need to check for these versions too.
*/
private static function identifierIs($sIdentifier, $sMatch) {
return (strcasecmp($sIdentifier, $sMatch) === 0)
?: preg_match("/^(-\\w+-)?$sMatch$/i", $sIdentifier) === 1;
}


/**
* @return int
*/
Expand Down
8 changes: 8 additions & 0 deletions lib/Sabberworm/CSS/CSSList/Document.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

namespace Sabberworm\CSS\CSSList;

use Sabberworm\CSS\Parsing\ParserState;

/**
* The root CSSList of a parsed file. Contains all top-level css contents, mostly declaration blocks, but also any @-rules encountered.
*/
Expand All @@ -14,6 +16,12 @@ public function __construct($iLineNo = 0) {
parent::__construct($iLineNo);
}

public static function parse(ParserState $oParserState) {
$oDocument = new Document($oParserState->currentLine());
CSSList::parseList($oParserState, $oDocument);
return $oDocument;
}

/**
* Gets all DeclarationBlock objects recursively.
*/
Expand Down
Loading