AutorÃa | Ultima modificación | Ver Log |
<?phpnamespace 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;/*** This is the most generic container available. It can contain `DeclarationBlock`s (rule sets with a selector),* `RuleSet`s as well as other `CSSList` objects.** It can also contain `Import` and `Charset` objects stemming from at-rules.*/abstract class CSSList implements Renderable, Commentable{/*** @var array<array-key, Comment>*/protected $aComments;/*** @var array<int, RuleSet|CSSList|Import|Charset>*/protected $aContents;/*** @var int*/protected $iLineNo;/*** @param int $iLineNo*/public function __construct($iLineNo = 0){$this->aComments = [];$this->aContents = [];$this->iLineNo = $iLineNo;}/*** @return void** @throws UnexpectedTokenException* @throws SourceException*/public static function parseList(ParserState $oParserState, CSSList $oList){$bIsRoot = $oList instanceof Document;if (is_string($oParserState)) {$oParserState = new ParserState($oParserState, Settings::create());}$bLenientParsing = $oParserState->getSettings()->bLenientParsing;$aComments = [];while (!$oParserState->isEnd()) {$aComments = array_merge($aComments, $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 finishedreturn;}if ($oListItem) {$oListItem->addComments($aComments);$oList->append($oListItem);}$aComments = $oParserState->consumeWhiteSpace();}$oList->addComments($aComments);if (!$bIsRoot && !$bLenientParsing) {throw new SourceException("Unexpected end of document", $oParserState->currentLine());}}/*** @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());}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());}return $oAtRule;} elseif ($oParserState->comes('}')) {if ($bIsRoot) {if ($oParserState->getSettings()->bLenientParsing) {return DeclarationBlock::parse($oParserState);} else {throw new SourceException("Unopened {", $oParserState->currentLine());}} else {// End of listreturn null;}} else {return DeclarationBlock::parse($oParserState, $oList);}}/*** @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 = trim($oParserState->consumeUntil([';', ParserState::EOF]));}$oParserState->consumeUntil([';', ParserState::EOF], true, true);return new Import($oLocation, $sMediaQuery ?: null, $iIdentifierLineNum);} elseif ($sIdentifier === 'charset') {$oCharsetString = CSSString::parse($oParserState);$oParserState->consumeWhiteSpace();$oParserState->consumeUntil([';', ParserState::EOF], true, true);return new Charset($oCharsetString, $iIdentifierLineNum);} elseif (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('}')) {$oParserState->consume('}');}return $oResult;} elseif ($sIdentifier === 'namespace') {$sPrefix = null;$mUrl = Value::parsePrimitiveValue($oParserState);if (!$oParserState->comes(';')) {$sPrefix = $mUrl;$mUrl = Value::parsePrimitiveValue($oParserState);}$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);}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;} 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.** @param string $sIdentifier* @param string $sMatch** @return bool*/private static function identifierIs($sIdentifier, $sMatch){return (strcasecmp($sIdentifier, $sMatch) === 0)?: preg_match("/^(-\\w+-)?$sMatch$/i", $sIdentifier) === 1;}/*** @return int*/public function getLineNo(){return $this->iLineNo;}/*** Prepends an item to the list of contents.** @param RuleSet|CSSList|Import|Charset $oItem** @return void*/public function prepend($oItem){array_unshift($this->aContents, $oItem);}/*** Appends an item to the list of contents.** @param RuleSet|CSSList|Import|Charset $oItem** @return void*/public function append($oItem){$this->aContents[] = $oItem;}/*** Splices the list of contents.** @param int $iOffset* @param int $iLength* @param array<int, RuleSet|CSSList|Import|Charset> $mReplacement** @return void*/public function splice($iOffset, $iLength = null, $mReplacement = null){array_splice($this->aContents, $iOffset, $iLength, $mReplacement);}/*** Inserts an item in the CSS list before its sibling. If the desired sibling cannot be found,* the item is appended at the end.** @param RuleSet|CSSList|Import|Charset $item* @param RuleSet|CSSList|Import|Charset $sibling*/public function insertBefore($item, $sibling){if (in_array($sibling, $this->aContents, true)) {$this->replace($sibling, [$item, $sibling]);} else {$this->append($item);}}/*** 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)** @return bool whether the item was removed*/public function remove($oItemToRemove){$iKey = array_search($oItemToRemove, $this->aContents, true);if ($iKey !== false) {unset($this->aContents[$iKey]);return true;}return false;}/*** Replaces an item from the CSS list.** @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, $mNewItem){$iKey = array_search($oOldItem, $this->aContents, true);if ($iKey !== false) {if (is_array($mNewItem)) {array_splice($this->aContents, $iKey, 1, $mNewItem);} else {array_splice($this->aContents, $iKey, 1, [$mNewItem]);}return true;}return false;}/*** @param array<int, RuleSet|Import|Charset|CSSList> $aContents*/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 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){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)) {throw new UnexpectedTokenException("Selector did not match '" . Selector::SELECTOR_VALIDATION_RX . "'.",$mSel,"custom");}$mSel = new Selector($mSel);}}foreach ($this->aContents as $iKey => $mItem) {if (!($mItem instanceof DeclarationBlock)) {continue;}if ($mItem->getSelectors() == $mSelector) {unset($this->aContents[$iKey]);if (!$bRemoveAll) {return;}}}}/*** @return string*/public function __toString(){return $this->render(new OutputFormat());}/*** @return string*/protected function renderListContents(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*/abstract public function isRootList();/*** Returns the stored items.** @return array<int, RuleSet|Import|Charset|CSSList>*/public function getContents(){return $this->aContents;}/*** @param array<array-key, Comment> $aComments** @return void*/public function addComments(array $aComments){$this->aComments = array_merge($this->aComments, $aComments);}/*** @return array<array-key, Comment>*/public function getComments(){return $this->aComments;}/*** @param array<array-key, Comment> $aComments** @return void*/public function setComments(array $aComments){$this->aComments = $aComments;}}