<?php
namespace Sabberworm\CSS\CSSList;
> use Sabberworm\CSS\Comment\Comment;
use Sabberworm\CSS\Comment\Commentable;
> use Sabberworm\CSS\OutputFormat;
use Sabberworm\CSS\Parsing\ParserState;
use Sabberworm\CSS\Parsing\SourceException;
> use Sabberworm\CSS\Parsing\UnexpectedEOFException;
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\Settings;
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.
< * Also, it may contain Import and Charset objects stemming from @-rules.
> * 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 at-rules.
> */
> abstract class CSSList implements Renderable, Commentable
> {
> /**
> * @var array<array-key, Comment>
*/
< abstract class CSSList implements Renderable, Commentable {
<
protected $aComments;
>
protected $aContents;
> /**
protected $iLineNo;
> * @var array<int, RuleSet|CSSList|Import|Charset>
> */
public function __construct($iLineNo = 0) {
>
$this->aComments = array();
> /**
$this->aContents = array();
> * @var int
$this->iLineNo = $iLineNo;
> */
< public function __construct($iLineNo = 0) {
< $this->aComments = array();
< $this->aContents = array();
> /**
> * @param int $iLineNo
> */
> public function __construct($iLineNo = 0)
> {
> $this->aComments = [];
> $this->aContents = [];
< public static function parseList(ParserState $oParserState, CSSList $oList) {
> /**
> * @return void
> *
> * @throws UnexpectedTokenException
> * @throws SourceException
> */
> public static function parseList(ParserState $oParserState, CSSList $oList)
> {
if(is_string($oParserState)) {
< $oParserState = new ParserState($oParserState);
> $oParserState = new ParserState($oParserState, Settings::create());
}
$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);
}
}
if(!$bIsRoot && !$bLenientParsing) {
throw new SourceException("Unexpected end of document", $oParserState->currentLine());
}
}
< private static function parseListItem(ParserState $oParserState, CSSList $oList) {
> /**
> * @return AtRuleBlockList|KeyFrame|Charset|CSSNamespace|Import|AtRuleSet|DeclarationBlock|null|false
> *
> * @throws SourceException
> * @throws UnexpectedEOFException
> * @throws UnexpectedTokenException
> */
> 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());
> 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());
> 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 (!$oParserState->getSettings()->bLenientParsing) {
> throw new UnexpectedTokenException('CSS selector', '}', 'identifier', $oParserState->currentLine());
> } else {
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);
> return DeclarationBlock::parse($oParserState, $oList);
}
}
< private static function parseAtRule(ParserState $oParserState) {
> /**
> * @param ParserState $oParserState
> *
> * @return AtRuleBlockList|KeyFrame|Charset|CSSNamespace|Import|AtRuleSet|null
> *
> * @throws SourceException
> * @throws UnexpectedTokenException
> * @throws UnexpectedEOFException
> */
> 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(';');
> $sMediaQuery = trim($oParserState->consumeUntil([';', ParserState::EOF]));
}
< $oParserState->consume(';');
< return new Import($oLocation, $sMediaQuery, $iIdentifierLineNum);
> $oParserState->consumeUntil([';', ParserState::EOF], true, true);
> return new Import($oLocation, $sMediaQuery ?: null, $iIdentifierLineNum);
} else if ($sIdentifier === 'charset') {
$sCharset = CSSString::parse($oParserState);
$oParserState->consumeWhiteSpace();
< $oParserState->consume(';');
> $oParserState->consumeUntil([';', ParserState::EOF], true, true);
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);
> if ($oParserState->comes('}')) {
return $oResult;
> $oParserState->consume('}');
} else if ($sIdentifier === 'namespace') {
> }
$sPrefix = null;
$mUrl = Value::parsePrimitiveValue($oParserState);
if (!$oParserState->comes(';')) {
$sPrefix = $mUrl;
$mUrl = Value::parsePrimitiveValue($oParserState);
}
< $oParserState->consume(';');
> $oParserState->consumeUntil([';', ParserState::EOF], true, true);
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);
> 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));
if (substr_count($sArgs, "(") != substr_count($sArgs, ")")) {
if($oParserState->getSettings()->bLenientParsing) {
< return NULL;
> return null;
} else {
throw new SourceException("Unmatched brace count in media query", $oParserState->currentLine());
}
}
$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);
> if ($oParserState->comes('}')) {
}
> $oParserState->consume('}');
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.
> * 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.
> *
> * @param string $sIdentifier
> * @param string $sMatch
> *
> * @return bool
*/
< private static function identifierIs($sIdentifier, $sMatch) {
> private static function identifierIs($sIdentifier, $sMatch)
> {
return (strcasecmp($sIdentifier, $sMatch) === 0)
?: preg_match("/^(-\\w+-)?$sMatch$/i", $sIdentifier) === 1;
}
<
/**
* @return int
*/
< public function getLineNo() {
> public function getLineNo()
> {
return $this->iLineNo;
}
/**
< * Prepend item to list of contents.
> * Prepends an item to the list of contents.
*
< * @param object $oItem Item.
> * @param RuleSet|CSSList|Import|Charset $oItem
> *
> * @return void
*/
< public function prepend($oItem) {
> public function prepend($oItem)
> {
array_unshift($this->aContents, $oItem);
}
/**
< * Append item to list of contents.
> * Appends an item to tje list of contents.
*
< * @param object $oItem Item.
< */
< public function append($oItem) {
< $this->aContents[] = $oItem;
< }
<
< /**
< * Splice the list of contents.
> * @param RuleSet|CSSList|Import|Charset $oItem
*
< * @param int $iOffset Offset.
< * @param int $iLength Length. Optional.
< * @param RuleSet[] $mReplacement Replacement. Optional.
> * @return void
*/
< public function splice($iOffset, $iLength = null, $mReplacement = null) {
< array_splice($this->aContents, $iOffset, $iLength, $mReplacement);
> public function append($oItem)
> {
> $this->aContents[] = $oItem;
}
/**
* Insert an item before its sibling.
*
* @param mixed $oItem The item.
* @param mixed $oSibling The sibling.
*/
public function insert($oItem, $oSibling) {
$iIndex = array_search($oSibling, $this->aContents);
if ($iIndex === false) {
return $this->append($oItem);
}
array_splice($this->aContents, $iIndex, 0, array($oItem));
}
/**
> * Splices the list of contents.
* Removes an item from the CSS list.
> *
* @param RuleSet|Import|Charset|CSSList $oItemToRemove May be a RuleSet (most likely a DeclarationBlock), a Import, a Charset or another CSSList (most likely a MediaQuery)
> * @param int $iOffset
* @return bool Whether the item was removed.
> * @param int $iLength
*/
> * @param array<int, RuleSet|CSSList|Import|Charset> $mReplacement
public function remove($oItemToRemove) {
> *
$iKey = array_search($oItemToRemove, $this->aContents, true);
> * @return void
if ($iKey !== false) {
> */
unset($this->aContents[$iKey]);
> public function splice($iOffset, $iLength = null, $mReplacement = null)
return true;
> {
}
> array_splice($this->aContents, $iOffset, $iLength, $mReplacement);
return false;
> }
}
>
> /**
< * @param RuleSet|Import|Charset|CSSList $oItemToRemove May be a RuleSet (most likely a DeclarationBlock), a Import, a Charset or another CSSList (most likely a MediaQuery)
< * @return bool Whether the item was removed.
> *
> * @param RuleSet|Import|Charset|CSSList $oItemToRemove
> * May be a RuleSet (most likely a DeclarationBlock), a Import,
> * a Charset or another CSSList (most likely a MediaQuery)
> *
> * @return bool whether the item was removed
< public function remove($oItemToRemove) {
> public function remove($oItemToRemove)
> {
< * @param RuleSet|Import|Charset|CSSList $oItemToRemove May be a RuleSet (most likely a DeclarationBlock), a Import, a Charset or another CSSList (most likely a MediaQuery)
> *
> * @param RuleSet|Import|Charset|CSSList $oOldItem
> * May be a `RuleSet` (most likely a `DeclarationBlock`), an `Import`, a `Charset`
> * or another `CSSList` (most likely a `MediaQuery`)
> *
> * @return bool
< public function replace($oOldItem, $oNewItem) {
> public function replace($oOldItem, $mNewItem)
> {
$iKey = array_search($oOldItem, $this->aContents, true);
if ($iKey !== false) {
< array_splice($this->aContents, $iKey, 1, $oNewItem);
> if (is_array($mNewItem)) {
> array_splice($this->aContents, $iKey, 1, $mNewItem);
> } else {
> array_splice($this->aContents, $iKey, 1, [$mNewItem]);
> }
return true;
}
return false;
}
/**
< * Set the contents.
< * @param array $aContents Objects to set as content.
> * @param array<int, RuleSet|Import|Charset|CSSList> $aContents
*/
< public function setContents(array $aContents) {
< $this->aContents = array();
> public function setContents(array $aContents)
> {
> $this->aContents = [];
foreach ($aContents as $content) {
$this->append($content);
}
}
/**
* Removes a declaration block from the CSS list if it matches all given selectors.
< * @param array|string $mSelector The selectors to match.
< * @param boolean $bRemoveAll Whether to stop at the first declaration block found or remove all blocks
> *
> * @param DeclarationBlock|array<array-key, Selector>|string $mSelector the selectors to match
> * @param bool $bRemoveAll whether to stop at the first declaration block found or remove all blocks
> *
> * @return void
*/
< public function removeDeclarationBlockBySelector($mSelector, $bRemoveAll = false) {
> public function removeDeclarationBlockBySelector($mSelector, $bRemoveAll = false)
> {
if ($mSelector instanceof DeclarationBlock) {
$mSelector = $mSelector->getSelectors();
}
if (!is_array($mSelector)) {
$mSelector = explode(',', $mSelector);
}
foreach ($mSelector as $iKey => &$mSel) {
if (!($mSel instanceof Selector)) {
> if (!Selector::isValid($mSel)) {
$mSel = new Selector($mSel);
> throw new UnexpectedTokenException(
}
> "Selector did not match '" . Selector::SELECTOR_VALIDATION_RX . "'.",
}
> $mSel,
foreach ($this->aContents as $iKey => $mItem) {
> "custom"
if (!($mItem instanceof DeclarationBlock)) {
> );
continue;
> }
}
if ($mItem->getSelectors() == $mSelector) {
unset($this->aContents[$iKey]);
if (!$bRemoveAll) {
return;
}
}
}
}
< public function __toString() {
< return $this->render(new \Sabberworm\CSS\OutputFormat());
> /**
> * @return string
> */
> public function __toString()
> {
> return $this->render(new OutputFormat());
}
< public function render(\Sabberworm\CSS\OutputFormat $oOutputFormat) {
> /**
> * @return string
> */
> public function render(OutputFormat $oOutputFormat)
> {
$sResult = '';
$bIsFirst = true;
$oNextLevel = $oOutputFormat;
if(!$this->isRootList()) {
$oNextLevel = $oOutputFormat->nextLevel();
}
foreach ($this->aContents as $oContent) {
$sRendered = $oOutputFormat->safely(function() use ($oNextLevel, $oContent) {
return $oContent->render($oNextLevel);
});
if($sRendered === null) {
continue;
}
if($bIsFirst) {
$bIsFirst = false;
$sResult .= $oNextLevel->spaceBeforeBlocks();
} else {
$sResult .= $oNextLevel->spaceBetweenBlocks();
}
$sResult .= $sRendered;
}
if(!$bIsFirst) {
// Had some output
$sResult .= $oOutputFormat->spaceAfterBlocks();
}
return $sResult;
}
/**
* Return true if the list can not be further outdented. Only important when rendering.
> *
*/
> * @return bool
< public abstract function isRootList();
> abstract public function isRootList();
< public function getContents() {
> /**
> * @return array<int, RuleSet|Import|Charset|CSSList>
> */
> public function getContents()
> {
return $this->aContents;
}
/**
< * @param array $aComments Array of comments.
> * @param array<array-key, Comment> $aComments
> *
> * @return void
*/
< public function addComments(array $aComments) {
> public function addComments(array $aComments)
> {
$this->aComments = array_merge($this->aComments, $aComments);
}
/**
< * @return array
> * @return array<array-key, Comment>
*/
< public function getComments() {
> public function getComments()
> {
return $this->aComments;
}
/**
< * @param array $aComments Array containing Comment objects.
> * @param array<array-key, Comment> $aComments
> *
> * @return void
*/
< public function setComments(array $aComments) {
> public function setComments(array $aComments)
> {
$this->aComments = $aComments;
}
<
}