Search moodle.org's
Developer Documentation

See Release Notes
Long Term Support Release

  • Bug fixes for general core bugs in 3.9.x will end* 10 May 2021 (12 months).
  • Bug fixes for security issues in 3.9.x will end* 8 May 2023 (36 months).
  • PHP version: minimum PHP 7.2.0 Note: minimum PHP version has increased since Moodle 3.8. PHP 7.3.x and 7.4.x are supported too.

Differences Between: [Versions 39 and 311] [Versions 39 and 400] [Versions 39 and 401] [Versions 39 and 402] [Versions 39 and 403]

   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                      $values = $group->getListComponents();
 274                      switch (count($values)) {
 275                          case 2:
 276                              $group->setListComponents(array_reverse($values));
 277                              break;
 278                          case 3:
 279                              $group->setListComponents([$values[1], $values[0], $values[1], $values[2]]);
 280                              break;
 281                          case 4:
 282                              $group->setListComponents([$values[1], $values[0], $values[3], $values[2]]);
 283                              break;
 284                      }
 285                  }
 286              }
 287  
 288          } else if (preg_match('/shadow/i', $property)) {
 289              // TODO Fix upstream, each shadow should be in a RuleValueList.
 290              if ($value instanceof RuleValueList) {
 291                  // negate($value->getListComponents()[0]);
 292              }
 293  
 294          } else if (preg_match('/transform-origin/i', $property)) {
 295              $this->processTransformOrigin($rule);
 296  
 297          } else if (preg_match('/^(?!text\-).*?transform$/i', $property)) {
 298              // TODO Parse function parameters first.
 299  
 300          } else if (preg_match('/background(-position(-x)?|-image)?$/i', $property)) {
 301              $this->processBackground($rule);
 302  
 303          } else if (preg_match('/cursor/i', $property)) {
 304              $hasList = false;
 305  
 306              $parts = [$value];
 307              if ($value instanceof RuleValueList) {
 308                  $hastList = true;
 309                  $parts = $value->getListComponents();
 310              }
 311  
 312              foreach ($parts as $key => $part) {
 313                  if (!is_object($part)) {
 314                      $parts[$key] = preg_replace_callback('/\b(ne|nw|se|sw|nesw|nwse)-resize/', function($matches) {
 315                          return str_replace($matches[1], str_replace(['e', 'w', '*'], ['*', 'e', 'w'], $matches[1]), $matches[0]);
 316                      }, $part);
 317                  }
 318              }
 319  
 320              if ($hasList) {
 321                  $value->setListComponents($parts);
 322              } else {
 323                  $rule->setValue($parts[0]);
 324              }
 325  
 326          }
 327  
 328      }
 329  
 330      protected function processTransformOrigin(Rule $rule) {
 331          $value = $rule->getValue();
 332          $foundLeftOrRight = false;
 333  
 334          // Search for left or right.
 335          $parts = [$value];
 336          if ($value instanceof RuleValueList) {
 337              $parts = $value->getListComponents();
 338              $isInList = true;
 339          }
 340          foreach ($parts as $key => $part) {
 341              if (!is_object($part) && preg_match('/left|right/i', $part)) {
 342                  $foundLeftOrRight = true;
 343                  $parts[$key] = $this->swapLeftRight($part);
 344              }
 345          }
 346  
 347          if ($foundLeftOrRight) {
 348              // We need to reconstruct the value because left/right are not represented by an object.
 349              $list = new RuleValueList(' ');
 350              $list->setListComponents($parts);
 351              $rule->setValue($list);
 352  
 353          } else {
 354  
 355              $value = $parts[0];
 356              // The first value may be referencing top or bottom (y instead of x).
 357              if (!is_object($value) && preg_match('/top|bottom/i', $value)) {
 358                  $value = $parts[1];
 359              }
 360  
 361              // Flip the value.
 362              if ($value instanceof Size) {
 363  
 364                  if ($value->getSize() == 0) {
 365                      $value->setSize(100);
 366                      $value->setUnit('%');
 367  
 368                  } else if ($value->getUnit() === '%') {
 369                      $this->complement($value);
 370                  }
 371  
 372              } else if ($value instanceof CSSFunction && strpos($value->getName(), 'calc') !== false) {
 373                  // TODO Fix upstream calc parsing.
 374                  $this->complement($value);
 375              }
 376          }
 377      }
 378  
 379      protected function shouldAddCss() {
 380          if (!empty($this->shouldAddCss)) {
 381              $css = $this->shouldAddCss;
 382              $this->shouldAddCss = [];
 383              return $css;
 384          }
 385          return [];
 386      }
 387  
 388      protected function shouldIgnoreNext() {
 389          if ($this->shouldIgnore) {
 390              if (is_int($this->shouldIgnore)) {
 391                  $this->shouldIgnore--;
 392              }
 393              return true;
 394          }
 395          return false;
 396      }
 397  
 398      protected function shouldRemoveNext() {
 399          if ($this->shouldRemove) {
 400              if (is_int($this->shouldRemove)) {
 401                  $this->shouldRemove--;
 402              }
 403              return true;
 404          }
 405          return false;
 406      }
 407  
 408      protected function swap($value, $a, $b, $options = ['scope' => '*', 'ignoreCase' => true]) {
 409          $expr = preg_quote($a) . '|' . preg_quote($b);
 410          if (!empty($options['greedy'])) {
 411              $expr = '\\b(' . $expr . ')\\b';
 412          }
 413          $flags = !empty($options['ignoreCase']) ? 'im' : 'm';
 414          $expr = "/$expr/$flags";
 415          return preg_replace_callback($expr, function($matches) use ($a, $b, $options) {
 416              return $this->compare($matches[0], $a, !empty($options['ignoreCase'])) ? $b : $a;
 417          }, $value);
 418      }
 419  
 420      protected function swapLeftRight($value) {
 421          return $this->swap($value, 'left', 'right');
 422      }
 423  
 424      protected function swapLtrRtl($value) {
 425          return $this->swap($value, 'ltr', 'rtl');
 426      }
 427  
 428  }