5 * @package MoodleHQ\RTLCSS
6 * @copyright 2016 Frédéric Massart - FMCorz.net
7 * @license https://opensource.org/licenses/MIT MIT
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
;
29 * @package MoodleHQ\RTLCSS
30 * @copyright 2016 Frédéric Massart - FMCorz.net
31 * @license https://opensource.org/licenses/MIT MIT
36 protected $shouldAddCss = [];
37 protected $shouldIgnore = false;
38 protected $shouldRemove = false;
40 public function __construct(Document
$tree) {
44 protected function compare($what, $to, $ignoreCase) {
46 return strtolower($what) === strtolower($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
);
67 protected function negate($value) {
68 if ($value instanceof ValueList
) {
69 foreach ($value->getListComponents() as $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.
108 if ($value instanceof RuleValueList
&& $value->getListSeparator() == ',') {
110 $items = $value->getListComponents();
114 foreach ($items as $itemKey => $item) {
116 // There can be multiple values in the same set.
119 if ($item instanceof RuleValueList
) {
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;
139 } else if ($part instanceof CSSFunction
&& strpos($part->getName(), 'gradient') !== false) {
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);
148 // We only need to change one value.
154 $hasPositionalArgument = false;
158 $item->setListComponents($parts);
160 $items[$itemKey] = $parts[$key];
165 $value->setListComponents($items);
167 $rule->setValue($items[0]);
171 protected function processBlock($block) {
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()) {
187 } else if (!$this->shouldIgnoreNext()) {
188 if ($node instanceof CSSList
) {
189 $this->processBlock($node);
191 if ($node instanceof RuleSet
) {
192 $this->processDeclaration($node);
199 $block->setContents($contents);
202 protected function processDeclaration($node) {
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) {
219 if ($this->shouldRemoveNext()) {
222 } else if (!$this->shouldIgnoreNext()) {
223 $this->processRule($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);
258 $values[3] = $values[1];
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() !== '/') {
272 foreach ($groups as $group) {
273 if ($group instanceof RuleValueList
) {
274 $values = $group->getListComponents();
275 switch (count($values)) {
277 $group->setListComponents(array_reverse($values));
280 $group->setListComponents([$values[1], $values[0], $values[1], $values[2]]);
283 $group->setListComponents([$values[1], $values[0], $values[3], $values[2]]);
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)) {
309 if ($value instanceof RuleValueList
) {
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]);
323 $value->setListComponents($parts);
325 $rule->setValue($parts[0]);
332 protected function processTransformOrigin(Rule
$rule) {
333 $value = $rule->getValue();
334 $foundLeftOrRight = false;
336 // Search for left or right.
338 if ($value instanceof RuleValueList
) {
339 $parts = $value->getListComponents();
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);
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) {
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
= [];
390 protected function shouldIgnoreNext() {
391 if ($this->shouldIgnore
) {
392 if (is_int($this->shouldIgnore
)) {
393 $this->shouldIgnore
--;
400 protected function shouldRemoveNext() {
401 if ($this->shouldRemove
) {
402 if (is_int($this->shouldRemove
)) {
403 $this->shouldRemove
--;
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;
422 protected function swapLeftRight($value) {
423 return $this->swap($value, 'left', 'right');
426 protected function swapLtrRtl($value) {
427 return $this->swap($value, 'ltr', 'rtl');