PageRenderTime 46ms CodeModel.GetById 13ms RepoModel.GetById 1ms app.codeStats 0ms

/lib/rtlcss/RTLCSS.php

https://gitlab.com/unofficial-mirrors/moodle
PHP | 428 lines | 321 code | 76 blank | 31 comment | 106 complexity | c4fc3d315ec11e8e4a5768f613bd998a MD5 | raw file
  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. namespace MoodleHQ\RTLCSS;
  10. use Sabberworm\CSS\CSSList\CSSList;
  11. use Sabberworm\CSS\CSSList\Document;
  12. use Sabberworm\CSS\OutputFormat;
  13. use Sabberworm\CSS\Parser;
  14. use Sabberworm\CSS\Rule\Rule;
  15. use Sabberworm\CSS\RuleSet\RuleSet;
  16. use Sabberworm\CSS\Settings;
  17. use Sabberworm\CSS\Value\CSSFunction;
  18. use Sabberworm\CSS\Value\CSSString;
  19. use Sabberworm\CSS\Value\PrimitiveValue;
  20. use Sabberworm\CSS\Value\RuleValueList;
  21. use Sabberworm\CSS\Value\Size;
  22. use Sabberworm\CSS\Value\ValueList;
  23. /**
  24. * RTLCSS Class.
  25. *
  26. * @package MoodleHQ\RTLCSS
  27. * @copyright 2016 Frédéric Massart - FMCorz.net
  28. * @license https://opensource.org/licenses/MIT MIT
  29. */
  30. class RTLCSS {
  31. protected $tree;
  32. protected $shouldAddCss = [];
  33. protected $shouldIgnore = false;
  34. protected $shouldRemove = false;
  35. public function __construct(Document $tree) {
  36. $this->tree = $tree;
  37. }
  38. protected function compare($what, $to, $ignoreCase) {
  39. if ($ignoreCase) {
  40. return strtolower($what) === strtolower($to);
  41. }
  42. return $what === $to;
  43. }
  44. protected function complement($value) {
  45. if ($value instanceof Size) {
  46. $value->setSize(100 - $value->getSize());
  47. } else if ($value instanceof CSSFunction) {
  48. $arguments = implode($value->getListSeparator(), $value->getArguments());
  49. $arguments = "100% - ($arguments)";
  50. $value->setListComponents([$arguments]);
  51. }
  52. }
  53. public function flip() {
  54. $this->processBlock($this->tree);
  55. return $this->tree;
  56. }
  57. protected function negate($value) {
  58. if ($value instanceof ValueList) {
  59. foreach ($value->getListComponents() as $part) {
  60. $this->negate($part);
  61. }
  62. } else if ($value instanceof Size) {
  63. if ($value->getSize() != 0) {
  64. $value->setSize(-$value->getSize());
  65. }
  66. }
  67. }
  68. protected function parseComments(array $comments) {
  69. $startRule = '^(\s|\*)*!?rtl:';
  70. foreach ($comments as $comment) {
  71. $content = $comment->getComment();
  72. if (preg_match('/' . $startRule . 'ignore/', $content)) {
  73. $this->shouldIgnore = 1;
  74. } else if (preg_match('/' . $startRule . 'begin:ignore/', $content)) {
  75. $this->shouldIgnore = true;
  76. } else if (preg_match('/' . $startRule . 'end:ignore/', $content)) {
  77. $this->shouldIgnore = false;
  78. } else if (preg_match('/' . $startRule . 'remove/', $content)) {
  79. $this->shouldRemove = 1;
  80. } else if (preg_match('/' . $startRule . 'begin:remove/', $content)) {
  81. $this->shouldRemove = true;
  82. } else if (preg_match('/' . $startRule . 'end:remove/', $content)) {
  83. $this->shouldRemove = false;
  84. } else if (preg_match('/' . $startRule . 'raw:/', $content)) {
  85. $this->shouldAddCss[] = preg_replace('/' . $startRule . 'raw:/', '', $content);
  86. }
  87. }
  88. }
  89. protected function processBackground(Rule $rule) {
  90. $value = $rule->getValue();
  91. // TODO Fix upstream library as it does not parse this well, commas don't take precedence.
  92. // There can be multiple sets of properties per rule.
  93. $hasItems = false;
  94. $items = [$value];
  95. if ($value instanceof RuleValueList && $value->getListSeparator() == ',') {
  96. $hasItems = true;
  97. $items = $value->getListComponents();
  98. }
  99. // Foreach set.
  100. foreach ($items as $itemKey => $item) {
  101. // There can be multiple values in the same set.
  102. $hasValues = false;
  103. $parts = [$item];
  104. if ($item instanceof RuleValueList) {
  105. $hasValues = true;
  106. $parts = $value->getListComponents();
  107. }
  108. $requiresPositionalArgument = false;
  109. $hasPositionalArgument = false;
  110. foreach ($parts as $key => $part) {
  111. $part = $parts[$key];
  112. if (!is_object($part)) {
  113. $flipped = $this->swapLeftRight($part);
  114. // Positional arguments can have a size following.
  115. $hasPositionalArgument = $parts[$key] != $flipped;
  116. $requiresPositionalArgument = true;
  117. $parts[$key] = $flipped;
  118. continue;
  119. } else if ($part instanceof CSSFunction && strpos($part->getName(), 'gradient') !== false) {
  120. // TODO Fix this.
  121. } else if ($part instanceof Size && ($part->getUnit() === '%' || !$part->getUnit())) {
  122. // Is this a value we're interested in?
  123. if (!$requiresPositionalArgument || $hasPositionalArgument) {
  124. $this->complement($part);
  125. $part->setUnit('%');
  126. // We only need to change one value.
  127. break;
  128. }
  129. }
  130. $hasPositionalArgument = false;
  131. }
  132. if ($hasValues) {
  133. $item->setListComponents($parts);
  134. } else {
  135. $items[$itemKey] = $parts[$key];
  136. }
  137. }
  138. if ($hasItems) {
  139. $value->setListComponents($items);
  140. } else {
  141. $rule->setValue($items[0]);
  142. }
  143. }
  144. protected function processBlock($block) {
  145. $contents = [];
  146. foreach ($block->getContents() as $node) {
  147. $this->parseComments($node->getComments());
  148. if ($toAdd = $this->shouldAddCss()) {
  149. foreach ($toAdd as $add) {
  150. $parser = new Parser($add);
  151. $contents[] = $parser->parse();
  152. }
  153. }
  154. if ($this->shouldRemoveNext()) {
  155. continue;
  156. } else if (!$this->shouldIgnoreNext()) {
  157. if ($node instanceof CSSList) {
  158. $this->processBlock($node);
  159. }
  160. if ($node instanceof RuleSet) {
  161. $this->processDeclaration($node);
  162. }
  163. }
  164. $contents[] = $node;
  165. }
  166. $block->setContents($contents);
  167. }
  168. protected function processDeclaration($node) {
  169. $rules = [];
  170. foreach ($node->getRules() as $key => $rule) {
  171. $this->parseComments($rule->getComments());
  172. if ($toAdd = $this->shouldAddCss()) {
  173. foreach ($toAdd as $add) {
  174. $parser = new Parser('.wrapper{' . $add . '}');
  175. $tree = $parser->parse();
  176. $contents = $tree->getContents();
  177. foreach ($contents[0]->getRules() as $newRule) {
  178. $rules[] = $newRule;
  179. }
  180. }
  181. }
  182. if ($this->shouldRemoveNext()) {
  183. continue;
  184. } else if (!$this->shouldIgnoreNext()) {
  185. $this->processRule($rule);
  186. }
  187. $rules[] = $rule;
  188. }
  189. $node->setRules($rules);
  190. }
  191. protected function processRule($rule) {
  192. $property = $rule->getRule();
  193. $value = $rule->getValue();
  194. if (preg_match('/direction$/im', $property)) {
  195. $rule->setValue($this->swapLtrRtl($value));
  196. } else if (preg_match('/left/im', $property)) {
  197. $rule->setRule(str_replace('left', 'right', $property));
  198. } else if (preg_match('/right/im', $property)) {
  199. $rule->setRule(str_replace('right', 'left', $property));
  200. } else if (preg_match('/transition(-property)?$/i', $property)) {
  201. $rule->setValue($this->swapLeftRight($value));
  202. } else if (preg_match('/float|clear|text-align/i', $property)) {
  203. $rule->setValue($this->swapLeftRight($value));
  204. } else if (preg_match('/^(margin|padding|border-(color|style|width))$/i', $property)) {
  205. if ($value instanceof RuleValueList) {
  206. $values = $value->getListComponents();
  207. $count = count($values);
  208. if ($count == 4) {
  209. $right = $values[3];
  210. $values[3] = $values[1];
  211. $values[1] = $right;
  212. }
  213. $value->setListComponents($values);
  214. }
  215. } else if (preg_match('/border-radius/i', $property)) {
  216. if ($value instanceof RuleValueList) {
  217. // Border radius can contain two lists separated by a slash.
  218. $groups = $value->getListComponents();
  219. if ($value->getListSeparator() !== '/') {
  220. $groups = [$value];
  221. }
  222. foreach ($groups as $group) {
  223. $values = $group->getListComponents();
  224. switch (count($values)) {
  225. case 2:
  226. $group->setListComponents(array_reverse($values));
  227. break;
  228. case 3:
  229. $group->setListComponents([$values[1], $values[0], $values[1], $values[2]]);
  230. break;
  231. case 4:
  232. $group->setListComponents([$values[1], $values[0], $values[3], $values[2]]);
  233. break;
  234. }
  235. }
  236. }
  237. } else if (preg_match('/shadow/i', $property)) {
  238. // TODO Fix upstream, each shadow should be in a RuleValueList.
  239. if ($value instanceof RuleValueList) {
  240. // negate($value->getListComponents()[0]);
  241. }
  242. } else if (preg_match('/transform-origin/i', $property)) {
  243. $this->processTransformOrigin($rule);
  244. } else if (preg_match('/^(?!text\-).*?transform$/i', $property)) {
  245. // TODO Parse function parameters first.
  246. } else if (preg_match('/background(-position(-x)?|-image)?$/i', $property)) {
  247. $this->processBackground($rule);
  248. } else if (preg_match('/cursor/i', $property)) {
  249. $hasList = false;
  250. $parts = [$value];
  251. if ($value instanceof RuleValueList) {
  252. $hastList = true;
  253. $parts = $value->getListComponents();
  254. }
  255. foreach ($parts as $key => $part) {
  256. if (!is_object($part)) {
  257. $parts[$key] = preg_replace_callback('/\b(ne|nw|se|sw|nesw|nwse)-resize/', function($matches) {
  258. return str_replace($matches[1], str_replace(['e', 'w', '*'], ['*', 'e', 'w'], $matches[1]), $matches[0]);
  259. }, $part);
  260. }
  261. }
  262. if ($hasList) {
  263. $value->setListComponents($parts);
  264. } else {
  265. $rule->setValue($parts[0]);
  266. }
  267. }
  268. }
  269. protected function processTransformOrigin(Rule $rule) {
  270. $value = $rule->getValue();
  271. $foundLeftOrRight = false;
  272. // Search for left or right.
  273. $parts = [$value];
  274. if ($value instanceof RuleValueList) {
  275. $parts = $value->getListComponents();
  276. $isInList = true;
  277. }
  278. foreach ($parts as $key => $part) {
  279. if (!is_object($part) && preg_match('/left|right/i', $part)) {
  280. $foundLeftOrRight = true;
  281. $parts[$key] = $this->swapLeftRight($part);
  282. }
  283. }
  284. if ($foundLeftOrRight) {
  285. // We need to reconstruct the value because left/right are not represented by an object.
  286. $list = new RuleValueList(' ');
  287. $list->setListComponents($parts);
  288. $rule->setValue($list);
  289. } else {
  290. $value = $parts[0];
  291. // The first value may be referencing top or bottom (y instead of x).
  292. if (!is_object($value) && preg_match('/top|bottom/i', $value)) {
  293. $value = $parts[1];
  294. }
  295. // Flip the value.
  296. if ($value instanceof Size) {
  297. if ($value->getSize() == 0) {
  298. $value->setSize(100);
  299. $value->setUnit('%');
  300. } else if ($value->getUnit() === '%') {
  301. $this->complement($value);
  302. }
  303. } else if ($value instanceof CSSFunction && strpos($value->getName(), 'calc') !== false) {
  304. // TODO Fix upstream calc parsing.
  305. $this->complement($value);
  306. }
  307. }
  308. }
  309. protected function shouldAddCss() {
  310. if (!empty($this->shouldAddCss)) {
  311. $css = $this->shouldAddCss;
  312. $this->shouldAddCss = [];
  313. return $css;
  314. }
  315. return [];
  316. }
  317. protected function shouldIgnoreNext() {
  318. if ($this->shouldIgnore) {
  319. if (is_int($this->shouldIgnore)) {
  320. $this->shouldIgnore--;
  321. }
  322. return true;
  323. }
  324. return false;
  325. }
  326. protected function shouldRemoveNext() {
  327. if ($this->shouldRemove) {
  328. if (is_int($this->shouldRemove)) {
  329. $this->shouldRemove--;
  330. }
  331. return true;
  332. }
  333. return false;
  334. }
  335. protected function swap($value, $a, $b, $options = ['scope' => '*', 'ignoreCase' => true]) {
  336. $expr = preg_quote($a) . '|' . preg_quote($b);
  337. if (!empty($options['greedy'])) {
  338. $expr = '\\b(' . $expr . ')\\b';
  339. }
  340. $flags = !empty($options['ignoreCase']) ? 'im' : 'm';
  341. $expr = "/$expr/$flags";
  342. return preg_replace_callback($expr, function($matches) use ($a, $b, $options) {
  343. return $this->compare($matches[0], $a, !empty($options['ignoreCase'])) ? $b : $a;
  344. }, $value);
  345. }
  346. protected function swapLeftRight($value) {
  347. return $this->swap($value, 'left', 'right');
  348. }
  349. protected function swapLtrRtl($value) {
  350. return $this->swap($value, 'ltr', 'rtl');
  351. }
  352. }