Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 3.11.x will end 14 Nov 2022 (12 months plus 6 months extension).
  • Bug fixes for security issues in 3.11.x will end 13 Nov 2023 (18 months plus 12 months extension).
  • PHP version: minimum PHP 7.3.0 Note: minimum PHP version has increased since Moodle 3.10. PHP 7.4.x is supported too.

Differences Between: [Versions 311 and 401]

   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  }