See Release Notes
Long Term Support Release
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 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body