Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 4.2.x will end 22 April 2024 (12 months).
  • Bug fixes for security issues in 4.2.x will end 7 October 2024 (18 months).
  • PHP version: minimum PHP 8.0.0 Note: minimum PHP version has increased since Moodle 4.1. PHP 8.1.x is supported too.

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

   1  <?php
   2  
   3  namespace Sabberworm\CSS\RuleSet;
   4  
   5  use Sabberworm\CSS\CSSList\CSSList;
   6  use Sabberworm\CSS\CSSList\KeyFrame;
   7  use Sabberworm\CSS\OutputFormat;
   8  use Sabberworm\CSS\Parsing\OutputException;
   9  use Sabberworm\CSS\Parsing\ParserState;
  10  use Sabberworm\CSS\Parsing\UnexpectedEOFException;
  11  use Sabberworm\CSS\Parsing\UnexpectedTokenException;
  12  use Sabberworm\CSS\Property\KeyframeSelector;
  13  use Sabberworm\CSS\Property\Selector;
  14  use Sabberworm\CSS\Rule\Rule;
  15  use Sabberworm\CSS\Value\Color;
  16  use Sabberworm\CSS\Value\RuleValueList;
  17  use Sabberworm\CSS\Value\Size;
  18  use Sabberworm\CSS\Value\URL;
  19  use Sabberworm\CSS\Value\Value;
  20  
  21  /**
  22   * Declaration blocks are the parts of a CSS file which denote the rules belonging to a selector.
  23   *
  24   * Declaration blocks usually appear directly inside a `Document` or another `CSSList` (mostly a `MediaQuery`).
  25   */
  26  class DeclarationBlock extends RuleSet
  27  {
  28      /**
  29       * @var array<int, Selector|string>
  30       */
  31      private $aSelectors;
  32  
  33      /**
  34       * @param int $iLineNo
  35       */
  36      public function __construct($iLineNo = 0)
  37      {
  38          parent::__construct($iLineNo);
  39          $this->aSelectors = [];
  40      }
  41  
  42      /**
  43       * @param CSSList|null $oList
  44       *
  45       * @return DeclarationBlock|false
  46       *
  47       * @throws UnexpectedTokenException
  48       * @throws UnexpectedEOFException
  49       */
  50      public static function parse(ParserState $oParserState, $oList = null)
  51      {
  52          $aComments = [];
  53          $oResult = new DeclarationBlock($oParserState->currentLine());
  54          try {
  55              $aSelectorParts = [];
  56              $sStringWrapperChar = false;
  57              do {
  58                  $aSelectorParts[] = $oParserState->consume(1)
  59                      . $oParserState->consumeUntil(['{', '}', '\'', '"'], false, false, $aComments);
  60                  if (in_array($oParserState->peek(), ['\'', '"']) && substr(end($aSelectorParts), -1) != "\\") {
  61                      if ($sStringWrapperChar === false) {
  62                          $sStringWrapperChar = $oParserState->peek();
  63                      } elseif ($sStringWrapperChar == $oParserState->peek()) {
  64                          $sStringWrapperChar = false;
  65                      }
  66                  }
  67              } while (!in_array($oParserState->peek(), ['{', '}']) || $sStringWrapperChar !== false);
  68              $oResult->setSelectors(implode('', $aSelectorParts), $oList);
  69              if ($oParserState->comes('{')) {
  70                  $oParserState->consume(1);
  71              }
  72          } catch (UnexpectedTokenException $e) {
  73              if ($oParserState->getSettings()->bLenientParsing) {
  74                  if (!$oParserState->comes('}')) {
  75                      $oParserState->consumeUntil('}', false, true);
  76                  }
  77                  return false;
  78              } else {
  79                  throw $e;
  80              }
  81          }
  82          $oResult->setComments($aComments);
  83          RuleSet::parseRuleSet($oParserState, $oResult);
  84          return $oResult;
  85      }
  86  
  87      /**
  88       * @param array<int, Selector|string>|string $mSelector
  89       * @param CSSList|null $oList
  90       *
  91       * @throws UnexpectedTokenException
  92       */
  93      public function setSelectors($mSelector, $oList = null)
  94      {
  95          if (is_array($mSelector)) {
  96              $this->aSelectors = $mSelector;
  97          } else {
  98              $this->aSelectors = explode(',', $mSelector);
  99          }
 100          foreach ($this->aSelectors as $iKey => $mSelector) {
 101              if (!($mSelector instanceof Selector)) {
 102                  if ($oList === null || !($oList instanceof KeyFrame)) {
 103                      if (!Selector::isValid($mSelector)) {
 104                          throw new UnexpectedTokenException(
 105                              "Selector did not match '" . Selector::SELECTOR_VALIDATION_RX . "'.",
 106                              $mSelector,
 107                              "custom"
 108                          );
 109                      }
 110                      $this->aSelectors[$iKey] = new Selector($mSelector);
 111                  } else {
 112                      if (!KeyframeSelector::isValid($mSelector)) {
 113                          throw new UnexpectedTokenException(
 114                              "Selector did not match '" . KeyframeSelector::SELECTOR_VALIDATION_RX . "'.",
 115                              $mSelector,
 116                              "custom"
 117                          );
 118                      }
 119                      $this->aSelectors[$iKey] = new KeyframeSelector($mSelector);
 120                  }
 121              }
 122          }
 123      }
 124  
 125      /**
 126       * Remove one of the selectors of the block.
 127       *
 128       * @param Selector|string $mSelector
 129       *
 130       * @return bool
 131       */
 132      public function removeSelector($mSelector)
 133      {
 134          if ($mSelector instanceof Selector) {
 135              $mSelector = $mSelector->getSelector();
 136          }
 137          foreach ($this->aSelectors as $iKey => $oSelector) {
 138              if ($oSelector->getSelector() === $mSelector) {
 139                  unset($this->aSelectors[$iKey]);
 140                  return true;
 141              }
 142          }
 143          return false;
 144      }
 145  
 146      /**
 147       * @return array<int, Selector|string>
 148       *
 149       * @deprecated will be removed in version 9.0; use `getSelectors()` instead
 150       */
 151      public function getSelector()
 152      {
 153          return $this->getSelectors();
 154      }
 155  
 156      /**
 157       * @param Selector|string $mSelector
 158       * @param CSSList|null $oList
 159       *
 160       * @return void
 161       *
 162       * @deprecated will be removed in version 9.0; use `setSelectors()` instead
 163       */
 164      public function setSelector($mSelector, $oList = null)
 165      {
 166          $this->setSelectors($mSelector, $oList);
 167      }
 168  
 169      /**
 170       * @return array<int, Selector|string>
 171       */
 172      public function getSelectors()
 173      {
 174          return $this->aSelectors;
 175      }
 176  
 177      /**
 178       * Splits shorthand declarations (e.g. `margin` or `font`) into their constituent parts.
 179       *
 180       * @return void
 181       */
 182      public function expandShorthands()
 183      {
 184          // border must be expanded before dimensions
 185          $this->expandBorderShorthand();
 186          $this->expandDimensionsShorthand();
 187          $this->expandFontShorthand();
 188          $this->expandBackgroundShorthand();
 189          $this->expandListStyleShorthand();
 190      }
 191  
 192      /**
 193       * Creates shorthand declarations (e.g. `margin` or `font`) whenever possible.
 194       *
 195       * @return void
 196       */
 197      public function createShorthands()
 198      {
 199          $this->createBackgroundShorthand();
 200          $this->createDimensionsShorthand();
 201          // border must be shortened after dimensions
 202          $this->createBorderShorthand();
 203          $this->createFontShorthand();
 204          $this->createListStyleShorthand();
 205      }
 206  
 207      /**
 208       * Splits shorthand border declarations (e.g. `border: 1px red;`).
 209       *
 210       * Additional splitting happens in expandDimensionsShorthand.
 211       *
 212       * Multiple borders are not yet supported as of 3.
 213       *
 214       * @return void
 215       */
 216      public function expandBorderShorthand()
 217      {
 218          $aBorderRules = [
 219              'border',
 220              'border-left',
 221              'border-right',
 222              'border-top',
 223              'border-bottom',
 224          ];
 225          $aBorderSizes = [
 226              'thin',
 227              'medium',
 228              'thick',
 229          ];
 230          $aRules = $this->getRulesAssoc();
 231          foreach ($aBorderRules as $sBorderRule) {
 232              if (!isset($aRules[$sBorderRule])) {
 233                  continue;
 234              }
 235              $oRule = $aRules[$sBorderRule];
 236              $mRuleValue = $oRule->getValue();
 237              $aValues = [];
 238              if (!$mRuleValue instanceof RuleValueList) {
 239                  $aValues[] = $mRuleValue;
 240              } else {
 241                  $aValues = $mRuleValue->getListComponents();
 242              }
 243              foreach ($aValues as $mValue) {
 244                  if ($mValue instanceof Value) {
 245                      $mNewValue = clone $mValue;
 246                  } else {
 247                      $mNewValue = $mValue;
 248                  }
 249                  if ($mValue instanceof Size) {
 250                      $sNewRuleName = $sBorderRule . "-width";
 251                  } elseif ($mValue instanceof Color) {
 252                      $sNewRuleName = $sBorderRule . "-color";
 253                  } else {
 254                      if (in_array($mValue, $aBorderSizes)) {
 255                          $sNewRuleName = $sBorderRule . "-width";
 256                      } else {
 257                          $sNewRuleName = $sBorderRule . "-style";
 258                      }
 259                  }
 260                  $oNewRule = new Rule($sNewRuleName, $oRule->getLineNo(), $oRule->getColNo());
 261                  $oNewRule->setIsImportant($oRule->getIsImportant());
 262                  $oNewRule->addValue([$mNewValue]);
 263                  $this->addRule($oNewRule);
 264              }
 265              $this->removeRule($sBorderRule);
 266          }
 267      }
 268  
 269      /**
 270       * Splits shorthand dimensional declarations (e.g. `margin: 0px auto;`)
 271       * into their constituent parts.
 272       *
 273       * Handles `margin`, `padding`, `border-color`, `border-style` and `border-width`.
 274       *
 275       * @return void
 276       */
 277      public function expandDimensionsShorthand()
 278      {
 279          $aExpansions = [
 280              'margin' => 'margin-%s',
 281              'padding' => 'padding-%s',
 282              'border-color' => 'border-%s-color',
 283              'border-style' => 'border-%s-style',
 284              'border-width' => 'border-%s-width',
 285          ];
 286          $aRules = $this->getRulesAssoc();
 287          foreach ($aExpansions as $sProperty => $sExpanded) {
 288              if (!isset($aRules[$sProperty])) {
 289                  continue;
 290              }
 291              $oRule = $aRules[$sProperty];
 292              $mRuleValue = $oRule->getValue();
 293              $aValues = [];
 294              if (!$mRuleValue instanceof RuleValueList) {
 295                  $aValues[] = $mRuleValue;
 296              } else {
 297                  $aValues = $mRuleValue->getListComponents();
 298              }
 299              $top = $right = $bottom = $left = null;
 300              switch (count($aValues)) {
 301                  case 1:
 302                      $top = $right = $bottom = $left = $aValues[0];
 303                      break;
 304                  case 2:
 305                      $top = $bottom = $aValues[0];
 306                      $left = $right = $aValues[1];
 307                      break;
 308                  case 3:
 309                      $top = $aValues[0];
 310                      $left = $right = $aValues[1];
 311                      $bottom = $aValues[2];
 312                      break;
 313                  case 4:
 314                      $top = $aValues[0];
 315                      $right = $aValues[1];
 316                      $bottom = $aValues[2];
 317                      $left = $aValues[3];
 318                      break;
 319              }
 320              foreach (['top', 'right', 'bottom', 'left'] as $sPosition) {
 321                  $oNewRule = new Rule(sprintf($sExpanded, $sPosition), $oRule->getLineNo(), $oRule->getColNo());
 322                  $oNewRule->setIsImportant($oRule->getIsImportant());
 323                  $oNewRule->addValue(${$sPosition});
 324                  $this->addRule($oNewRule);
 325              }
 326              $this->removeRule($sProperty);
 327          }
 328      }
 329  
 330      /**
 331       * Converts shorthand font declarations
 332       * (e.g. `font: 300 italic 11px/14px verdana, helvetica, sans-serif;`)
 333       * into their constituent parts.
 334       *
 335       * @return void
 336       */
 337      public function expandFontShorthand()
 338      {
 339          $aRules = $this->getRulesAssoc();
 340          if (!isset($aRules['font'])) {
 341              return;
 342          }
 343          $oRule = $aRules['font'];
 344          // reset properties to 'normal' per http://www.w3.org/TR/21/fonts.html#font-shorthand
 345          $aFontProperties = [
 346              'font-style' => 'normal',
 347              'font-variant' => 'normal',
 348              'font-weight' => 'normal',
 349              'font-size' => 'normal',
 350              'line-height' => 'normal',
 351          ];
 352          $mRuleValue = $oRule->getValue();
 353          $aValues = [];
 354          if (!$mRuleValue instanceof RuleValueList) {
 355              $aValues[] = $mRuleValue;
 356          } else {
 357              $aValues = $mRuleValue->getListComponents();
 358          }
 359          foreach ($aValues as $mValue) {
 360              if (!$mValue instanceof Value) {
 361                  $mValue = mb_strtolower($mValue);
 362              }
 363              if (in_array($mValue, ['normal', 'inherit'])) {
 364                  foreach (['font-style', 'font-weight', 'font-variant'] as $sProperty) {
 365                      if (!isset($aFontProperties[$sProperty])) {
 366                          $aFontProperties[$sProperty] = $mValue;
 367                      }
 368                  }
 369              } elseif (in_array($mValue, ['italic', 'oblique'])) {
 370                  $aFontProperties['font-style'] = $mValue;
 371              } elseif ($mValue == 'small-caps') {
 372                  $aFontProperties['font-variant'] = $mValue;
 373              } elseif (
 374                  in_array($mValue, ['bold', 'bolder', 'lighter'])
 375                  || ($mValue instanceof Size
 376                      && in_array($mValue->getSize(), range(100, 900, 100)))
 377              ) {
 378                  $aFontProperties['font-weight'] = $mValue;
 379              } elseif ($mValue instanceof RuleValueList && $mValue->getListSeparator() == '/') {
 380                  list($oSize, $oHeight) = $mValue->getListComponents();
 381                  $aFontProperties['font-size'] = $oSize;
 382                  $aFontProperties['line-height'] = $oHeight;
 383              } elseif ($mValue instanceof Size && $mValue->getUnit() !== null) {
 384                  $aFontProperties['font-size'] = $mValue;
 385              } else {
 386                  $aFontProperties['font-family'] = $mValue;
 387              }
 388          }
 389          foreach ($aFontProperties as $sProperty => $mValue) {
 390              $oNewRule = new Rule($sProperty, $oRule->getLineNo(), $oRule->getColNo());
 391              $oNewRule->addValue($mValue);
 392              $oNewRule->setIsImportant($oRule->getIsImportant());
 393              $this->addRule($oNewRule);
 394          }
 395          $this->removeRule('font');
 396      }
 397  
 398      /**
 399       * Converts shorthand background declarations
 400       * (e.g. `background: url("chess.png") gray 50% repeat fixed;`)
 401       * into their constituent parts.
 402       *
 403       * @see http://www.w3.org/TR/21/colors.html#propdef-background
 404       *
 405       * @return void
 406       */
 407      public function expandBackgroundShorthand()
 408      {
 409          $aRules = $this->getRulesAssoc();
 410          if (!isset($aRules['background'])) {
 411              return;
 412          }
 413          $oRule = $aRules['background'];
 414          $aBgProperties = [
 415              'background-color' => ['transparent'],
 416              'background-image' => ['none'],
 417              'background-repeat' => ['repeat'],
 418              'background-attachment' => ['scroll'],
 419              'background-position' => [
 420                  new Size(0, '%', null, false, $this->iLineNo),
 421                  new Size(0, '%', null, false, $this->iLineNo),
 422              ],
 423          ];
 424          $mRuleValue = $oRule->getValue();
 425          $aValues = [];
 426          if (!$mRuleValue instanceof RuleValueList) {
 427              $aValues[] = $mRuleValue;
 428          } else {
 429              $aValues = $mRuleValue->getListComponents();
 430          }
 431          if (count($aValues) == 1 && $aValues[0] == 'inherit') {
 432              foreach ($aBgProperties as $sProperty => $mValue) {
 433                  $oNewRule = new Rule($sProperty, $oRule->getLineNo(), $oRule->getColNo());
 434                  $oNewRule->addValue('inherit');
 435                  $oNewRule->setIsImportant($oRule->getIsImportant());
 436                  $this->addRule($oNewRule);
 437              }
 438              $this->removeRule('background');
 439              return;
 440          }
 441          $iNumBgPos = 0;
 442          foreach ($aValues as $mValue) {
 443              if (!$mValue instanceof Value) {
 444                  $mValue = mb_strtolower($mValue);
 445              }
 446              if ($mValue instanceof URL) {
 447                  $aBgProperties['background-image'] = $mValue;
 448              } elseif ($mValue instanceof Color) {
 449                  $aBgProperties['background-color'] = $mValue;
 450              } elseif (in_array($mValue, ['scroll', 'fixed'])) {
 451                  $aBgProperties['background-attachment'] = $mValue;
 452              } elseif (in_array($mValue, ['repeat', 'no-repeat', 'repeat-x', 'repeat-y'])) {
 453                  $aBgProperties['background-repeat'] = $mValue;
 454              } elseif (
 455                  in_array($mValue, ['left', 'center', 'right', 'top', 'bottom'])
 456                  || $mValue instanceof Size
 457              ) {
 458                  if ($iNumBgPos == 0) {
 459                      $aBgProperties['background-position'][0] = $mValue;
 460                      $aBgProperties['background-position'][1] = 'center';
 461                  } else {
 462                      $aBgProperties['background-position'][$iNumBgPos] = $mValue;
 463                  }
 464                  $iNumBgPos++;
 465              }
 466          }
 467          foreach ($aBgProperties as $sProperty => $mValue) {
 468              $oNewRule = new Rule($sProperty, $oRule->getLineNo(), $oRule->getColNo());
 469              $oNewRule->setIsImportant($oRule->getIsImportant());
 470              $oNewRule->addValue($mValue);
 471              $this->addRule($oNewRule);
 472          }
 473          $this->removeRule('background');
 474      }
 475  
 476      /**
 477       * @return void
 478       */
 479      public function expandListStyleShorthand()
 480      {
 481          $aListProperties = [
 482              'list-style-type' => 'disc',
 483              'list-style-position' => 'outside',
 484              'list-style-image' => 'none',
 485          ];
 486          $aListStyleTypes = [
 487              'none',
 488              'disc',
 489              'circle',
 490              'square',
 491              'decimal-leading-zero',
 492              'decimal',
 493              'lower-roman',
 494              'upper-roman',
 495              'lower-greek',
 496              'lower-alpha',
 497              'lower-latin',
 498              'upper-alpha',
 499              'upper-latin',
 500              'hebrew',
 501              'armenian',
 502              'georgian',
 503              'cjk-ideographic',
 504              'hiragana',
 505              'hira-gana-iroha',
 506              'katakana-iroha',
 507              'katakana',
 508          ];
 509          $aListStylePositions = [
 510              'inside',
 511              'outside',
 512          ];
 513          $aRules = $this->getRulesAssoc();
 514          if (!isset($aRules['list-style'])) {
 515              return;
 516          }
 517          $oRule = $aRules['list-style'];
 518          $mRuleValue = $oRule->getValue();
 519          $aValues = [];
 520          if (!$mRuleValue instanceof RuleValueList) {
 521              $aValues[] = $mRuleValue;
 522          } else {
 523              $aValues = $mRuleValue->getListComponents();
 524          }
 525          if (count($aValues) == 1 && $aValues[0] == 'inherit') {
 526              foreach ($aListProperties as $sProperty => $mValue) {
 527                  $oNewRule = new Rule($sProperty, $oRule->getLineNo(), $oRule->getColNo());
 528                  $oNewRule->addValue('inherit');
 529                  $oNewRule->setIsImportant($oRule->getIsImportant());
 530                  $this->addRule($oNewRule);
 531              }
 532              $this->removeRule('list-style');
 533              return;
 534          }
 535          foreach ($aValues as $mValue) {
 536              if (!$mValue instanceof Value) {
 537                  $mValue = mb_strtolower($mValue);
 538              }
 539              if ($mValue instanceof Url) {
 540                  $aListProperties['list-style-image'] = $mValue;
 541              } elseif (in_array($mValue, $aListStyleTypes)) {
 542                  $aListProperties['list-style-types'] = $mValue;
 543              } elseif (in_array($mValue, $aListStylePositions)) {
 544                  $aListProperties['list-style-position'] = $mValue;
 545              }
 546          }
 547          foreach ($aListProperties as $sProperty => $mValue) {
 548              $oNewRule = new Rule($sProperty, $oRule->getLineNo(), $oRule->getColNo());
 549              $oNewRule->setIsImportant($oRule->getIsImportant());
 550              $oNewRule->addValue($mValue);
 551              $this->addRule($oNewRule);
 552          }
 553          $this->removeRule('list-style');
 554      }
 555  
 556      /**
 557       * @param array<array-key, string> $aProperties
 558       * @param string $sShorthand
 559       *
 560       * @return void
 561       */
 562      public function createShorthandProperties(array $aProperties, $sShorthand)
 563      {
 564          $aRules = $this->getRulesAssoc();
 565          $aNewValues = [];
 566          foreach ($aProperties as $sProperty) {
 567              if (!isset($aRules[$sProperty])) {
 568                  continue;
 569              }
 570              $oRule = $aRules[$sProperty];
 571              if (!$oRule->getIsImportant()) {
 572                  $mRuleValue = $oRule->getValue();
 573                  $aValues = [];
 574                  if (!$mRuleValue instanceof RuleValueList) {
 575                      $aValues[] = $mRuleValue;
 576                  } else {
 577                      $aValues = $mRuleValue->getListComponents();
 578                  }
 579                  foreach ($aValues as $mValue) {
 580                      $aNewValues[] = $mValue;
 581                  }
 582                  $this->removeRule($sProperty);
 583              }
 584          }
 585          if (count($aNewValues)) {
 586              $oNewRule = new Rule($sShorthand, $oRule->getLineNo(), $oRule->getColNo());
 587              foreach ($aNewValues as $mValue) {
 588                  $oNewRule->addValue($mValue);
 589              }
 590              $this->addRule($oNewRule);
 591          }
 592      }
 593  
 594      /**
 595       * @return void
 596       */
 597      public function createBackgroundShorthand()
 598      {
 599          $aProperties = [
 600              'background-color',
 601              'background-image',
 602              'background-repeat',
 603              'background-position',
 604              'background-attachment',
 605          ];
 606          $this->createShorthandProperties($aProperties, 'background');
 607      }
 608  
 609      /**
 610       * @return void
 611       */
 612      public function createListStyleShorthand()
 613      {
 614          $aProperties = [
 615              'list-style-type',
 616              'list-style-position',
 617              'list-style-image',
 618          ];
 619          $this->createShorthandProperties($aProperties, 'list-style');
 620      }
 621  
 622      /**
 623       * Combines `border-color`, `border-style` and `border-width` into `border`.
 624       *
 625       * Should be run after `create_dimensions_shorthand`!
 626       *
 627       * @return void
 628       */
 629      public function createBorderShorthand()
 630      {
 631          $aProperties = [
 632              'border-width',
 633              'border-style',
 634              'border-color',
 635          ];
 636          $this->createShorthandProperties($aProperties, 'border');
 637      }
 638  
 639      /**
 640       * Looks for long format CSS dimensional properties
 641       * (margin, padding, border-color, border-style and border-width)
 642       * and converts them into shorthand CSS properties.
 643       *
 644       * @return void
 645       */
 646      public function createDimensionsShorthand()
 647      {
 648          $aPositions = ['top', 'right', 'bottom', 'left'];
 649          $aExpansions = [
 650              'margin' => 'margin-%s',
 651              'padding' => 'padding-%s',
 652              'border-color' => 'border-%s-color',
 653              'border-style' => 'border-%s-style',
 654              'border-width' => 'border-%s-width',
 655          ];
 656          $aRules = $this->getRulesAssoc();
 657          foreach ($aExpansions as $sProperty => $sExpanded) {
 658              $aFoldable = [];
 659              foreach ($aRules as $sRuleName => $oRule) {
 660                  foreach ($aPositions as $sPosition) {
 661                      if ($sRuleName == sprintf($sExpanded, $sPosition)) {
 662                          $aFoldable[$sRuleName] = $oRule;
 663                      }
 664                  }
 665              }
 666              // All four dimensions must be present
 667              if (count($aFoldable) == 4) {
 668                  $aValues = [];
 669                  foreach ($aPositions as $sPosition) {
 670                      $oRule = $aRules[sprintf($sExpanded, $sPosition)];
 671                      $mRuleValue = $oRule->getValue();
 672                      $aRuleValues = [];
 673                      if (!$mRuleValue instanceof RuleValueList) {
 674                          $aRuleValues[] = $mRuleValue;
 675                      } else {
 676                          $aRuleValues = $mRuleValue->getListComponents();
 677                      }
 678                      $aValues[$sPosition] = $aRuleValues;
 679                  }
 680                  $oNewRule = new Rule($sProperty, $oRule->getLineNo(), $oRule->getColNo());
 681                  if ((string)$aValues['left'][0] == (string)$aValues['right'][0]) {
 682                      if ((string)$aValues['top'][0] == (string)$aValues['bottom'][0]) {
 683                          if ((string)$aValues['top'][0] == (string)$aValues['left'][0]) {
 684                              // All 4 sides are equal
 685                              $oNewRule->addValue($aValues['top']);
 686                          } else {
 687                              // Top and bottom are equal, left and right are equal
 688                              $oNewRule->addValue($aValues['top']);
 689                              $oNewRule->addValue($aValues['left']);
 690                          }
 691                      } else {
 692                          // Only left and right are equal
 693                          $oNewRule->addValue($aValues['top']);
 694                          $oNewRule->addValue($aValues['left']);
 695                          $oNewRule->addValue($aValues['bottom']);
 696                      }
 697                  } else {
 698                      // No sides are equal
 699                      $oNewRule->addValue($aValues['top']);
 700                      $oNewRule->addValue($aValues['left']);
 701                      $oNewRule->addValue($aValues['bottom']);
 702                      $oNewRule->addValue($aValues['right']);
 703                  }
 704                  $this->addRule($oNewRule);
 705                  foreach ($aPositions as $sPosition) {
 706                      $this->removeRule(sprintf($sExpanded, $sPosition));
 707                  }
 708              }
 709          }
 710      }
 711  
 712      /**
 713       * Looks for long format CSS font properties (e.g. `font-weight`) and
 714       * tries to convert them into a shorthand CSS `font` property.
 715       *
 716       * At least `font-size` AND `font-family` must be present in order to create a shorthand declaration.
 717       *
 718       * @return void
 719       */
 720      public function createFontShorthand()
 721      {
 722          $aFontProperties = [
 723              'font-style',
 724              'font-variant',
 725              'font-weight',
 726              'font-size',
 727              'line-height',
 728              'font-family',
 729          ];
 730          $aRules = $this->getRulesAssoc();
 731          if (!isset($aRules['font-size']) || !isset($aRules['font-family'])) {
 732              return;
 733          }
 734          $oOldRule = isset($aRules['font-size']) ? $aRules['font-size'] : $aRules['font-family'];
 735          $oNewRule = new Rule('font', $oOldRule->getLineNo(), $oOldRule->getColNo());
 736          unset($oOldRule);
 737          foreach (['font-style', 'font-variant', 'font-weight'] as $sProperty) {
 738              if (isset($aRules[$sProperty])) {
 739                  $oRule = $aRules[$sProperty];
 740                  $mRuleValue = $oRule->getValue();
 741                  $aValues = [];
 742                  if (!$mRuleValue instanceof RuleValueList) {
 743                      $aValues[] = $mRuleValue;
 744                  } else {
 745                      $aValues = $mRuleValue->getListComponents();
 746                  }
 747                  if ($aValues[0] !== 'normal') {
 748                      $oNewRule->addValue($aValues[0]);
 749                  }
 750              }
 751          }
 752          // Get the font-size value
 753          $oRule = $aRules['font-size'];
 754          $mRuleValue = $oRule->getValue();
 755          $aFSValues = [];
 756          if (!$mRuleValue instanceof RuleValueList) {
 757              $aFSValues[] = $mRuleValue;
 758          } else {
 759              $aFSValues = $mRuleValue->getListComponents();
 760          }
 761          // But wait to know if we have line-height to add it
 762          if (isset($aRules['line-height'])) {
 763              $oRule = $aRules['line-height'];
 764              $mRuleValue = $oRule->getValue();
 765              $aLHValues = [];
 766              if (!$mRuleValue instanceof RuleValueList) {
 767                  $aLHValues[] = $mRuleValue;
 768              } else {
 769                  $aLHValues = $mRuleValue->getListComponents();
 770              }
 771              if ($aLHValues[0] !== 'normal') {
 772                  $val = new RuleValueList('/', $this->iLineNo);
 773                  $val->addListComponent($aFSValues[0]);
 774                  $val->addListComponent($aLHValues[0]);
 775                  $oNewRule->addValue($val);
 776              }
 777          } else {
 778              $oNewRule->addValue($aFSValues[0]);
 779          }
 780          $oRule = $aRules['font-family'];
 781          $mRuleValue = $oRule->getValue();
 782          $aFFValues = [];
 783          if (!$mRuleValue instanceof RuleValueList) {
 784              $aFFValues[] = $mRuleValue;
 785          } else {
 786              $aFFValues = $mRuleValue->getListComponents();
 787          }
 788          $oFFValue = new RuleValueList(',', $this->iLineNo);
 789          $oFFValue->setListComponents($aFFValues);
 790          $oNewRule->addValue($oFFValue);
 791  
 792          $this->addRule($oNewRule);
 793          foreach ($aFontProperties as $sProperty) {
 794              $this->removeRule($sProperty);
 795          }
 796      }
 797  
 798      /**
 799       * @return string
 800       *
 801       * @throws OutputException
 802       */
 803      public function __toString()
 804      {
 805          return $this->render(new OutputFormat());
 806      }
 807  
 808      /**
 809       * @return string
 810       *
 811       * @throws OutputException
 812       */
 813      public function render(OutputFormat $oOutputFormat)
 814      {
 815          if (count($this->aSelectors) === 0) {
 816              // If all the selectors have been removed, this declaration block becomes invalid
 817              throw new OutputException("Attempt to print declaration block with missing selector", $this->iLineNo);
 818          }
 819          $sResult = $oOutputFormat->sBeforeDeclarationBlock;
 820          $sResult .= $oOutputFormat->implode(
 821              $oOutputFormat->spaceBeforeSelectorSeparator() . ',' . $oOutputFormat->spaceAfterSelectorSeparator(),
 822              $this->aSelectors
 823          );
 824          $sResult .= $oOutputFormat->sAfterDeclarationBlockSelectors;
 825          $sResult .= $oOutputFormat->spaceBeforeOpeningBrace() . '{';
 826          $sResult .= parent::render($oOutputFormat);
 827          $sResult .= '}';
 828          $sResult .= $oOutputFormat->sAfterDeclarationBlock;
 829          return $sResult;
 830      }
 831  }