Merge branch 'MDL-62560-master'
[moodle.git] / lib / rtlcss / RTLCSS.php
blob42773c0ea42e3de63759bb313b459b98e8e6ea5a
1 <?php
2 /**
3 * RTLCSS.
5 * @package MoodleHQ\RTLCSS
6 * @copyright 2016 Frédéric Massart - FMCorz.net
7 * @license https://opensource.org/licenses/MIT MIT
8 */
10 namespace MoodleHQ\RTLCSS;
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;
26 /**
27 * RTLCSS Class.
29 * @package MoodleHQ\RTLCSS
30 * @copyright 2016 Frédéric Massart - FMCorz.net
31 * @license https://opensource.org/licenses/MIT MIT
33 class RTLCSS {
35 protected $tree;
36 protected $shouldAddCss = [];
37 protected $shouldIgnore = false;
38 protected $shouldRemove = false;
40 public function __construct(Document $tree) {
41 $this->tree = $tree;
44 protected function compare($what, $to, $ignoreCase) {
45 if ($ignoreCase) {
46 return strtolower($what) === strtolower($to);
48 return $what === $to;
51 protected function complement($value) {
52 if ($value instanceof Size) {
53 $value->setSize(100 - $value->getSize());
55 } else if ($value instanceof CSSFunction) {
56 $arguments = implode($value->getListSeparator(), $value->getArguments());
57 $arguments = "100% - ($arguments)";
58 $value->setListComponents([$arguments]);
62 public function flip() {
63 $this->processBlock($this->tree);
64 return $this->tree;
67 protected function negate($value) {
68 if ($value instanceof ValueList) {
69 foreach ($value->getListComponents() as $part) {
70 $this->negate($part);
72 } else if ($value instanceof Size) {
73 if ($value->getSize() != 0) {
74 $value->setSize(-$value->getSize());
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);
101 protected function processBackground(Rule $rule) {
102 $value = $rule->getValue();
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();
113 // Foreach set.
114 foreach ($items as $itemKey => $item) {
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();
124 $requiresPositionalArgument = false;
125 $hasPositionalArgument = false;
126 foreach ($parts as $key => $part) {
127 $part = $parts[$key];
129 if (!is_object($part)) {
130 $flipped = $this->swapLeftRight($part);
132 // Positional arguments can have a size following.
133 $hasPositionalArgument = $parts[$key] != $flipped;
134 $requiresPositionalArgument = true;
136 $parts[$key] = $flipped;
137 continue;
139 } else if ($part instanceof CSSFunction && strpos($part->getName(), 'gradient') !== false) {
140 // TODO Fix this.
142 } else if ($part instanceof Size && ($part->getUnit() === '%' || !$part->getUnit())) {
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;
154 $hasPositionalArgument = false;
157 if ($hasValues) {
158 $item->setListComponents($parts);
159 } else {
160 $items[$itemKey] = $parts[$key];
164 if ($hasItems) {
165 $value->setListComponents($items);
166 } else {
167 $rule->setValue($items[0]);
171 protected function processBlock($block) {
172 $contents = [];
174 foreach ($block->getContents() as $node) {
175 $this->parseComments($node->getComments());
177 if ($toAdd = $this->shouldAddCss()) {
178 foreach ($toAdd as $add) {
179 $parser = new Parser($add);
180 $contents[] = $parser->parse();
184 if ($this->shouldRemoveNext()) {
185 continue;
187 } else if (!$this->shouldIgnoreNext()) {
188 if ($node instanceof CSSList) {
189 $this->processBlock($node);
191 if ($node instanceof RuleSet) {
192 $this->processDeclaration($node);
196 $contents[] = $node;
199 $block->setContents($contents);
202 protected function processDeclaration($node) {
203 $rules = [];
205 foreach ($node->getRules() as $key => $rule) {
206 $this->parseComments($rule->getComments());
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;
219 if ($this->shouldRemoveNext()) {
220 continue;
222 } else if (!$this->shouldIgnoreNext()) {
223 $this->processRule($rule);
226 $rules[] = $rule;
229 $node->setRules($rules);
232 protected function processRule($rule) {
233 $property = $rule->getRule();
234 $value = $rule->getValue();
236 if (preg_match('/direction$/im', $property)) {
237 $rule->setValue($this->swapLtrRtl($value));
239 } else if (preg_match('/left/im', $property)) {
240 $rule->setRule(str_replace('left', 'right', $property));
242 } else if (preg_match('/right/im', $property)) {
243 $rule->setRule(str_replace('right', 'left', $property));
245 } else if (preg_match('/transition(-property)?$/i', $property)) {
246 $rule->setValue($this->swapLeftRight($value));
248 } else if (preg_match('/float|clear|text-align/i', $property)) {
249 $rule->setValue($this->swapLeftRight($value));
251 } else if (preg_match('/^(margin|padding|border-(color|style|width))$/i', $property)) {
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;
261 $value->setListComponents($values);
264 } else if (preg_match('/border-radius/i', $property)) {
265 if ($value instanceof RuleValueList) {
267 // Border radius can contain two lists separated by a slash.
268 $groups = $value->getListComponents();
269 if ($value->getListSeparator() !== '/') {
270 $groups = [$value];
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;
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]);
294 } else if (preg_match('/transform-origin/i', $property)) {
295 $this->processTransformOrigin($rule);
297 } else if (preg_match('/^(?!text\-).*?transform$/i', $property)) {
298 // TODO Parse function parameters first.
300 } else if (preg_match('/background(-position(-x)?|-image)?$/i', $property)) {
301 $this->processBackground($rule);
303 } else if (preg_match('/cursor/i', $property)) {
304 $hasList = false;
306 $parts = [$value];
307 if ($value instanceof RuleValueList) {
308 $hastList = true;
309 $parts = $value->getListComponents();
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);
320 if ($hasList) {
321 $value->setListComponents($parts);
322 } else {
323 $rule->setValue($parts[0]);
330 protected function processTransformOrigin(Rule $rule) {
331 $value = $rule->getValue();
332 $foundLeftOrRight = false;
334 // Search for left or right.
335 $parts = [$value];
336 if ($value instanceof RuleValueList) {
337 $parts = $value->getListComponents();
338 $isInList = true;
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);
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);
353 } else {
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];
361 // Flip the value.
362 if ($value instanceof Size) {
364 if ($value->getSize() == 0) {
365 $value->setSize(100);
366 $value->setUnit('%');
368 } else if ($value->getUnit() === '%') {
369 $this->complement($value);
372 } else if ($value instanceof CSSFunction && strpos($value->getName(), 'calc') !== false) {
373 // TODO Fix upstream calc parsing.
374 $this->complement($value);
379 protected function shouldAddCss() {
380 if (!empty($this->shouldAddCss)) {
381 $css = $this->shouldAddCss;
382 $this->shouldAddCss = [];
383 return $css;
385 return [];
388 protected function shouldIgnoreNext() {
389 if ($this->shouldIgnore) {
390 if (is_int($this->shouldIgnore)) {
391 $this->shouldIgnore--;
393 return true;
395 return false;
398 protected function shouldRemoveNext() {
399 if ($this->shouldRemove) {
400 if (is_int($this->shouldRemove)) {
401 $this->shouldRemove--;
403 return true;
405 return false;
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';
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);
420 protected function swapLeftRight($value) {
421 return $this->swap($value, 'left', 'right');
424 protected function swapLtrRtl($value) {
425 return $this->swap($value, 'ltr', 'rtl');