Search moodle.org's
Developer Documentation

See Release Notes
Long Term Support Release

  • Bug fixes for general core bugs in 4.1.x will end 13 November 2023 (12 months).
  • Bug fixes for security issues in 4.1.x will end 10 November 2025 (36 months).
  • PHP version: minimum PHP 7.4.0 Note: minimum PHP version has increased since Moodle 4.0. PHP 8.0.x is supported too.

Differences Between: [Versions 310 and 401] [Versions 311 and 401] [Versions 39 and 401] [Versions 400 and 401]

   1  <?php
   2  
   3  namespace Sabberworm\CSS\RuleSet;
   4  
   5  use Sabberworm\CSS\Comment\Comment;
   6  use Sabberworm\CSS\Comment\Commentable;
   7  use Sabberworm\CSS\OutputFormat;
   8  use Sabberworm\CSS\Parsing\ParserState;
   9  use Sabberworm\CSS\Parsing\UnexpectedEOFException;
  10  use Sabberworm\CSS\Parsing\UnexpectedTokenException;
  11  use Sabberworm\CSS\Renderable;
  12  use Sabberworm\CSS\Rule\Rule;
  13  
  14  /**
  15   * RuleSet is a generic superclass denoting rules. The typical example for rule sets are declaration block.
  16   * However, unknown At-Rules (like `@font-face`) are also rule sets.
  17   */
  18  abstract class RuleSet implements Renderable, Commentable
  19  {
  20      /**
  21       * @var array<string, Rule>
  22       */
  23      private $aRules;
  24  
  25      /**
  26       * @var int
  27       */
  28      protected $iLineNo;
  29  
  30      /**
  31       * @var array<array-key, Comment>
  32       */
  33      protected $aComments;
  34  
  35      /**
  36       * @param int $iLineNo
  37       */
  38      public function __construct($iLineNo = 0)
  39      {
  40          $this->aRules = [];
  41          $this->iLineNo = $iLineNo;
  42          $this->aComments = [];
  43      }
  44  
  45      /**
  46       * @return void
  47       *
  48       * @throws UnexpectedTokenException
  49       * @throws UnexpectedEOFException
  50       */
  51      public static function parseRuleSet(ParserState $oParserState, RuleSet $oRuleSet)
  52      {
  53          while ($oParserState->comes(';')) {
  54              $oParserState->consume(';');
  55          }
  56          while (!$oParserState->comes('}')) {
  57              $oRule = null;
  58              if ($oParserState->getSettings()->bLenientParsing) {
  59                  try {
  60                      $oRule = Rule::parse($oParserState);
  61                  } catch (UnexpectedTokenException $e) {
  62                      try {
  63                          $sConsume = $oParserState->consumeUntil(["\n", ";", '}'], true);
  64                          // We need to “unfind” the matches to the end of the ruleSet as this will be matched later
  65                          if ($oParserState->streql(substr($sConsume, -1), '}')) {
  66                              $oParserState->backtrack(1);
  67                          } else {
  68                              while ($oParserState->comes(';')) {
  69                                  $oParserState->consume(';');
  70                              }
  71                          }
  72                      } catch (UnexpectedTokenException $e) {
  73                          // We’ve reached the end of the document. Just close the RuleSet.
  74                          return;
  75                      }
  76                  }
  77              } else {
  78                  $oRule = Rule::parse($oParserState);
  79              }
  80              if ($oRule) {
  81                  $oRuleSet->addRule($oRule);
  82              }
  83          }
  84          $oParserState->consume('}');
  85      }
  86  
  87      /**
  88       * @return int
  89       */
  90      public function getLineNo()
  91      {
  92          return $this->iLineNo;
  93      }
  94  
  95      /**
  96       * @param Rule|null $oSibling
  97       *
  98       * @return void
  99       */
 100      public function addRule(Rule $oRule, Rule $oSibling = null)
 101      {
 102          $sRule = $oRule->getRule();
 103          if (!isset($this->aRules[$sRule])) {
 104              $this->aRules[$sRule] = [];
 105          }
 106  
 107          $iPosition = count($this->aRules[$sRule]);
 108  
 109          if ($oSibling !== null) {
 110              $iSiblingPos = array_search($oSibling, $this->aRules[$sRule], true);
 111              if ($iSiblingPos !== false) {
 112                  $iPosition = $iSiblingPos;
 113                  $oRule->setPosition($oSibling->getLineNo(), $oSibling->getColNo() - 1);
 114              }
 115          }
 116          if ($oRule->getLineNo() === 0 && $oRule->getColNo() === 0) {
 117              //this node is added manually, give it the next best line
 118              $rules = $this->getRules();
 119              $pos = count($rules);
 120              if ($pos > 0) {
 121                  $last = $rules[$pos - 1];
 122                  $oRule->setPosition($last->getLineNo() + 1, 0);
 123              }
 124          }
 125  
 126          array_splice($this->aRules[$sRule], $iPosition, 0, [$oRule]);
 127      }
 128  
 129      /**
 130       * Returns all rules matching the given rule name
 131       *
 132       * @example $oRuleSet->getRules('font') // returns array(0 => $oRule, …) or array().
 133       *
 134       * @example $oRuleSet->getRules('font-')
 135       *          //returns an array of all rules either beginning with font- or matching font.
 136       *
 137       * @param Rule|string|null $mRule
 138       *        Pattern to search for. If null, returns all rules.
 139       *        If the pattern ends with a dash, all rules starting with the pattern are returned
 140       *        as well as one matching the pattern with the dash excluded.
 141       *        Passing a Rule behaves like calling `getRules($mRule->getRule())`.
 142       *
 143       * @return array<int, Rule>
 144       */
 145      public function getRules($mRule = null)
 146      {
 147          if ($mRule instanceof Rule) {
 148              $mRule = $mRule->getRule();
 149          }
 150          /** @var array<int, Rule> $aResult */
 151          $aResult = [];
 152          foreach ($this->aRules as $sName => $aRules) {
 153              // Either no search rule is given or the search rule matches the found rule exactly
 154              // or the search rule ends in “-” and the found rule starts with the search rule.
 155              if (
 156                  !$mRule || $sName === $mRule
 157                  || (
 158                      strrpos($mRule, '-') === strlen($mRule) - strlen('-')
 159                      && (strpos($sName, $mRule) === 0 || $sName === substr($mRule, 0, -1))
 160                  )
 161              ) {
 162                  $aResult = array_merge($aResult, $aRules);
 163              }
 164          }
 165          usort($aResult, function (Rule $first, Rule $second) {
 166              if ($first->getLineNo() === $second->getLineNo()) {
 167                  return $first->getColNo() - $second->getColNo();
 168              }
 169              return $first->getLineNo() - $second->getLineNo();
 170          });
 171          return $aResult;
 172      }
 173  
 174      /**
 175       * Overrides all the rules of this set.
 176       *
 177       * @param array<array-key, Rule> $aRules The rules to override with.
 178       *
 179       * @return void
 180       */
 181      public function setRules(array $aRules)
 182      {
 183          $this->aRules = [];
 184          foreach ($aRules as $rule) {
 185              $this->addRule($rule);
 186          }
 187      }
 188  
 189      /**
 190       * Returns all rules matching the given pattern and returns them in an associative array with the rule’s name
 191       * as keys. This method exists mainly for backwards-compatibility and is really only partially useful.
 192       *
 193       * Note: This method loses some information: Calling this (with an argument of `background-`) on a declaration block
 194       * like `{ background-color: green; background-color; rgba(0, 127, 0, 0.7); }` will only yield an associative array
 195       * containing the rgba-valued rule while `getRules()` would yield an indexed array containing both.
 196       *
 197       * @param Rule|string|null $mRule $mRule
 198       *        Pattern to search for. If null, returns all rules. If the pattern ends with a dash,
 199       *        all rules starting with the pattern are returned as well as one matching the pattern with the dash
 200       *        excluded. Passing a Rule behaves like calling `getRules($mRule->getRule())`.
 201       *
 202       * @return array<string, Rule>
 203       */
 204      public function getRulesAssoc($mRule = null)
 205      {
 206          /** @var array<string, Rule> $aResult */
 207          $aResult = [];
 208          foreach ($this->getRules($mRule) as $oRule) {
 209              $aResult[$oRule->getRule()] = $oRule;
 210          }
 211          return $aResult;
 212      }
 213  
 214      /**
 215       * Removes a rule from this RuleSet. This accepts all the possible values that `getRules()` accepts.
 216       *
 217       * If given a Rule, it will only remove this particular rule (by identity).
 218       * If given a name, it will remove all rules by that name.
 219       *
 220       * Note: this is different from pre-v.2.0 behaviour of PHP-CSS-Parser, where passing a Rule instance would
 221       * remove all rules with the same name. To get the old behaviour, use `removeRule($oRule->getRule())`.
 222       *
 223       * @param Rule|string|null $mRule
 224       *        pattern to remove. If $mRule is null, all rules are removed. If the pattern ends in a dash,
 225       *        all rules starting with the pattern are removed as well as one matching the pattern with the dash
 226       *        excluded. Passing a Rule behaves matches by identity.
 227       *
 228       * @return void
 229       */
 230      public function removeRule($mRule)
 231      {
 232          if ($mRule instanceof Rule) {
 233              $sRule = $mRule->getRule();
 234              if (!isset($this->aRules[$sRule])) {
 235                  return;
 236              }
 237              foreach ($this->aRules[$sRule] as $iKey => $oRule) {
 238                  if ($oRule === $mRule) {
 239                      unset($this->aRules[$sRule][$iKey]);
 240                  }
 241              }
 242          } else {
 243              foreach ($this->aRules as $sName => $aRules) {
 244                  // Either no search rule is given or the search rule matches the found rule exactly
 245                  // or the search rule ends in “-” and the found rule starts with the search rule or equals it
 246                  // (without the trailing dash).
 247                  if (
 248                      !$mRule || $sName === $mRule
 249                      || (strrpos($mRule, '-') === strlen($mRule) - strlen('-')
 250                          && (strpos($sName, $mRule) === 0 || $sName === substr($mRule, 0, -1)))
 251                  ) {
 252                      unset($this->aRules[$sName]);
 253                  }
 254              }
 255          }
 256      }
 257  
 258      /**
 259       * @return string
 260       */
 261      public function __toString()
 262      {
 263          return $this->render(new OutputFormat());
 264      }
 265  
 266      /**
 267       * @return string
 268       */
 269      public function render(OutputFormat $oOutputFormat)
 270      {
 271          $sResult = '';
 272          $bIsFirst = true;
 273          foreach ($this->aRules as $aRules) {
 274              foreach ($aRules as $oRule) {
 275                  $sRendered = $oOutputFormat->safely(function () use ($oRule, $oOutputFormat) {
 276                      return $oRule->render($oOutputFormat->nextLevel());
 277                  });
 278                  if ($sRendered === null) {
 279                      continue;
 280                  }
 281                  if ($bIsFirst) {
 282                      $bIsFirst = false;
 283                      $sResult .= $oOutputFormat->nextLevel()->spaceBeforeRules();
 284                  } else {
 285                      $sResult .= $oOutputFormat->nextLevel()->spaceBetweenRules();
 286                  }
 287                  $sResult .= $sRendered;
 288              }
 289          }
 290  
 291          if (!$bIsFirst) {
 292              // Had some output
 293              $sResult .= $oOutputFormat->spaceAfterRules();
 294          }
 295  
 296          return $oOutputFormat->removeLastSemicolon($sResult);
 297      }
 298  
 299      /**
 300       * @param array<string, Comment> $aComments
 301       *
 302       * @return void
 303       */
 304      public function addComments(array $aComments)
 305      {
 306          $this->aComments = array_merge($this->aComments, $aComments);
 307      }
 308  
 309      /**
 310       * @return array<string, Comment>
 311       */
 312      public function getComments()
 313      {
 314          return $this->aComments;
 315      }
 316  
 317      /**
 318       * @param array<string, Comment> $aComments
 319       *
 320       * @return void
 321       */
 322      public function setComments(array $aComments)
 323      {
 324          $this->aComments = $aComments;
 325      }
 326  }