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