Differences Between: [Versions 310 and 401] [Versions 310 and 402] [Versions 310 and 403]
1 <?php 2 3 namespace Sabberworm\CSS\CSSList; 4 5 use Sabberworm\CSS\Comment\Commentable; 6 use Sabberworm\CSS\Parsing\ParserState; 7 use Sabberworm\CSS\Parsing\SourceException; 8 use Sabberworm\CSS\Parsing\UnexpectedTokenException; 9 use Sabberworm\CSS\Property\AtRule; 10 use Sabberworm\CSS\Property\Charset; 11 use Sabberworm\CSS\Property\CSSNamespace; 12 use Sabberworm\CSS\Property\Import; 13 use Sabberworm\CSS\Property\Selector; 14 use Sabberworm\CSS\Renderable; 15 use Sabberworm\CSS\RuleSet\AtRuleSet; 16 use Sabberworm\CSS\RuleSet\DeclarationBlock; 17 use Sabberworm\CSS\RuleSet\RuleSet; 18 use Sabberworm\CSS\Value\CSSString; 19 use Sabberworm\CSS\Value\URL; 20 use Sabberworm\CSS\Value\Value; 21 22 /** 23 * A CSSList is the most generic container available. Its contents include RuleSet as well as other CSSList objects. 24 * Also, it may contain Import and Charset objects stemming from @-rules. 25 */ 26 abstract class CSSList implements Renderable, Commentable { 27 28 protected $aComments; 29 protected $aContents; 30 protected $iLineNo; 31 32 public function __construct($iLineNo = 0) { 33 $this->aComments = array(); 34 $this->aContents = array(); 35 $this->iLineNo = $iLineNo; 36 } 37 38 public static function parseList(ParserState $oParserState, CSSList $oList) { 39 $bIsRoot = $oList instanceof Document; 40 if(is_string($oParserState)) { 41 $oParserState = new ParserState($oParserState); 42 } 43 $bLenientParsing = $oParserState->getSettings()->bLenientParsing; 44 while(!$oParserState->isEnd()) { 45 $comments = $oParserState->consumeWhiteSpace(); 46 $oListItem = null; 47 if($bLenientParsing) { 48 try { 49 $oListItem = self::parseListItem($oParserState, $oList); 50 } catch (UnexpectedTokenException $e) { 51 $oListItem = false; 52 } 53 } else { 54 $oListItem = self::parseListItem($oParserState, $oList); 55 } 56 if($oListItem === null) { 57 // List parsing finished 58 return; 59 } 60 if($oListItem) { 61 $oListItem->setComments($comments); 62 $oList->append($oListItem); 63 } 64 } 65 if(!$bIsRoot && !$bLenientParsing) { 66 throw new SourceException("Unexpected end of document", $oParserState->currentLine()); 67 } 68 } 69 70 private static function parseListItem(ParserState $oParserState, CSSList $oList) { 71 $bIsRoot = $oList instanceof Document; 72 if ($oParserState->comes('@')) { 73 $oAtRule = self::parseAtRule($oParserState); 74 if($oAtRule instanceof Charset) { 75 if(!$bIsRoot) { 76 throw new UnexpectedTokenException('@charset may only occur in root document', '', 'custom', $oParserState->currentLine()); 77 } 78 if(count($oList->getContents()) > 0) { 79 throw new UnexpectedTokenException('@charset must be the first parseable token in a document', '', 'custom', $oParserState->currentLine()); 80 } 81 $oParserState->setCharset($oAtRule->getCharset()->getString()); 82 } 83 return $oAtRule; 84 } else if ($oParserState->comes('}')) { 85 $oParserState->consume('}'); 86 if ($bIsRoot) { 87 if ($oParserState->getSettings()->bLenientParsing) { 88 while ($oParserState->comes('}')) $oParserState->consume('}'); 89 return DeclarationBlock::parse($oParserState); 90 } else { 91 throw new SourceException("Unopened {", $oParserState->currentLine()); 92 } 93 } else { 94 return null; 95 } 96 } else { 97 return DeclarationBlock::parse($oParserState); 98 } 99 } 100 101 private static function parseAtRule(ParserState $oParserState) { 102 $oParserState->consume('@'); 103 $sIdentifier = $oParserState->parseIdentifier(); 104 $iIdentifierLineNum = $oParserState->currentLine(); 105 $oParserState->consumeWhiteSpace(); 106 if ($sIdentifier === 'import') { 107 $oLocation = URL::parse($oParserState); 108 $oParserState->consumeWhiteSpace(); 109 $sMediaQuery = null; 110 if (!$oParserState->comes(';')) { 111 $sMediaQuery = $oParserState->consumeUntil(';'); 112 } 113 $oParserState->consume(';'); 114 return new Import($oLocation, $sMediaQuery, $iIdentifierLineNum); 115 } else if ($sIdentifier === 'charset') { 116 $sCharset = CSSString::parse($oParserState); 117 $oParserState->consumeWhiteSpace(); 118 $oParserState->consume(';'); 119 return new Charset($sCharset, $iIdentifierLineNum); 120 } else if (self::identifierIs($sIdentifier, 'keyframes')) { 121 $oResult = new KeyFrame($iIdentifierLineNum); 122 $oResult->setVendorKeyFrame($sIdentifier); 123 $oResult->setAnimationName(trim($oParserState->consumeUntil('{', false, true))); 124 CSSList::parseList($oParserState, $oResult); 125 return $oResult; 126 } else if ($sIdentifier === 'namespace') { 127 $sPrefix = null; 128 $mUrl = Value::parsePrimitiveValue($oParserState); 129 if (!$oParserState->comes(';')) { 130 $sPrefix = $mUrl; 131 $mUrl = Value::parsePrimitiveValue($oParserState); 132 } 133 $oParserState->consume(';'); 134 if ($sPrefix !== null && !is_string($sPrefix)) { 135 throw new UnexpectedTokenException('Wrong namespace prefix', $sPrefix, 'custom', $iIdentifierLineNum); 136 } 137 if (!($mUrl instanceof CSSString || $mUrl instanceof URL)) { 138 throw new UnexpectedTokenException('Wrong namespace url of invalid type', $mUrl, 'custom', $iIdentifierLineNum); 139 } 140 return new CSSNamespace($mUrl, $sPrefix, $iIdentifierLineNum); 141 } else { 142 //Unknown other at rule (font-face or such) 143 $sArgs = trim($oParserState->consumeUntil('{', false, true)); 144 if (substr_count($sArgs, "(") != substr_count($sArgs, ")")) { 145 if($oParserState->getSettings()->bLenientParsing) { 146 return NULL; 147 } else { 148 throw new SourceException("Unmatched brace count in media query", $oParserState->currentLine()); 149 } 150 } 151 $bUseRuleSet = true; 152 foreach(explode('/', AtRule::BLOCK_RULES) as $sBlockRuleName) { 153 if(self::identifierIs($sIdentifier, $sBlockRuleName)) { 154 $bUseRuleSet = false; 155 break; 156 } 157 } 158 if($bUseRuleSet) { 159 $oAtRule = new AtRuleSet($sIdentifier, $sArgs, $iIdentifierLineNum); 160 RuleSet::parseRuleSet($oParserState, $oAtRule); 161 } else { 162 $oAtRule = new AtRuleBlockList($sIdentifier, $sArgs, $iIdentifierLineNum); 163 CSSList::parseList($oParserState, $oAtRule); 164 } 165 return $oAtRule; 166 } 167 } 168 169 /** 170 * 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. 171 */ 172 private static function identifierIs($sIdentifier, $sMatch) { 173 return (strcasecmp($sIdentifier, $sMatch) === 0) 174 ?: preg_match("/^(-\\w+-)?$sMatch$/i", $sIdentifier) === 1; 175 } 176 177 178 /** 179 * @return int 180 */ 181 public function getLineNo() { 182 return $this->iLineNo; 183 } 184 185 /** 186 * Prepend item to list of contents. 187 * 188 * @param object $oItem Item. 189 */ 190 public function prepend($oItem) { 191 array_unshift($this->aContents, $oItem); 192 } 193 194 /** 195 * Append item to list of contents. 196 * 197 * @param object $oItem Item. 198 */ 199 public function append($oItem) { 200 $this->aContents[] = $oItem; 201 } 202 203 /** 204 * Splice the list of contents. 205 * 206 * @param int $iOffset Offset. 207 * @param int $iLength Length. Optional. 208 * @param RuleSet[] $mReplacement Replacement. Optional. 209 */ 210 public function splice($iOffset, $iLength = null, $mReplacement = null) { 211 array_splice($this->aContents, $iOffset, $iLength, $mReplacement); 212 } 213 214 /** 215 * Insert an item before its sibling. 216 * 217 * @param mixed $oItem The item. 218 * @param mixed $oSibling The sibling. 219 */ 220 public function insert($oItem, $oSibling) { 221 $iIndex = array_search($oSibling, $this->aContents); 222 if ($iIndex === false) { 223 return $this->append($oItem); 224 } 225 array_splice($this->aContents, $iIndex, 0, array($oItem)); 226 } 227 228 /** 229 * Removes an item from the CSS list. 230 * @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) 231 * @return bool Whether the item was removed. 232 */ 233 public function remove($oItemToRemove) { 234 $iKey = array_search($oItemToRemove, $this->aContents, true); 235 if ($iKey !== false) { 236 unset($this->aContents[$iKey]); 237 return true; 238 } 239 return false; 240 } 241 242 /** 243 * Replaces an item from the CSS list. 244 * @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) 245 */ 246 public function replace($oOldItem, $oNewItem) { 247 $iKey = array_search($oOldItem, $this->aContents, true); 248 if ($iKey !== false) { 249 array_splice($this->aContents, $iKey, 1, $oNewItem); 250 return true; 251 } 252 return false; 253 } 254 255 /** 256 * Set the contents. 257 * @param array $aContents Objects to set as content. 258 */ 259 public function setContents(array $aContents) { 260 $this->aContents = array(); 261 foreach ($aContents as $content) { 262 $this->append($content); 263 } 264 } 265 266 /** 267 * Removes a declaration block from the CSS list if it matches all given selectors. 268 * @param array|string $mSelector The selectors to match. 269 * @param boolean $bRemoveAll Whether to stop at the first declaration block found or remove all blocks 270 */ 271 public function removeDeclarationBlockBySelector($mSelector, $bRemoveAll = false) { 272 if ($mSelector instanceof DeclarationBlock) { 273 $mSelector = $mSelector->getSelectors(); 274 } 275 if (!is_array($mSelector)) { 276 $mSelector = explode(',', $mSelector); 277 } 278 foreach ($mSelector as $iKey => &$mSel) { 279 if (!($mSel instanceof Selector)) { 280 $mSel = new Selector($mSel); 281 } 282 } 283 foreach ($this->aContents as $iKey => $mItem) { 284 if (!($mItem instanceof DeclarationBlock)) { 285 continue; 286 } 287 if ($mItem->getSelectors() == $mSelector) { 288 unset($this->aContents[$iKey]); 289 if (!$bRemoveAll) { 290 return; 291 } 292 } 293 } 294 } 295 296 public function __toString() { 297 return $this->render(new \Sabberworm\CSS\OutputFormat()); 298 } 299 300 public function render(\Sabberworm\CSS\OutputFormat $oOutputFormat) { 301 $sResult = ''; 302 $bIsFirst = true; 303 $oNextLevel = $oOutputFormat; 304 if(!$this->isRootList()) { 305 $oNextLevel = $oOutputFormat->nextLevel(); 306 } 307 foreach ($this->aContents as $oContent) { 308 $sRendered = $oOutputFormat->safely(function() use ($oNextLevel, $oContent) { 309 return $oContent->render($oNextLevel); 310 }); 311 if($sRendered === null) { 312 continue; 313 } 314 if($bIsFirst) { 315 $bIsFirst = false; 316 $sResult .= $oNextLevel->spaceBeforeBlocks(); 317 } else { 318 $sResult .= $oNextLevel->spaceBetweenBlocks(); 319 } 320 $sResult .= $sRendered; 321 } 322 323 if(!$bIsFirst) { 324 // Had some output 325 $sResult .= $oOutputFormat->spaceAfterBlocks(); 326 } 327 328 return $sResult; 329 } 330 331 /** 332 * Return true if the list can not be further outdented. Only important when rendering. 333 */ 334 public abstract function isRootList(); 335 336 public function getContents() { 337 return $this->aContents; 338 } 339 340 /** 341 * @param array $aComments Array of comments. 342 */ 343 public function addComments(array $aComments) { 344 $this->aComments = array_merge($this->aComments, $aComments); 345 } 346 347 /** 348 * @return array 349 */ 350 public function getComments() { 351 return $this->aComments; 352 } 353 354 /** 355 * @param array $aComments Array containing Comment objects. 356 */ 357 public function setComments(array $aComments) { 358 $this->aComments = $aComments; 359 } 360 361 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body