MDL-81386 output: Fix failure in PHPUnit when running filtered tests
[moodle.git] / lib / rtlcss / RTLCSS.php
blob1e14e805e2fed7261ec61cd397d8ed892ac49fb0
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 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;
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]);
296 } else if (preg_match('/transform-origin/i', $property)) {
297 $this->processTransformOrigin($rule);
299 } else if (preg_match('/^(?!text\-).*?transform$/i', $property)) {
300 // TODO Parse function parameters first.
302 } else if (preg_match('/background(-position(-x)?|-image)?$/i', $property)) {
303 $this->processBackground($rule);
305 } else if (preg_match('/cursor/i', $property)) {
306 $hasList = false;
308 $parts = [$value];
309 if ($value instanceof RuleValueList) {
310 $hastList = true;
311 $parts = $value->getListComponents();
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);
322 if ($hasList) {
323 $value->setListComponents($parts);
324 } else {
325 $rule->setValue($parts[0]);
332 protected function processTransformOrigin(Rule $rule) {
333 $value = $rule->getValue();
334 $foundLeftOrRight = false;
336 // Search for left or right.
337 $parts = [$value];
338 if ($value instanceof RuleValueList) {
339 $parts = $value->getListComponents();
340 $isInList = true;
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);
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);
355 } else {
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];
363 // Flip the value.
364 if ($value instanceof Size) {
366 if ($value->getSize() == 0) {
367 $value->setSize(100);
368 $value->setUnit('%');
370 } else if ($value->getUnit() === '%') {
371 $this->complement($value);
374 } else if ($value instanceof CSSFunction && strpos($value->getName(), 'calc') !== false) {
375 // TODO Fix upstream calc parsing.
376 $this->complement($value);
381 protected function shouldAddCss() {
382 if (!empty($this->shouldAddCss)) {
383 $css = $this->shouldAddCss;
384 $this->shouldAddCss = [];
385 return $css;
387 return [];
390 protected function shouldIgnoreNext() {
391 if ($this->shouldIgnore) {
392 if (is_int($this->shouldIgnore)) {
393 $this->shouldIgnore--;
395 return true;
397 return false;
400 protected function shouldRemoveNext() {
401 if ($this->shouldRemove) {
402 if (is_int($this->shouldRemove)) {
403 $this->shouldRemove--;
405 return true;
407 return false;
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';
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);
422 protected function swapLeftRight($value) {
423 return $this->swap($value, 'left', 'right');
426 protected function swapLtrRtl($value) {
427 return $this->swap($value, 'ltr', 'rtl');