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 39 and 401]

   1  <?php
   2  /**
   3   * RTLCSS.
   4   *
   5   * @package   MoodleHQ\RTLCSS
   6   * @copyright 2016 Frédéric Massart - FMCorz.net
   7   * @license   https://opensource.org/licenses/MIT MIT
   8   */
   9  
  10  namespace MoodleHQ\RTLCSS;
  11  
  12  use Sabberworm\CSS\CSSList\CSSList;
  13  use Sabberworm\CSS\CSSList\Document;
  14  use Sabberworm\CSS\OutputFormat;
  15  use Sabberworm\CSS\Parser;
  16  use Sabberworm\CSS\Rule\Rule;
  17  use Sabberworm\CSS\RuleSet\RuleSet;
  18  use Sabberworm\CSS\Settings;
  19  use Sabberworm\CSS\Value\CSSFunction;
  20  use Sabberworm\CSS\Value\CSSString;
  21  use Sabberworm\CSS\Value\PrimitiveValue;
  22  use Sabberworm\CSS\Value\RuleValueList;
  23  use Sabberworm\CSS\Value\Size;
  24  use Sabberworm\CSS\Value\ValueList;
  25  
  26  /**
  27   * RTLCSS Class.
  28   *
  29   * @package   MoodleHQ\RTLCSS
  30   * @copyright 2016 Frédéric Massart - FMCorz.net
  31   * @license   https://opensource.org/licenses/MIT MIT
  32   */
  33  class RTLCSS {
  34  
  35      protected $tree;
  36      protected $shouldAddCss = [];
  37      protected $shouldIgnore = false;
  38      protected $shouldRemove = false;
  39  
  40      public function __construct(Document $tree) {
  41          $this->tree = $tree;
  42      }
  43  
  44      protected function compare($what, $to, $ignoreCase) {
  45          if ($ignoreCase) {
  46              return strtolower($what) === strtolower($to);
  47          }
  48          return $what === $to;
  49      }
  50  
  51      protected function complement($value) {
  52          if ($value instanceof Size) {
  53              $value->setSize(100 - $value->getSize());
  54  
  55          } else if ($value instanceof CSSFunction) {
  56              $arguments = implode($value->getListSeparator(), $value->getArguments());
  57              $arguments = "100% - ($arguments)";
  58              $value->setListComponents([$arguments]);
  59          }
  60      }
  61  
  62      public function flip() {
  63          $this->processBlock($this->tree);
  64          return $this->tree;
  65      }
  66  
  67      protected function negate($value) {
  68          if ($value instanceof ValueList) {
  69              foreach ($value->getListComponents() as $part) {
  70                  $this->negate($part);
  71              }
  72          } else if ($value instanceof Size) {
  73              if ($value->getSize() != 0) {
  74                  $value->setSize(-$value->getSize());
  75              }
  76          }
  77      }
  78  
  79      protected function parseComments(array $comments) {
  80          $startRule = '^(\s|\*)*!?rtl:';
  81          foreach ($comments as $comment) {
  82              $content = $comment->getComment();
  83              if (preg_match('/' . $startRule . 'ignore/', $content)) {
  84                  $this->shouldIgnore = 1;
  85              } else if (preg_match('/' . $startRule . 'begin:ignore/', $content)) {
  86                  $this->shouldIgnore = true;
  87              } else if (preg_match('/' . $startRule . 'end:ignore/', $content)) {
  88                  $this->shouldIgnore = false;
  89              } else if (preg_match('/' . $startRule . 'remove/', $content)) {
  90                  $this->shouldRemove = 1;
  91              } else if (preg_match('/' . $startRule . 'begin:remove/', $content)) {
  92                  $this->shouldRemove = true;
  93              } else if (preg_match('/' . $startRule . 'end:remove/', $content)) {
  94                  $this->shouldRemove = false;
  95              } else if (preg_match('/' . $startRule . 'raw:/', $content)) {
  96                  $this->shouldAddCss[] = preg_replace('/' . $startRule . 'raw:/', '', $content);
  97              }
  98          }
  99      }
 100  
 101      protected function processBackground(Rule $rule) {
 102          $value = $rule->getValue();
 103  
 104          // TODO Fix upstream library as it does not parse this well, commas don't take precedence.
 105          // There can be multiple sets of properties per rule.
 106          $hasItems = false;
 107          $items = [$value];
 108          if ($value instanceof RuleValueList && $value->getListSeparator() == ',') {
 109              $hasItems = true;
 110              $items = $value->getListComponents();
 111          }
 112  
 113          // Foreach set.
 114          foreach ($items as $itemKey => $item) {
 115  
 116              // There can be multiple values in the same set.
 117              $hasValues = false;
 118              $parts = [$item];
 119              if ($item instanceof RuleValueList) {
 120                  $hasValues = true;
 121                  $parts = $value->getListComponents();
 122              }
 123  
 124              $requiresPositionalArgument = false;
 125              $hasPositionalArgument = false;
 126              foreach ($parts as $key => $part) {
 127                  $part = $parts[$key];
 128  
 129                  if (!is_object($part)) {
 130                      $flipped = $this->swapLeftRight($part);
 131  
 132                      // Positional arguments can have a size following.
 133                      $hasPositionalArgument = $parts[$key] != $flipped;
 134                      $requiresPositionalArgument = true;
 135  
 136                      $parts[$key] = $flipped;
 137                      continue;
 138  
 139                  } else if ($part instanceof CSSFunction && strpos($part->getName(), 'gradient') !== false) {
 140                      // TODO Fix this.
 141  
 142                  } else if ($part instanceof Size && ($part->getUnit() === '%' || !$part->getUnit())) {
 143  
 144                      // Is this a value we're interested in?
 145                      if (!$requiresPositionalArgument || $hasPositionalArgument) {
 146                          $this->complement($part);
 147                          $part->setUnit('%');
 148                          // We only need to change one value.
 149                          break;
 150                      }
 151  
 152                  }
 153  
 154                  $hasPositionalArgument = false;
 155              }
 156  
 157              if ($hasValues) {
 158                  $item->setListComponents($parts);
 159              } else {
 160                  $items[$itemKey] = $parts[$key];
 161              }
 162          }
 163  
 164          if ($hasItems) {
 165              $value->setListComponents($items);
 166          } else {
 167              $rule->setValue($items[0]);
 168          }
 169      }
 170  
 171      protected function processBlock($block) {
 172          $contents = [];
 173  
 174          foreach ($block->getContents() as $node) {
 175              $this->parseComments($node->getComments());
 176  
 177              if ($toAdd = $this->shouldAddCss()) {
 178                  foreach ($toAdd as $add) {
 179                      $parser = new Parser($add);
 180                      $contents[] = $parser->parse();
 181                  }
 182              }
 183  
 184              if ($this->shouldRemoveNext()) {
 185                  continue;
 186  
 187              } else if (!$this->shouldIgnoreNext()) {
 188                  if ($node instanceof CSSList) {
 189                      $this->processBlock($node);
 190                  }
 191                  if ($node instanceof RuleSet) {
 192                      $this->processDeclaration($node);
 193                  }
 194              }
 195  
 196              $contents[] = $node;
 197          }
 198  
 199          $block->setContents($contents);
 200      }
 201  
 202      protected function processDeclaration($node) {
 203          $rules = [];
 204  
 205          foreach ($node->getRules() as $key => $rule) {
 206              $this->parseComments($rule->getComments());
 207  
 208              if ($toAdd = $this->shouldAddCss()) {
 209                  foreach ($toAdd as $add) {
 210                      $parser = new Parser('.wrapper{' . $add . '}');
 211                      $tree = $parser->parse();
 212                      $contents = $tree->getContents();
 213                      foreach ($contents[0]->getRules() as $newRule) {
 214                          $rules[] = $newRule;
 215                      }
 216                  }
 217              }
 218  
 219              if ($this->shouldRemoveNext()) {
 220                  continue;
 221  
 222              } else if (!$this->shouldIgnoreNext()) {
 223                  $this->processRule($rule);
 224              }
 225  
 226              $rules[] = $rule;
 227          }
 228  
 229          $node->setRules($rules);
 230      }
 231  
 232      protected function processRule($rule) {
 233          $property = $rule->getRule();
 234          $value = $rule->getValue();
 235  
 236          if (preg_match('/direction$/im', $property)) {
 237              $rule->setValue($this->swapLtrRtl($value));
 238  
 239          } else if (preg_match('/left/im', $property)) {
 240              $rule->setRule(str_replace('left', 'right', $property));
 241  
 242          } else if (preg_match('/right/im', $property)) {
 243              $rule->setRule(str_replace('right', 'left', $property));
 244  
 245          } else if (preg_match('/transition(-property)?$/i', $property)) {
 246              $rule->setValue($this->swapLeftRight($value));
 247  
 248          } else if (preg_match('/float|clear|text-align/i', $property)) {
 249              $rule->setValue($this->swapLeftRight($value));
 250  
 251          } else if (preg_match('/^(margin|padding|border-(color|style|width))$/i', $property)) {
 252  
 253              if ($value instanceof RuleValueList) {
 254                  $values = $value->getListComponents();
 255                  $count = count($values);
 256                  if ($count == 4) {
 257                      $right = $values[3];
 258                      $values[3] = $values[1];
 259                      $values[1] = $right;
 260                  }
 261                  $value->setListComponents($values);
 262              }
 263  
 264          } else if (preg_match('/border-radius/i', $property)) {
 265              if ($value instanceof RuleValueList) {
 266  
 267                  // Border radius can contain two lists separated by a slash.
 268                  $groups = $value->getListComponents();
 269                  if ($value->getListSeparator() !== '/') {
 270                      $groups = [$value];
 271                  }
 272                  foreach ($groups as $group) {
 273                      if ($group instanceof RuleValueList) {
 274                          $values = $group->getListComponents();
 275                          switch (count($values)) {
 276                              case 2:
 277                                  $group->setListComponents(array_reverse($values));
 278                                  break;
 279                              case 3:
 280                                  $group->setListComponents([$values[1], $values[0], $values[1], $values[2]]);
 281                                  break;
 282                              case 4:
 283                                  $group->setListComponents([$values[1], $values[0], $values[3], $values[2]]);
 284                                  break;
 285                          }
 286                      }
 287                  }
 288              }
 289  
 290          } else if (preg_match('/shadow/i', $property)) {
 291              // TODO Fix upstream, each shadow should be in a RuleValueList.
 292              if ($value instanceof RuleValueList) {
 293                  // negate($value->getListComponents()[0]);
 294              }
 295  
 296          } else if (preg_match('/transform-origin/i', $property)) {
 297              $this->processTransformOrigin($rule);
 298  
 299          } else if (preg_match('/^(?!text\-).*?transform$/i', $property)) {
 300              // TODO Parse function parameters first.
 301  
 302          } else if (preg_match('/background(-position(-x)?|-image)?$/i', $property)) {
 303              $this->processBackground($rule);
 304  
 305          } else if (preg_match('/cursor/i', $property)) {
 306              $hasList = false;
 307  
 308              $parts = [$value];
 309              if ($value instanceof RuleValueList) {
 310                  $hastList = true;
 311                  $parts = $value->getListComponents();
 312              }
 313  
 314              foreach ($parts as $key => $part) {
 315                  if (!is_object($part)) {
 316                      $parts[$key] = preg_replace_callback('/\b(ne|nw|se|sw|nesw|nwse)-resize/', function($matches) {
 317                          return str_replace($matches[1], str_replace(['e', 'w', '*'], ['*', 'e', 'w'], $matches[1]), $matches[0]);
 318                      }, $part);
 319                  }
 320              }
 321  
 322              if ($hasList) {
 323                  $value->setListComponents($parts);
 324              } else {
 325                  $rule->setValue($parts[0]);
 326              }
 327  
 328          }
 329  
 330      }
 331  
 332      protected function processTransformOrigin(Rule $rule) {
 333          $value = $rule->getValue();
 334          $foundLeftOrRight = false;
 335  
 336          // Search for left or right.
 337          $parts = [$value];
 338          if ($value instanceof RuleValueList) {
 339              $parts = $value->getListComponents();
 340              $isInList = true;
 341          }
 342          foreach ($parts as $key => $part) {
 343              if (!is_object($part) && preg_match('/left|right/i', $part)) {
 344                  $foundLeftOrRight = true;
 345                  $parts[$key] = $this->swapLeftRight($part);
 346              }
 347          }
 348  
 349          if ($foundLeftOrRight) {
 350              // We need to reconstruct the value because left/right are not represented by an object.
 351              $list = new RuleValueList(' ');
 352              $list->setListComponents($parts);
 353              $rule->setValue($list);
 354  
 355          } else {
 356  
 357              $value = $parts[0];
 358              // The first value may be referencing top or bottom (y instead of x).
 359              if (!is_object($value) && preg_match('/top|bottom/i', $value) && count($parts)>1) {
 360                  $value = $parts[1];
 361              }
 362  
 363              // Flip the value.
 364              if ($value instanceof Size) {
 365  
 366                  if ($value->getSize() == 0) {
 367                      $value->setSize(100);
 368                      $value->setUnit('%');
 369  
 370                  } else if ($value->getUnit() === '%') {
 371                      $this->complement($value);
 372                  }
 373  
 374              } else if ($value instanceof CSSFunction && strpos($value->getName(), 'calc') !== false) {
 375                  // TODO Fix upstream calc parsing.
 376                  $this->complement($value);
 377              }
 378          }
 379      }
 380  
 381      protected function shouldAddCss() {
 382          if (!empty($this->shouldAddCss)) {
 383              $css = $this->shouldAddCss;
 384              $this->shouldAddCss = [];
 385              return $css;
 386          }
 387          return [];
 388      }
 389  
 390      protected function shouldIgnoreNext() {
 391          if ($this->shouldIgnore) {
 392              if (is_int($this->shouldIgnore)) {
 393                  $this->shouldIgnore--;
 394              }
 395              return true;
 396          }
 397          return false;
 398      }
 399  
 400      protected function shouldRemoveNext() {
 401          if ($this->shouldRemove) {
 402              if (is_int($this->shouldRemove)) {
 403                  $this->shouldRemove--;
 404              }
 405              return true;
 406          }
 407          return false;
 408      }
 409  
 410      protected function swap($value, $a, $b, $options = ['scope' => '*', 'ignoreCase' => true]) {
 411          $expr = preg_quote($a) . '|' . preg_quote($b);
 412          if (!empty($options['greedy'])) {
 413              $expr = '\\b(' . $expr . ')\\b';
 414          }
 415          $flags = !empty($options['ignoreCase']) ? 'im' : 'm';
 416          $expr = "/$expr/$flags";
 417          return preg_replace_callback($expr, function($matches) use ($a, $b, $options) {
 418              return $this->compare($matches[0], $a, !empty($options['ignoreCase'])) ? $b : $a;
 419          }, $value);
 420      }
 421  
 422      protected function swapLeftRight($value) {
 423          return $this->swap($value, 'left', 'right');
 424      }
 425  
 426      protected function swapLtrRtl($value) {
 427          return $this->swap($value, 'ltr', 'rtl');
 428      }
 429  
 430  }