PluginProbe
MaxButtons – Create buttons / 6.3
MaxButtons – Create buttons v6.3
6.23 6.24 6.25 6.26 6.26.1 6.27 6.28 6.3 6.4 6.5 6.6 6.7 6.8 6.9 7.0 7.1 7.1.1 7.1.2 7.1.3 7.10 7.11 7.13 7.13.1 7.13.2 7.13.3 All 100 releases
maxbuttons / assets / libraries / scssphp / src / Compiler.php

Compiler.php in MaxButtons – Create buttons 6.3, at assets/libraries/scssphp/src/Compiler.php

4,609 lines 115.8 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * SCSSPHP
4 *
5 * @copyright 2012-2015 Leaf Corcoran
6 *
7 * @license http://opensource.org/licenses/MIT MIT
8 *
9 * @link http://leafo.github.io/scssphp
10 */
11
12 namespace Leafo\ScssPhp;
13
14 use Leafo\ScssPhp\Base\Range;
15 use Leafo\ScssPhp\Colors;
16 use Leafo\ScssPhp\Parser;
17 use Leafo\ScssPhp\Util;
18
19 /**
20 * The scss compiler and parser.
21 *
22 * Converting SCSS to CSS is a three stage process. The incoming file is parsed
23 * by `Parser` into a syntax tree, then it is compiled into another tree
24 * representing the CSS structure by `Compiler`. The CSS tree is fed into a
25 * formatter, like `Formatter` which then outputs CSS as a string.
26 *
27 * During the first compile, all values are *reduced*, which means that their
28 * types are brought to the lowest form before being dump as strings. This
29 * handles math equations, variable dereferences, and the like.
30 *
31 * The `compile` function of `Compiler` is the entry point.
32 *
33 * In summary:
34 *
35 * The `Compiler` class creates an instance of the parser, feeds it SCSS code,
36 * then transforms the resulting tree to a CSS tree. This class also holds the
37 * evaluation context, such as all available mixins and variables at any given
38 * time.
39 *
40 * The `Parser` class is only concerned with parsing its input.
41 *
42 * The `Formatter` takes a CSS tree, and dumps it to a formatted string,
43 * handling things like indentation.
44 */
45
46 /**
47 * SCSS compiler
48 *
49 * @author Leaf Corcoran <leafot@gmail.com>
50 */
51 class Compiler
52 {
53 const LINE_COMMENTS = 1;
54 const DEBUG_INFO = 2;
55
56 /**
57 * @var array
58 */
59 static protected $operatorNames = array(
60 '+' => 'add',
61 '-' => 'sub',
62 '*' => 'mul',
63 '/' => 'div',
64 '%' => 'mod',
65
66 '==' => 'eq',
67 '!=' => 'neq',
68 '<' => 'lt',
69 '>' => 'gt',
70
71 '<=' => 'lte',
72 '>=' => 'gte',
73 );
74
75 /**
76 * @var array
77 */
78 static protected $namespaces = array(
79 'special' => '%',
80 'mixin' => '@',
81 'function' => '^',
82 );
83
84 /**
85 * @var array
86 */
87 static protected $unitTable = array(
88 'in' => array(
89 'in' => 1,
90 'pt' => 72,
91 'pc' => 6,
92 'cm' => 2.54,
93 'mm' => 25.4,
94 'px' => 96,
95 'q' => 101.6,
96 ),
97 );
98
99 static public $true = array('keyword', 'true');
100 static public $false = array('keyword', 'false');
101 static public $null = array('null');
102 static public $defaultValue = array('keyword', '');
103 static public $selfSelector = array('self');
104 static public $emptyList = array('list', '', array());
105 static public $emptyMap = array('map', array(), array());
106 static public $emptyString = array('string', '"', array());
107
108 protected $importPaths = array('');
109 protected $importCache = array();
110 protected $userFunctions = array();
111 protected $registeredVars = array();
112
113 protected $numberPrecision = 5;
114 protected $lineNumberStyle = null;
115
116 protected $formatter = 'Leafo\ScssPhp\Formatter\Nested';
117
118 private $indentLevel;
119 private $commentsSeen;
120 private $extends;
121 private $extendsMap;
122 private $parsedFiles;
123 private $env;
124 private $scope;
125 private $parser;
126 private $sourcePos;
127 private $sourceParser;
128 private $storeEnv;
129 private $charsetSeen;
130 private $stderr;
131 private $shouldEvaluate;
132
133 /**
134 * Compile scss
135 *
136 * @api
137 *
138 * @param string $code
139 * @param string $path
140 *
141 * @return string
142 */
143 public function compile($code, $path = null)
144 {
145 $this->indentLevel = -1;
146 $this->commentsSeen = array();
147 $this->extends = array();
148 $this->extendsMap = array();
149 $this->parsedFiles = array();
150 $this->env = null;
151 $this->scope = null;
152
153 $this->stderr = fopen('php://stderr', 'w');
154
155 $locale = setlocale(LC_NUMERIC, 0);
156 setlocale(LC_NUMERIC, 'C');
157
158 $this->parser = new Parser($path);
159
160 $tree = $this->parser->parse($code);
161
162 $this->formatter = new $this->formatter();
163
164 $this->addParsedFile($path);
165
166 $this->rootEnv = $this->pushEnv($tree);
167 $this->injectVariables($this->registeredVars);
168 $this->compileRoot($tree);
169 $this->popEnv();
170
171 $out = $this->formatter->format($this->scope);
172
173 setlocale(LC_NUMERIC, $locale);
174
175 return $out;
176 }
177
178 /**
179 * Is self extend?
180 *
181 * @param array $target
182 * @param array $origin
183 *
184 * @return boolean
185 */
186 protected function isSelfExtend($target, $origin)
187 {
188 foreach ($origin as $sel) {
189 if (in_array($target, $sel)) {
190 return true;
191 }
192 }
193
194 return false;
195 }
196
197 /**
198 * Push extends
199 *
200 * @param array $target
201 * @param array $origin
202 */
203 protected function pushExtends($target, $origin)
204 {
205 if ($this->isSelfExtend($target, $origin)) {
206 return;
207 }
208
209 $i = count($this->extends);
210 $this->extends[] = array($target, $origin);
211
212 foreach ($target as $part) {
213 if (isset($this->extendsMap[$part])) {
214 $this->extendsMap[$part][] = $i;
215 } else {
216 $this->extendsMap[$part] = array($i);
217 }
218 }
219 }
220
221 /**
222 * Make output block
223 *
224 * @param string $type
225 * @param array $selectors
226 *
227 * @return \stdClass
228 */
229 protected function makeOutputBlock($type, $selectors = null)
230 {
231 $out = new \stdClass;
232 $out->type = $type;
233 $out->lines = array();
234 $out->children = array();
235 $out->parent = $this->scope;
236 $out->selectors = $selectors;
237 $out->depth = $this->env->depth;
238
239 return $out;
240 }
241
242 /**
243 * Compile root
244 *
245 * @param \stdClass $rootBlock
246 */
247 protected function compileRoot($rootBlock)
248 {
249 $this->scope = $this->makeOutputBlock('root');
250
251 $this->compileChildren($rootBlock->children, $this->scope);
252 $this->flattenSelectors($this->scope);
253 }
254
255 /**
256 * Flatten selectors
257 *
258 * @param \stdClass $block
259 * @parent string $parentKey
260 */
261 protected function flattenSelectors($block, $parentKey = null)
262 {
263 if ($block->selectors) {
264 $selectors = array();
265
266 foreach ($block->selectors as $s) {
267 $selectors[] = $s;
268
269 if (! is_array($s)) {
270 continue;
271 }
272
273 // check extends
274 if (! empty($this->extendsMap)) {
275 $this->matchExtends($s, $selectors);
276
277 // remove duplicates
278 array_walk($selectors, function (&$value) {
279 $value = json_encode($value);
280 });
281 $selectors = array_unique($selectors);
282 array_walk($selectors, function (&$value) {
283 $value = json_decode($value);
284 });
285 }
286 }
287
288 $block->selectors = array();
289 $placeholderSelector = false;
290
291 foreach ($selectors as $selector) {
292 if ($this->hasSelectorPlaceholder($selector)) {
293 $placeholderSelector = true;
294 continue;
295 }
296
297 $block->selectors[] = $this->compileSelector($selector);
298 }
299
300 if ($placeholderSelector && 0 === count($block->selectors) && null !== $parentKey) {
301 unset($block->parent->children[$parentKey]);
302
303 return;
304 }
305 }
306
307 foreach ($block->children as $key => $child) {
308 $this->flattenSelectors($child, $key);
309 }
310 }
311
312 /**
313 * Match extends
314 *
315 * @param array $selector
316 * @param array $out
317 * @param integer $from
318 * @param boolean $initial
319 */
320 protected function matchExtends($selector, &$out, $from = 0, $initial = true)
321 {
322 foreach ($selector as $i => $part) {
323 if ($i < $from) {
324 continue;
325 }
326
327 if ($this->matchExtendsSingle($part, $origin)) {
328 $before = array_slice($selector, 0, $i);
329 $after = array_slice($selector, $i + 1);
330 $s = count($before);
331
332 foreach ($origin as $new) {
333 $k = 0;
334
335 // remove shared parts
336 if ($initial) {
337 while ($k < $s && isset($new[$k]) && $before[$k] === $new[$k]) {
338 $k++;
339 }
340 }
341
342 $result = array_merge(
343 $before,
344 $k > 0 ? array_slice($new, $k) : $new,
345 $after
346 );
347
348 if ($result === $selector) {
349 continue;
350 }
351
352 $out[] = $result;
353
354 // recursively check for more matches
355 $this->matchExtends($result, $out, $i, false);
356
357 // selector sequence merging
358 if (! empty($before) && count($new) > 1) {
359 $result2 = array_merge(
360 array_slice($new, 0, -1),
361 $k > 0 ? array_slice($before, $k) : $before,
362 array_slice($new, -1),
363 $after
364 );
365
366 $out[] = $result2;
367 }
368 }
369 }
370 }
371 }
372
373 /**
374 * Match extends single
375 *
376 * @param array $rawSingle
377 * @param array $outOrigin
378 *
379 * @return boolean
380 */
381 protected function matchExtendsSingle($rawSingle, &$outOrigin)
382 {
383 $counts = array();
384 $single = array();
385
386 foreach ($rawSingle as $part) {
387 // matches Number
388 if (! is_string($part)) {
389 return false;
390 }
391
392 if (! preg_match('/^[\[.:#%]/', $part) && count($single)) {
393 $single[count($single) - 1] .= $part;
394 } else {
395 $single[] = $part;
396 }
397 }
398
399 foreach ($single as $part) {
400 if (isset($this->extendsMap[$part])) {
401 foreach ($this->extendsMap[$part] as $idx) {
402 $counts[$idx] = isset($counts[$idx]) ? $counts[$idx] + 1 : 1;
403 }
404 }
405 }
406
407 $outOrigin = array();
408 $found = false;
409
410 foreach ($counts as $idx => $count) {
411 list($target, $origin) = $this->extends[$idx];
412
413 // check count
414 if ($count !== count($target)) {
415 continue;
416 }
417
418 $rem = array_diff($single, $target);
419
420 foreach ($origin as $j => $new) {
421 // prevent infinite loop when target extends itself
422 if ($this->isSelfExtend($single, $origin)) {
423 return false;
424 }
425
426 $origin[$j][count($origin[$j]) - 1] = $this->combineSelectorSingle(end($new), $rem);
427 }
428
429 $outOrigin = array_merge($outOrigin, $origin);
430
431 $found = true;
432 }
433
434 return $found;
435 }
436
437 /**
438 * Combine selector single
439 *
440 * @param array $base
441 * @param array $other
442 *
443 * @return array
444 */
445 protected function combineSelectorSingle($base, $other)
446 {
447 $tag = null;
448 $out = array();
449
450 foreach (array($base, $other) as $single) {
451 foreach ($single as $part) {
452 if (preg_match('/^[^\[.#:]/', $part)) {
453 $tag = $part;
454 } else {
455 $out[] = $part;
456 }
457 }
458 }
459
460 if ($tag) {
461 array_unshift($out, $tag);
462 }
463
464 return $out;
465 }
466
467 /**
468 * Compile media
469 *
470 * @param \stdClass $media
471 */
472 protected function compileMedia($media)
473 {
474 $this->pushEnv($media);
475
476 $mediaQuery = $this->compileMediaQuery($this->multiplyMedia($this->env));
477
478 if (! empty($mediaQuery)) {
479 $this->scope = $this->makeOutputBlock('media', array($mediaQuery));
480
481 $parentScope = $this->mediaParent($this->scope);
482 $parentScope->children[] = $this->scope;
483
484 // top level properties in a media cause it to be wrapped
485 $needsWrap = false;
486
487 foreach ($media->children as $child) {
488 $type = $child[0];
489
490 if ($type !== 'block' && $type !== 'media' && $type !== 'directive' && $type !== 'import') {
491 $needsWrap = true;
492 break;
493 }
494 }
495
496 if ($needsWrap) {
497 $wrapped = (object) array(
498 'selectors' => array(),
499 'children' => $media->children,
500 );
501
502 $media->children = array(array('block', $wrapped));
503 }
504
505 $this->compileChildren($media->children, $this->scope);
506
507 $this->scope = $this->scope->parent;
508 }
509
510 $this->popEnv();
511 }
512
513 /**
514 * Media parent
515 *
516 * @param \stdClass $scope
517 *
518 * @return \stdClass
519 */
520 protected function mediaParent($scope)
521 {
522 while (! empty($scope->parent)) {
523 if (! empty($scope->type) && $scope->type !== 'media') {
524 break;
525 }
526
527 $scope = $scope->parent;
528 }
529
530 return $scope;
531 }
532
533 /**
534 * Compile at-root
535 *
536 * @param \stdClass $block
537 */
538 protected function compileAtRoot($block)
539 {
540 $env = $this->pushEnv($block);
541
542 $envs = $this->compactEnv($env);
543
544 if (isset($block->with)) {
545 // @todo move outside of nested directives, e.g., (without: all), (without: media supports), (with: rule)
546 } else {
547 // exclude selectors by default
548 $this->env->parent = $this->rootEnv;
549 }
550
551 $this->scope = $this->makeOutputBlock('at-root');
552 $this->scope->depth = 1;
553 $this->scope->parent->children[] = $this->scope;
554
555 // wrap inline selector
556 if ($block->selector) {
557 $wrapped = (object) array(
558 'parent' => $block,
559 'sourcePosition' => $block->sourcePosition,
560 'sourceParser' => $block->sourceParser,
561 'selectors' => $block->selector,
562 'comments' => array(),
563 'children' => $block->children,
564 );
565
566 $block->children = array(array('block', $wrapped));
567 }
568
569 $this->compileChildren($block->children, $this->scope);
570
571 $this->scope = $this->scope->parent;
572 $this->env = $this->extractEnv($envs);
573
574 $this->popEnv();
575 }
576
577 /**
578 * Compile keyframe block
579 *
580 * @param \stdClass $block
581 * @param array $selectors
582 */
583 protected function compileKeyframeBlock($block, $selectors)
584 {
585 $env = $this->pushEnv($block);
586
587 $envs = $this->compactEnv($env);
588
589 $this->env = $this->extractEnv(array_filter($envs, function ($e) {
590 return ! isset($e->block->selectors);
591 }));
592
593 $this->scope = $this->makeOutputBlock($block->type, $selectors);
594 $this->scope->depth = 1;
595 $this->scope->parent->children[] = $this->scope;
596
597 $this->compileChildren($block->children, $this->scope);
598
599 $this->scope = $this->scope->parent;
600 $this->env = $this->extractEnv($envs);
601
602 $this->popEnv();
603 }
604
605 /**
606 * Compile nested block
607 *
608 * @param \stdClass $block
609 * @param array $selectors
610 */
611 protected function compileNestedBlock($block, $selectors)
612 {
613 $this->pushEnv($block);
614
615 $this->scope = $this->makeOutputBlock($block->type, $selectors);
616 $this->scope->parent->children[] = $this->scope;
617
618 $this->compileChildren($block->children, $this->scope);
619
620 $this->scope = $this->scope->parent;
621
622 $this->popEnv();
623 }
624
625 /**
626 * Recursively compiles a block.
627 *
628 * A block is analogous to a CSS block in most cases. A single SCSS document
629 * is encapsulated in a block when parsed, but it does not have parent tags
630 * so all of its children appear on the root level when compiled.
631 *
632 * Blocks are made up of selectors and children.
633 *
634 * The children of a block are just all the blocks that are defined within.
635 *
636 * Compiling the block involves pushing a fresh environment on the stack,
637 * and iterating through the props, compiling each one.
638 *
639 * @see Compiler::compileChild()
640 *
641 * @param \stdClass $block
642 */
643 protected function compileBlock($block)
644 {
645 $env = $this->pushEnv($block);
646 $env->selectors = $this->evalSelectors($block->selectors);
647
648 $out = $this->makeOutputBlock(null);
649
650 if (isset($this->lineNumberStyle) && count($env->selectors) && count($block->children)) {
651 $annotation = $this->makeOutputBlock('comment');
652 $annotation->depth = 0;
653
654 $file = $block->sourceParser->getSourceName();
655 $line = $block->sourceParser->getLineNo($block->sourcePosition);
656
657 switch ($this->lineNumberStyle) {
658 case self::LINE_COMMENTS:
659 $annotation->lines[] = '/* line ' . $line . ', ' . $file . ' */';
660 break;
661
662 case self::DEBUG_INFO:
663 $annotation->lines[] = '@media -sass-debug-info{filename{font-family:"' . $file
664 . '"}line{font-family:' . $line . '}}';
665 break;
666 }
667
668 $this->scope->children[] = $annotation;
669 }
670
671 $this->scope->children[] = $out;
672
673 if (count($block->children)) {
674 $out->selectors = $this->multiplySelectors($env);
675
676 $this->compileChildren($block->children, $out);
677 }
678
679 $this->formatter->stripSemicolon($out->lines);
680
681 $this->popEnv();
682 }
683
684 /**
685 * Compile root level comment
686 *
687 * @param array $block
688 */
689 protected function compileComment($block)
690 {
691 $out = $this->makeOutputBlock('comment');
692 $out->lines[] = $block[1];
693 $this->scope->children[] = $out;
694 }
695
696 /**
697 * Evaluate selectors
698 *
699 * @param array $selectors
700 *
701 * @return array
702 */
703 protected function evalSelectors($selectors)
704 {
705 $this->shouldEvaluate = false;
706
707 $selectors = array_map(array($this, 'evalSelector'), $selectors);
708
709 // after evaluating interpolates, we might need a second pass
710 if ($this->shouldEvaluate) {
711 $buffer = $this->collapseSelectors($selectors);
712 $parser = new Parser(__METHOD__, false);
713
714 if ($parser->parseSelector($buffer, $newSelectors)) {
715 $selectors = array_map(array($this, 'evalSelector'), $newSelectors);
716 }
717 }
718
719 return $selectors;
720 }
721
722 /**
723 * Evaluate selector
724 *
725 * @param array $selector
726 *
727 * @return array
728 */
729 protected function evalSelector($selector)
730 {
731 return array_map(array($this, 'evalSelectorPart'), $selector);
732 }
733
734 /**
735 * Evaluate selector part; replaces all the interpolates, stripping quotes
736 *
737 * @param array $part
738 *
739 * @return array
740 */
741 protected function evalSelectorPart($part)
742 {
743 foreach ($part as &$p) {
744 if (is_array($p) && ($p[0] === 'interpolate' || $p[0] === 'string')) {
745 $p = $this->compileValue($p);
746
747 // force re-evaluation
748 if (strpos($p, '&') !== false || strpos($p, ',') !== false) {
749 $this->shouldEvaluate = true;
750 }
751 } elseif (is_string($p) && strlen($p) >= 2 &&
752 ($first = $p[0]) && ($first === '"' || $first === "'") &&
753 substr($p, -1) === $first
754 ) {
755 $p = substr($p, 1, -1);
756 }
757 }
758
759 return $this->flattenSelectorSingle($part);
760 }
761
762 /**
763 * Collapse selectors
764 *
765 * @param array $selectors
766 *
767 * @return string
768 */
769 protected function collapseSelectors($selectors)
770 {
771 $parts = array();
772
773 foreach ($selectors as $selector) {
774 $output = '';
775
776 array_walk_recursive(
777 $selector,
778 function ($value, $key) use (&$output) {
779 $output .= $value;
780 }
781 );
782
783 $parts[] = $output;
784 }
785
786 return implode(', ', $parts);
787 }
788
789 /**
790 * Flatten selector single; joins together .classes and #ids
791 *
792 * @param array $single
793 *
794 * @return array
795 */
796 protected function flattenSelectorSingle($single)
797 {
798 $joined = array();
799
800 foreach ($single as $part) {
801 if (empty($joined) ||
802 ! is_string($part) ||
803 preg_match('/[\[.:#%]/', $part)
804 ) {
805 $joined[] = $part;
806 continue;
807 }
808
809 if (is_array(end($joined))) {
810 $joined[] = $part;
811 } else {
812 $joined[count($joined) - 1] .= $part;
813 }
814 }
815
816 return $joined;
817 }
818
819 /**
820 * Compile selector to string; self(&) should have been replaced by now
821 *
822 * @param array $selector
823 *
824 * @return string
825 */
826 protected function compileSelector($selector)
827 {
828 if (! is_array($selector)) {
829 return $selector; // media and the like
830 }
831
832 return implode(
833 ' ',
834 array_map(
835 array($this, 'compileSelectorPart'),
836 $selector
837 )
838 );
839 }
840
841 /**
842 * Compile selector part
843 *
844 * @param arary $piece
845 *
846 * @return string
847 */
848 protected function compileSelectorPart($piece)
849 {
850 foreach ($piece as &$p) {
851 if (! is_array($p)) {
852 continue;
853 }
854
855 switch ($p[0]) {
856 case 'self':
857 $p = '&';
858 break;
859
860 default:
861 $p = $this->compileValue($p);
862 break;
863 }
864 }
865
866 return implode($piece);
867 }
868
869 /**
870 * Has selector placeholder?
871 *
872 * @param array $selector
873 *
874 * @return boolean
875 */
876 protected function hasSelectorPlaceholder($selector)
877 {
878 if (! is_array($selector)) {
879 return false;
880 }
881
882 foreach ($selector as $parts) {
883 foreach ($parts as $part) {
884 if ('%' === $part[0]) {
885 return true;
886 }
887 }
888 }
889
890 return false;
891 }
892
893 /**
894 * Compile children
895 *
896 * @param array $stms
897 * @param \stdClass $out
898 *
899 * @return array
900 */
901 protected function compileChildren($stms, $out)
902 {
903 foreach ($stms as $stm) {
904 $ret = $this->compileChild($stm, $out);
905
906 if (isset($ret)) {
907 return $ret;
908 }
909 }
910 }
911
912 /**
913 * Compile media query
914 *
915 * @param array $queryList
916 *
917 * @return string
918 */
919 protected function compileMediaQuery($queryList)
920 {
921 $out = '@media';
922 $first = true;
923
924 foreach ($queryList as $query) {
925 $type = null;
926 $parts = array();
927
928 foreach ($query as $q) {
929 switch ($q[0]) {
930 case 'mediaType':
931 if ($type) {
932 $type = $this->mergeMediaTypes(
933 $type,
934 array_map(array($this, 'compileValue'), array_slice($q, 1))
935 );
936
937 if (empty($type)) { // merge failed
938 return null;
939 }
940 } else {
941 $type = array_map(array($this, 'compileValue'), array_slice($q, 1));
942 }
943 break;
944
945 case 'mediaExp':
946 if (isset($q[2])) {
947 $parts[] = '('
948 . $this->compileValue($q[1])
949 . $this->formatter->assignSeparator
950 . $this->compileValue($q[2])
951 . ')';
952 } else {
953 $parts[] = '('
954 . $this->compileValue($q[1])
955 . ')';
956 }
957 break;
958 }
959 }
960
961 if ($type) {
962 array_unshift($parts, implode(' ', array_filter($type)));
963 }
964
965 if (! empty($parts)) {
966 if ($first) {
967 $first = false;
968 $out .= ' ';
969 } else {
970 $out .= $this->formatter->tagSeparator;
971 }
972
973 $out .= implode(' and ', $parts);
974 }
975 }
976
977 return $out;
978 }
979
980 /**
981 * Merge media types
982 *
983 * @param array $type1
984 * @param array $type2
985 *
986 * @return array|null
987 */
988 protected function mergeMediaTypes($type1, $type2)
989 {
990 if (empty($type1)) {
991 return $type2;
992 }
993
994 if (empty($type2)) {
995 return $type1;
996 }
997
998 $m1 = '';
999 $t1 = '';
1000
1001 if (count($type1) > 1) {
1002 $m1= strtolower($type1[0]);
1003 $t1= strtolower($type1[1]);
1004 } else {
1005 $t1 = strtolower($type1[0]);
1006 }
1007
1008 $m2 = '';
1009 $t2 = '';
1010
1011 if (count($type2) > 1) {
1012 $m2 = strtolower($type2[0]);
1013 $t2 = strtolower($type2[1]);
1014 } else {
1015 $t2 = strtolower($type2[0]);
1016 }
1017
1018 if (($m1 === 'not') ^ ($m2 === 'not')) {
1019 if ($t1 === $t2) {
1020 return null;
1021 }
1022
1023 return array(
1024 $m1 === 'not' ? $m2 : $m1,
1025 $m1 === 'not' ? $t2 : $t1,
1026 );
1027 }
1028
1029 if ($m1 === 'not' && $m2 === 'not') {
1030 // CSS has no way of representing "neither screen nor print"
1031 if ($t1 !== $t2) {
1032 return null;
1033 }
1034
1035 return array('not', $t1);
1036 }
1037
1038 if ($t1 !== $t2) {
1039 return null;
1040 }
1041
1042 // t1 == t2, neither m1 nor m2 are "not"
1043 return array(empty($m1)? $m2 : $m1, $t1);
1044 }
1045
1046 /**
1047 * Compile import; returns true if the value was something that could be imported
1048 *
1049 * @param array $rawPath
1050 * @param array $out
1051 *
1052 * @return boolean
1053 */
1054 protected function compileImport($rawPath, $out)
1055 {
1056 if ($rawPath[0] === 'string') {
1057 $path = $this->compileStringContent($rawPath);
1058
1059 if ($path = $this->findImport($path)) {
1060 $this->importFile($path, $out);
1061
1062 return true;
1063 }
1064
1065 return false;
1066 }
1067
1068 if ($rawPath[0] === 'list') {
1069 // handle a list of strings
1070 if (count($rawPath[2]) === 0) {
1071 return false;
1072 }
1073
1074 foreach ($rawPath[2] as $path) {
1075 if ($path[0] !== 'string') {
1076 return false;
1077 }
1078 }
1079
1080 foreach ($rawPath[2] as $path) {
1081 $this->compileImport($path, $out);
1082 }
1083
1084 return true;
1085 }
1086
1087 return false;
1088 }
1089
1090 /**
1091 * Compile child; returns a value to halt execution
1092 *
1093 * @param array $child
1094 * @param \stdClass $out
1095 *
1096 * @return array
1097 */
1098 protected function compileChild($child, $out)
1099 {
1100 $this->sourcePos = isset($child[Parser::SOURCE_POSITION]) ? $child[Parser::SOURCE_POSITION] : -1;
1101 $this->sourceParser = isset($child[Parser::SOURCE_PARSER]) ? $child[Parser::SOURCE_PARSER] : $this->parser;
1102
1103 switch ($child[0]) {
1104 case 'import':
1105 list(, $rawPath) = $child;
1106
1107 $rawPath = $this->reduce($rawPath);
1108
1109 if (! $this->compileImport($rawPath, $out)) {
1110 $out->lines[] = '@import ' . $this->compileValue($rawPath) . ';';
1111 }
1112 break;
1113
1114 case 'directive':
1115 list(, $directive) = $child;
1116
1117 $s = '@' . $directive->name;
1118
1119 if (! empty($directive->value)) {
1120 $s .= ' ' . $this->compileValue($directive->value);
1121 }
1122
1123 if ($directive->name === 'keyframes' || substr($directive->name, -10) === '-keyframes') {
1124 $this->compileKeyframeBlock($directive, array($s));
1125 } else {
1126 $this->compileNestedBlock($directive, array($s));
1127 }
1128 break;
1129
1130 case 'at-root':
1131 $this->compileAtRoot($child[1]);
1132 break;
1133
1134 case 'media':
1135 $this->compileMedia($child[1]);
1136 break;
1137
1138 case 'block':
1139 $this->compileBlock($child[1]);
1140 break;
1141
1142 case 'charset':
1143 if (! $this->charsetSeen) {
1144 $this->charsetSeen = true;
1145
1146 $out->lines[] = '@charset ' . $this->compileValue($child[1]) . ';';
1147 }
1148 break;
1149
1150 case 'assign':
1151 list(, $name, $value) = $child;
1152
1153 if ($name[0] === 'var') {
1154 $flag = isset($child[3]) ? $child[3] : null;
1155 $isDefault = $flag === '!default';
1156 $isGlobal = $flag === '!global';
1157
1158 if ($isGlobal) {
1159 $this->set($name[1], $this->reduce($value), false, $this->rootEnv);
1160 break;
1161 }
1162
1163 $shouldSet = $isDefault &&
1164 (($result = $this->get($name[1], false)) === null
1165 || $result === self::$null);
1166
1167 if (! $isDefault || $shouldSet) {
1168 $this->set($name[1], $this->reduce($value));
1169 }
1170 break;
1171 }
1172
1173 $compiledName = $this->compileValue($name);
1174
1175 // handle shorthand syntax: size / line-height
1176 if ($compiledName === 'font') {
1177 if ($value[0] === 'exp' && $value[1] === '/') {
1178 $value = $this->expToString($value);
1179 } elseif ($value[0] === 'list') {
1180 foreach ($value[2] as &$item) {
1181 if ($item[0] === 'exp' && $item[1] === '/') {
1182 $item = $this->expToString($item);
1183 }
1184 }
1185 }
1186 }
1187
1188 // if the value reduces to null from something else then
1189 // the property should be discarded
1190 if ($value[0] !== 'null') {
1191 $value = $this->reduce($value);
1192
1193 if ($value[0] === 'null') {
1194 break;
1195 }
1196 }
1197
1198 $compiledValue = $this->compileValue($value);
1199
1200 $out->lines[] = $this->formatter->property(
1201 $compiledName,
1202 $compiledValue
1203 );
1204 break;
1205
1206 case 'comment':
1207 if ($out->type === 'root') {
1208 $this->compileComment($child);
1209 break;
1210 }
1211
1212 $out->lines[] = $child[1];
1213 break;
1214
1215 case 'mixin':
1216 case 'function':
1217 list(, $block) = $child;
1218
1219 $this->set(self::$namespaces[$block->type] . $block->name, $block);
1220 break;
1221
1222 case 'extend':
1223 list(, $selectors) = $child;
1224
1225 foreach ($selectors as $sel) {
1226 $results = $this->evalSelectors(array($sel));
1227
1228 foreach ($results as $result) {
1229 // only use the first one
1230 $result = current($result);
1231
1232 $this->pushExtends($result, $out->selectors);
1233 }
1234 }
1235 break;
1236
1237 case 'if':
1238 list(, $if) = $child;
1239
1240 if ($this->isTruthy($this->reduce($if->cond, true))) {
1241 return $this->compileChildren($if->children, $out);
1242 }
1243
1244 foreach ($if->cases as $case) {
1245 if ($case->type === 'else' ||
1246 $case->type === 'elseif' && $this->isTruthy($this->reduce($case->cond))
1247 ) {
1248 return $this->compileChildren($case->children, $out);
1249 }
1250 }
1251 break;
1252
1253 case 'return':
1254 return $this->reduce($child[1], true);
1255
1256 case 'each':
1257 list(, $each) = $child;
1258
1259 $list = $this->coerceList($this->reduce($each->list));
1260
1261 $this->pushEnv();
1262
1263 foreach ($list[2] as $item) {
1264 if (count($each->vars) === 1) {
1265 $this->set($each->vars[0], $item, true);
1266 } else {
1267 list(,, $values) = $this->coerceList($item);
1268
1269 foreach ($each->vars as $i => $var) {
1270 $this->set($var, isset($values[$i]) ? $values[$i] : self::$null, true);
1271 }
1272 }
1273
1274 $ret = $this->compileChildren($each->children, $out);
1275
1276 if ($ret) {
1277 $this->popEnv();
1278
1279 return $ret;
1280 }
1281 }
1282
1283 $this->popEnv();
1284 break;
1285
1286 case 'while':
1287 list(, $while) = $child;
1288
1289 while ($this->isTruthy($this->reduce($while->cond, true))) {
1290 $ret = $this->compileChildren($while->children, $out);
1291
1292 if ($ret) {
1293 return $ret;
1294 }
1295 }
1296 break;
1297
1298 case 'for':
1299 list(, $for) = $child;
1300
1301 $start = $this->reduce($for->start, true);
1302 $start = $start[1];
1303 $end = $this->reduce($for->end, true);
1304 $end = $end[1];
1305 $d = $start < $end ? 1 : -1;
1306
1307 while (true) {
1308 if ((! $for->until && $start - $d == $end) ||
1309 ($for->until && $start == $end)
1310 ) {
1311 break;
1312 }
1313
1314 $this->set($for->var, array('number', $start, ''));
1315 $start += $d;
1316
1317 $ret = $this->compileChildren($for->children, $out);
1318
1319 if ($ret) {
1320 return $ret;
1321 }
1322 }
1323 break;
1324
1325 case 'nestedprop':
1326 list(, $prop) = $child;
1327
1328 $prefixed = array();
1329 $prefix = $this->compileValue($prop->prefix) . '-';
1330
1331 foreach ($prop->children as $child) {
1332 if ($child[0] === 'assign') {
1333 array_unshift($child[1][2], $prefix);
1334 }
1335
1336 if ($child[0] === 'nestedprop') {
1337 array_unshift($child[1]->prefix[2], $prefix);
1338 }
1339
1340 $prefixed[] = $child;
1341 }
1342
1343 $this->compileChildren($prefixed, $out);
1344 break;
1345
1346 case 'include':
1347 // including a mixin
1348 list(, $name, $argValues, $content) = $child;
1349
1350 $mixin = $this->get(self::$namespaces['mixin'] . $name, false);
1351
1352 if (! $mixin) {
1353 $this->throwError("Undefined mixin $name");
1354 }
1355
1356 $callingScope = $this->env;
1357
1358 // push scope, apply args
1359 $this->pushEnv();
1360 $this->env->depth--;
1361
1362 if (isset($content)) {
1363 $content->scope = $callingScope;
1364
1365 $this->setRaw(self::$namespaces['special'] . 'content', $content);
1366 }
1367
1368 if (isset($mixin->args)) {
1369 $this->applyArguments($mixin->args, $argValues);
1370 }
1371
1372 $this->env->marker = 'mixin';
1373
1374 foreach ($mixin->children as $child) {
1375 $this->compileChild($child, $out);
1376 }
1377
1378 $this->popEnv();
1379 break;
1380
1381 case 'mixin_content':
1382 $content = $this->get(self::$namespaces['special'] . 'content', false);
1383
1384 if (! $content) {
1385 $this->throwError('Expected @content inside of mixin');
1386 }
1387
1388 if (! isset($content->children)) {
1389 break;
1390 }
1391
1392 $this->storeEnv = $content->scope;
1393
1394 foreach ($content->children as $child) {
1395 $this->compileChild($child, $out);
1396 }
1397
1398 $this->storeEnv = null;
1399
1400 break;
1401
1402 case 'debug':
1403 list(, $value) = $child;
1404
1405 $line = $this->parser->getLineNo($this->sourcePos);
1406 $value = $this->compileValue($this->reduce($value, true));
1407 fwrite($this->stderr, "Line $line DEBUG: $value\n");
1408 break;
1409
1410 case 'warn':
1411 list(, $value) = $child;
1412
1413 $line = $this->parser->getLineNo($this->sourcePos);
1414 $value = $this->compileValue($this->reduce($value, true));
1415 echo "Line $line WARN: $value\n";
1416 break;
1417
1418 case 'error':
1419 list(, $value) = $child;
1420
1421 $line = $this->parser->getLineNo($this->sourcePos);
1422 $value = $this->compileValue($this->reduce($value, true));
1423 $this->throwError("Line $line ERROR: $value\n");
1424 break;
1425
1426 default:
1427 $this->throwError("unknown child type: $child[0]");
1428 }
1429 }
1430
1431 /**
1432 * Reduce expression to string
1433 *
1434 * @param array $exp
1435 *
1436 * @return array
1437 */
1438 protected function expToString($exp)
1439 {
1440 list(, $op, $left, $right, $inParens, $whiteLeft, $whiteRight) = $exp;
1441
1442 $content = array($this->reduce($left));
1443
1444 if ($whiteLeft) {
1445 $content[] = ' ';
1446 }
1447
1448 $content[] = $op;
1449
1450 if ($whiteRight) {
1451 $content[] = ' ';
1452 }
1453
1454 $content[] = $this->reduce($right);
1455
1456 return array('string', '', $content);
1457 }
1458
1459 /**
1460 * Is truthy?
1461 *
1462 * @param array $value
1463 *
1464 * @return array
1465 */
1466 protected function isTruthy($value)
1467 {
1468 return $value !== self::$false && $value !== self::$null;
1469 }
1470
1471 /**
1472 * Should $value cause its operand to eval
1473 *
1474 * @param array $value
1475 *
1476 * @return boolean
1477 */
1478 protected function shouldEval($value)
1479 {
1480 switch ($value[0]) {
1481 case 'exp':
1482 if ($value[1] === '/') {
1483 return $this->shouldEval($value[2], $value[3]);
1484 }
1485
1486 // fall-thru
1487 case 'var':
1488 case 'fncall':
1489 return true;
1490 }
1491
1492 return false;
1493 }
1494
1495 /**
1496 * Reduce value
1497 *
1498 * @param array $value
1499 * @param boolean $inExp
1500 *
1501 * @return array
1502 */
1503 protected function reduce($value, $inExp = false)
1504 {
1505 list($type) = $value;
1506
1507 switch ($type) {
1508 case 'exp':
1509 list(, $op, $left, $right, $inParens) = $value;
1510
1511 $opName = isset(self::$operatorNames[$op]) ? self::$operatorNames[$op] : $op;
1512 $inExp = $inExp || $this->shouldEval($left) || $this->shouldEval($right);
1513
1514 $left = $this->reduce($left, true);
1515
1516 if ($op !== 'and' && $op !== 'or') {
1517 $right = $this->reduce($right, true);
1518 }
1519
1520 // special case: looks like css short-hand
1521 if ($opName === 'div' && ! $inParens && ! $inExp && isset($right[2]) && $right[2] !== '') {
1522 return $this->expToString($value);
1523 }
1524
1525 $left = $this->coerceForExpression($left);
1526 $right = $this->coerceForExpression($right);
1527
1528 $ltype = $left[0];
1529 $rtype = $right[0];
1530
1531 $ucOpName = ucfirst($opName);
1532 $ucLType = ucfirst($ltype);
1533 $ucRType = ucfirst($rtype);
1534
1535 // this tries:
1536 // 1. op[op name][left type][right type]
1537 // 2. op[left type][right type] (passing the op as first arg
1538 // 3. op[op name]
1539 $fn = "op${ucOpName}${ucLType}${ucRType}";
1540
1541 if (is_callable(array($this, $fn)) ||
1542 (($fn = "op${ucLType}${ucRType}") &&
1543 is_callable(array($this, $fn)) &&
1544 $passOp = true) ||
1545 (($fn = "op${ucOpName}") &&
1546 is_callable(array($this, $fn)) &&
1547 $genOp = true)
1548 ) {
1549 $unitChange = false;
1550
1551 if (! isset($genOp) &&
1552 $left[0] === 'number' && $right[0] === 'number'
1553 ) {
1554 if ($opName === 'mod' && $right[2] !== '') {
1555 $this->throwError("Cannot modulo by a number with units: $right[1]$right[2].");
1556 }
1557
1558 $unitChange = true;
1559 $emptyUnit = $left[2] === '' || $right[2] === '';
1560 $targetUnit = '' !== $left[2] ? $left[2] : $right[2];
1561
1562 if ($opName !== 'mul') {
1563 $left[2] = '' !== $left[2] ? $left[2] : $targetUnit;
1564 $right[2] = '' !== $right[2] ? $right[2] : $targetUnit;
1565 }
1566
1567 if ($opName !== 'mod') {
1568 $left = $this->normalizeNumber($left);
1569 $right = $this->normalizeNumber($right);
1570 }
1571
1572 if ($opName === 'div' && ! $emptyUnit && $left[2] === $right[2]) {
1573 $targetUnit = '';
1574 }
1575
1576 if ($opName === 'mul') {
1577 $left[2] = '' !== $left[2] ? $left[2] : $right[2];
1578 $right[2] = '' !== $right[2] ? $right[2] : $left[2];
1579 } elseif ($opName === 'div' && $left[2] === $right[2]) {
1580 $left[2] = '';
1581 $right[2] = '';
1582 }
1583 }
1584
1585 $shouldEval = $inParens || $inExp;
1586
1587 if (isset($passOp)) {
1588 $out = $this->$fn($op, $left, $right, $shouldEval);
1589 } else {
1590 $out = $this->$fn($left, $right, $shouldEval);
1591 }
1592
1593 if (isset($out)) {
1594 if ($unitChange && $out[0] === 'number') {
1595 $out = $this->coerceUnit($out, $targetUnit);
1596 }
1597
1598 return $out;
1599 }
1600 }
1601
1602 return $this->expToString($value);
1603
1604 case 'unary':
1605 list(, $op, $exp, $inParens) = $value;
1606
1607 $inExp = $inExp || $this->shouldEval($exp);
1608 $exp = $this->reduce($exp);
1609
1610 if ($exp[0] === 'number') {
1611 switch ($op) {
1612 case '+':
1613 return $exp;
1614
1615 case '-':
1616 $exp[1] *= -1;
1617
1618 return $exp;
1619 }
1620 }
1621
1622 if ($op === 'not') {
1623 if ($inExp || $inParens) {
1624 if ($exp === self::$false) {
1625 return self::$true;
1626 }
1627
1628 return self::$false;
1629 }
1630
1631 $op = $op . ' ';
1632 }
1633
1634 return array('string', '', array($op, $exp));
1635
1636 case 'var':
1637 list(, $name) = $value;
1638
1639 return $this->reduce($this->get($name));
1640
1641 case 'list':
1642 foreach ($value[2] as &$item) {
1643 $item = $this->reduce($item);
1644 }
1645
1646 return $value;
1647
1648 case 'map':
1649 foreach ($value[1] as &$item) {
1650 $item = $this->reduce($item);
1651 }
1652
1653 foreach ($value[2] as &$item) {
1654 $item = $this->reduce($item);
1655 }
1656
1657 return $value;
1658
1659 case 'string':
1660 foreach ($value[2] as &$item) {
1661 if (is_array($item)) {
1662 $item = $this->reduce($item);
1663 }
1664 }
1665
1666 return $value;
1667
1668 case 'interpolate':
1669 $value[1] = $this->reduce($value[1]);
1670
1671 return $value;
1672
1673 case 'fncall':
1674 list(, $name, $argValues) = $value;
1675
1676 // user defined function?
1677 $func = $this->get(self::$namespaces['function'] . $name, false);
1678
1679 if ($func) {
1680 $this->pushEnv();
1681
1682 // set the args
1683 if (isset($func->args)) {
1684 $this->applyArguments($func->args, $argValues);
1685 }
1686
1687 // throw away lines and children
1688 $tmp = (object) array(
1689 'lines' => array(),
1690 'children' => array(),
1691 );
1692
1693 $ret = $this->compileChildren($func->children, $tmp);
1694
1695 $this->popEnv();
1696
1697 return ! isset($ret) ? self::$defaultValue : $ret;
1698 }
1699
1700 // built in function
1701 if ($this->callBuiltin($name, $argValues, $returnValue)) {
1702 return $returnValue;
1703 }
1704
1705 // need to flatten the arguments into a list
1706 $listArgs = array();
1707
1708 foreach ((array)$argValues as $arg) {
1709 if (empty($arg[0])) {
1710 $listArgs[] = $this->reduce($arg[1]);
1711 }
1712 }
1713
1714 return array('function', $name, array('list', ',', $listArgs));
1715
1716 default:
1717 return $value;
1718 }
1719 }
1720
1721 /**
1722 * Normalize name
1723 *
1724 * @param string $name
1725 *
1726 * @return string
1727 */
1728 protected function normalizeName($name)
1729 {
1730 return str_replace('-', '_', $name);
1731 }
1732
1733 /**
1734 * Normalize value
1735 *
1736 * @param array $value
1737 *
1738 * @return array
1739 */
1740 public function normalizeValue($value)
1741 {
1742 $value = $this->coerceForExpression($this->reduce($value));
1743 list($type) = $value;
1744
1745 switch ($type) {
1746 case 'list':
1747 $value = $this->extractInterpolation($value);
1748
1749 if ($value[0] !== 'list') {
1750 return array('keyword', $this->compileValue($value));
1751 }
1752
1753 foreach ($value[2] as $key => $item) {
1754 $value[2][$key] = $this->normalizeValue($item);
1755 }
1756
1757 return $value;
1758
1759 case 'string':
1760 return array($type, '"', $this->compileStringContent($value));
1761
1762 case 'number':
1763 return $this->normalizeNumber($value);
1764
1765 default:
1766 return $value;
1767 }
1768 }
1769
1770 /**
1771 * Normalize number; just does physical lengths for now
1772 *
1773 * @param array $number
1774 *
1775 * @return array
1776 */
1777 protected function normalizeNumber($number)
1778 {
1779 list(, $value, $unit) = $number;
1780
1781 if (isset(self::$unitTable['in'][$unit])) {
1782 $conv = self::$unitTable['in'][$unit];
1783
1784 return array('number', $value / $conv, 'in');
1785 }
1786
1787 return $number;
1788 }
1789
1790 /**
1791 * Add numbers
1792 *
1793 * @param array $left
1794 * @param array $right
1795 *
1796 * @return array
1797 */
1798 protected function opAddNumberNumber($left, $right)
1799 {
1800 return array('number', $left[1] + $right[1], $left[2]);
1801 }
1802
1803 /**
1804 * Multiply numbers
1805 *
1806 * @param array $left
1807 * @param array $right
1808 *
1809 * @return array
1810 */
1811 protected function opMulNumberNumber($left, $right)
1812 {
1813 return array('number', $left[1] * $right[1], $left[2]);
1814 }
1815
1816 /**
1817 * Subtract numbers
1818 *
1819 * @param array $left
1820 * @param array $right
1821 *
1822 * @return array
1823 */
1824 protected function opSubNumberNumber($left, $right)
1825 {
1826 return array('number', $left[1] - $right[1], $left[2]);
1827 }
1828
1829 /**
1830 * Divide numbers
1831 *
1832 * @param array $left
1833 * @param array $right
1834 *
1835 * @return array
1836 */
1837 protected function opDivNumberNumber($left, $right)
1838 {
1839 if ($right[1] == 0) {
1840 $this->throwError('Division by zero');
1841 }
1842
1843 return array('number', $left[1] / $right[1], $left[2]);
1844 }
1845
1846 /**
1847 * Mod numbers
1848 *
1849 * @param array $left
1850 * @param array $right
1851 *
1852 * @return array
1853 */
1854 protected function opModNumberNumber($left, $right)
1855 {
1856 return array('number', $left[1] % $right[1], $left[2]);
1857 }
1858
1859 /**
1860 * Add strings
1861 *
1862 * @param array $left
1863 * @param array $right
1864 *
1865 * @return array
1866 */
1867 protected function opAdd($left, $right)
1868 {
1869 if ($strLeft = $this->coerceString($left)) {
1870 if ($right[0] === 'string') {
1871 $right[1] = '';
1872 }
1873
1874 $strLeft[2][] = $right;
1875
1876 return $strLeft;
1877 }
1878
1879 if ($strRight = $this->coerceString($right)) {
1880 if ($left[0] === 'string') {
1881 $left[1] = '';
1882 }
1883
1884 array_unshift($strRight[2], $left);
1885
1886 return $strRight;
1887 }
1888 }
1889
1890 /**
1891 * Boolean and
1892 *
1893 * @param array $left
1894 * @param array $right
1895 * @param boolean $shouldEval
1896 *
1897 * @return array
1898 */
1899 protected function opAnd($left, $right, $shouldEval)
1900 {
1901 if (! $shouldEval) {
1902 return;
1903 }
1904
1905 if ($left !== self::$false) {
1906 return $this->reduce($right, true);
1907 }
1908
1909 return $left;
1910 }
1911
1912 /**
1913 * Boolean or
1914 *
1915 * @param array $left
1916 * @param array $right
1917 * @param boolean $shouldEval
1918 *
1919 * @return array
1920 */
1921 protected function opOr($left, $right, $shouldEval)
1922 {
1923 if (! $shouldEval) {
1924 return;
1925 }
1926
1927 if ($left !== self::$false) {
1928 return $left;
1929 }
1930
1931 return $this->reduce($right, true);
1932 }
1933
1934 /**
1935 * Compare colors
1936 *
1937 * @param string $op
1938 * @param array $left
1939 * @param array $right
1940 *
1941 * @return array
1942 */
1943 protected function opColorColor($op, $left, $right)
1944 {
1945 $out = array('color');
1946
1947 foreach (range(1, 3) as $i) {
1948 $lval = isset($left[$i]) ? $left[$i] : 0;
1949 $rval = isset($right[$i]) ? $right[$i] : 0;
1950
1951 switch ($op) {
1952 case '+':
1953 $out[] = $lval + $rval;
1954 break;
1955
1956 case '-':
1957 $out[] = $lval - $rval;
1958 break;
1959
1960 case '*':
1961 $out[] = $lval * $rval;
1962 break;
1963
1964 case '%':
1965 $out[] = $lval % $rval;
1966 break;
1967
1968 case '/':
1969 if ($rval == 0) {
1970 $this->throwError("color: Can't divide by zero");
1971 }
1972
1973 $out[] = (int) ($lval / $rval);
1974 break;
1975
1976 case '==':
1977 return $this->opEq($left, $right);
1978
1979 case '!=':
1980 return $this->opNeq($left, $right);
1981
1982 default:
1983 $this->throwError("color: unknown op $op");
1984 }
1985 }
1986
1987 if (isset($left[4])) {
1988 $out[4] = $left[4];
1989 } elseif (isset($right[4])) {
1990 $out[4] = $right[4];
1991 }
1992
1993 return $this->fixColor($out);
1994 }
1995
1996 /**
1997 * Compare color and number
1998 *
1999 * @param string $op
2000 * @param array $left
2001 * @param array $right
2002 *
2003 * @return array
2004 */
2005 protected function opColorNumber($op, $left, $right)
2006 {
2007 $value = $right[1];
2008
2009 return $this->opColorColor(
2010 $op,
2011 $left,
2012 array('color', $value, $value, $value)
2013 );
2014 }
2015
2016 /**
2017 * Compare number and color
2018 *
2019 * @param string $op
2020 * @param array $left
2021 * @param array $right
2022 *
2023 * @return array
2024 */
2025 protected function opNumberColor($op, $left, $right)
2026 {
2027 $value = $left[1];
2028
2029 return $this->opColorColor(
2030 $op,
2031 array('color', $value, $value, $value),
2032 $right
2033 );
2034 }
2035
2036 /**
2037 * Compare number1 == number2
2038 *
2039 * @param array $left
2040 * @param array $right
2041 *
2042 * @return array
2043 */
2044 protected function opEq($left, $right)
2045 {
2046 if (($lStr = $this->coerceString($left)) && ($rStr = $this->coerceString($right))) {
2047 $lStr[1] = '';
2048 $rStr[1] = '';
2049
2050 $left = $this->compileValue($lStr);
2051 $right = $this->compileValue($rStr);
2052 }
2053
2054 return $this->toBool($left === $right);
2055 }
2056
2057 /**
2058 * Compare number1 != number2
2059 *
2060 * @param array $left
2061 * @param array $right
2062 *
2063 * @return array
2064 */
2065 protected function opNeq($left, $right)
2066 {
2067 if (($lStr = $this->coerceString($left)) && ($rStr = $this->coerceString($right))) {
2068 $lStr[1] = '';
2069 $rStr[1] = '';
2070
2071 $left = $this->compileValue($lStr);
2072 $right = $this->compileValue($rStr);
2073 }
2074
2075 return $this->toBool($left !== $right);
2076 }
2077
2078 /**
2079 * Compare number1 >= number2
2080 *
2081 * @param array $left
2082 * @param array $right
2083 *
2084 * @return array
2085 */
2086 protected function opGteNumberNumber($left, $right)
2087 {
2088 return $this->toBool($left[1] >= $right[1]);
2089 }
2090
2091 /**
2092 * Compare number1 > number2
2093 *
2094 * @param array $left
2095 * @param array $right
2096 *
2097 * @return array
2098 */
2099 protected function opGtNumberNumber($left, $right)
2100 {
2101 return $this->toBool($left[1] > $right[1]);
2102 }
2103
2104 /**
2105 * Compare number1 <= number2
2106 *
2107 * @param array $left
2108 * @param array $right
2109 *
2110 * @return array
2111 */
2112 protected function opLteNumberNumber($left, $right)
2113 {
2114 return $this->toBool($left[1] <= $right[1]);
2115 }
2116
2117 /**
2118 * Compare number1 < number2
2119 *
2120 * @param array $left
2121 * @param array $right
2122 *
2123 * @return array
2124 */
2125 protected function opLtNumberNumber($left, $right)
2126 {
2127 return $this->toBool($left[1] < $right[1]);
2128 }
2129
2130 /**
2131 * Cast to boolean
2132 *
2133 * @api
2134 *
2135 * @param mixed $thing
2136 *
2137 * @return array
2138 */
2139 public function toBool($thing)
2140 {
2141 return $thing ? self::$true : self::$false;
2142 }
2143
2144 /**
2145 * Compiles a primitive value into a CSS property value.
2146 *
2147 * Values in scssphp are typed by being wrapped in arrays, their format is
2148 * typically:
2149 *
2150 * array(type, contents [, additional_contents]*)
2151 *
2152 * The input is expected to be reduced. This function will not work on
2153 * things like expressions and variables.
2154 *
2155 * @api
2156 *
2157 * @param array $value
2158 *
2159 * @return string
2160 */
2161 public function compileValue($value)
2162 {
2163 $value = $this->reduce($value);
2164
2165 list($type) = $value;
2166
2167 switch ($type) {
2168 case 'keyword':
2169 return $value[1];
2170
2171 case 'color':
2172 // [1] - red component (either number for a %)
2173 // [2] - green component
2174 // [3] - blue component
2175 // [4] - optional alpha component
2176 list(, $r, $g, $b) = $value;
2177
2178 $r = round($r);
2179 $g = round($g);
2180 $b = round($b);
2181
2182 if (count($value) === 5 && $value[4] !== 1) { // rgba
2183 return 'rgba(' . $r . ', ' . $g . ', ' . $b . ', ' . $value[4] . ')';
2184 }
2185
2186 $h = sprintf('#%02x%02x%02x', $r, $g, $b);
2187
2188 // Converting hex color to short notation (e.g. #003399 to #039)
2189 if ($h[1] === $h[2] && $h[3] === $h[4] && $h[5] === $h[6]) {
2190 $h = '#' . $h[1] . $h[3] . $h[5];
2191 }
2192
2193 return $h;
2194
2195 case 'number':
2196 return round($value[1], $this->numberPrecision) . $value[2];
2197
2198 case 'string':
2199 return $value[1] . $this->compileStringContent($value) . $value[1];
2200
2201 case 'function':
2202 $args = ! empty($value[2]) ? $this->compileValue($value[2]) : '';
2203
2204 return "$value[1]($args)";
2205
2206 case 'list':
2207 $value = $this->extractInterpolation($value);
2208
2209 if ($value[0] !== 'list') {
2210 return $this->compileValue($value);
2211 }
2212
2213 list(, $delim, $items) = $value;
2214
2215 $filtered = array();
2216
2217 foreach ($items as $item) {
2218 if ($item[0] === 'null') {
2219 continue;
2220 }
2221
2222 $filtered[] = $this->compileValue($item);
2223 }
2224
2225 return implode("$delim ", $filtered);
2226
2227 case 'map':
2228 $keys = $value[1];
2229 $values = $value[2];
2230 $filtered = array();
2231
2232 for ($i = 0, $s = count($keys); $i < $s; $i++) {
2233 $filtered[$this->compileValue($keys[$i])] = $this->compileValue($values[$i]);
2234 }
2235
2236 array_walk($filtered, function (&$value, $key) {
2237 $value = $key . ': ' . $value;
2238 });
2239
2240 return '(' . implode(', ', $filtered) . ')';
2241
2242 case 'interpolated':
2243 // node created by extractInterpolation
2244 list(, $interpolate, $left, $right) = $value;
2245 list(,, $whiteLeft, $whiteRight) = $interpolate;
2246
2247 $left = count($left[2]) > 0 ?
2248 $this->compileValue($left) . $whiteLeft : '';
2249
2250 $right = count($right[2]) > 0 ?
2251 $whiteRight . $this->compileValue($right) : '';
2252
2253 return $left . $this->compileValue($interpolate) . $right;
2254
2255 case 'interpolate':
2256 // raw parse node
2257 list(, $exp) = $value;
2258
2259 // strip quotes if it's a string
2260 $reduced = $this->reduce($exp);
2261
2262 switch ($reduced[0]) {
2263 case 'string':
2264 $reduced = array('keyword', $this->compileStringContent($reduced));
2265 break;
2266
2267 case 'null':
2268 $reduced = array('keyword', '');
2269 }
2270
2271 return $this->compileValue($reduced);
2272
2273 case 'null':
2274 return 'null';
2275
2276 default:
2277 $this->throwError("unknown value type: $type");
2278 }
2279 }
2280
2281 /**
2282 * Flatten list
2283 *
2284 * @param array $list
2285 *
2286 * @return string
2287 */
2288 protected function flattenList($list)
2289 {
2290 return $this->compileValue($list);
2291 }
2292
2293 /**
2294 * Compile string content
2295 *
2296 * @param array $string
2297 *
2298 * @return string
2299 */
2300 protected function compileStringContent($string)
2301 {
2302 $parts = array();
2303
2304 foreach ($string[2] as $part) {
2305 if (is_array($part)) {
2306 $parts[] = $this->compileValue($part);
2307 } else {
2308 $parts[] = $part;
2309 }
2310 }
2311
2312 return implode($parts);
2313 }
2314
2315 /**
2316 * Extract interpolation; it doesn't need to be recursive, compileValue will handle that
2317 *
2318 * @param array $list
2319 *
2320 * @return array
2321 */
2322 protected function extractInterpolation($list)
2323 {
2324 $items = $list[2];
2325
2326 foreach ($items as $i => $item) {
2327 if ($item[0] === 'interpolate') {
2328 $before = array('list', $list[1], array_slice($items, 0, $i));
2329 $after = array('list', $list[1], array_slice($items, $i + 1));
2330
2331 return array('interpolated', $item, $before, $after);
2332 }
2333 }
2334
2335 return $list;
2336 }
2337
2338 /**
2339 * Find the final set of selectors
2340 *
2341 * @param \stdClass $env
2342 *
2343 * @return array
2344 */
2345 protected function multiplySelectors($env)
2346 {
2347 $envs = $this->compactEnv($env);
2348 $selectors = array();
2349 $parentSelectors = array(array());
2350
2351 while ($env = array_pop($envs)) {
2352 if (empty($env->selectors)) {
2353 continue;
2354 }
2355
2356 $selectors = array();
2357
2358 foreach ($env->selectors as $selector) {
2359 foreach ($parentSelectors as $parent) {
2360 $selectors[] = $this->joinSelectors($parent, $selector);
2361 }
2362 }
2363
2364 $parentSelectors = $selectors;
2365 }
2366
2367 return $selectors;
2368 }
2369
2370 /**
2371 * Join selectors; looks for & to replace, or append parent before child
2372 *
2373 * @param array $parent
2374 * @param array $child
2375 *
2376 * @return array
2377 */
2378 protected function joinSelectors($parent, $child)
2379 {
2380 $setSelf = false;
2381 $out = array();
2382
2383 foreach ($child as $part) {
2384 $newPart = array();
2385
2386 foreach ($part as $p) {
2387 if ($p === self::$selfSelector) {
2388 $setSelf = true;
2389
2390 foreach ($parent as $i => $parentPart) {
2391 if ($i > 0) {
2392 $out[] = $newPart;
2393 $newPart = array();
2394 }
2395
2396 foreach ($parentPart as $pp) {
2397 $newPart[] = $pp;
2398 }
2399 }
2400 } else {
2401 $newPart[] = $p;
2402 }
2403 }
2404
2405 $out[] = $newPart;
2406 }
2407
2408 return $setSelf ? $out : array_merge($parent, $child);
2409 }
2410
2411 /**
2412 * Multiply media
2413 *
2414 * @param \stdClass $env
2415 * @param array $childQueries
2416 *
2417 * @return array
2418 */
2419 protected function multiplyMedia($env, $childQueries = null)
2420 {
2421 if (! isset($env) ||
2422 ! empty($env->block->type) && $env->block->type !== 'media'
2423 ) {
2424 return $childQueries;
2425 }
2426
2427 // plain old block, skip
2428 if (empty($env->block->type)) {
2429 return $this->multiplyMedia($env->parent, $childQueries);
2430 }
2431
2432 $parentQueries = $env->block->queryList;
2433 if ($childQueries === null) {
2434 $childQueries = $parentQueries;
2435 } else {
2436 $originalQueries = $childQueries;
2437 $childQueries = array();
2438
2439 foreach ($parentQueries as $parentQuery) {
2440 foreach ($originalQueries as $childQuery) {
2441 $childQueries []= array_merge($parentQuery, $childQuery);
2442 }
2443 }
2444 }
2445
2446 return $this->multiplyMedia($env->parent, $childQueries);
2447 }
2448
2449 /**
2450 * Convert env linked list to stack
2451 *
2452 * @param \stdClass $env
2453 *
2454 * @return array
2455 */
2456 private function compactEnv($env)
2457 {
2458 for ($envs = array(); $env; $env = $env->parent) {
2459 $envs[] = $env;
2460 }
2461
2462 return $envs;
2463 }
2464
2465 /**
2466 * Convert env stack to singly linked list
2467 *
2468 * @param array $envs
2469 *
2470 * @return \stdClass
2471 */
2472 private function extractEnv($envs)
2473 {
2474 for ($env = null; $e = array_pop($envs);) {
2475 $e->parent = $env;
2476 $env = $e;
2477 }
2478
2479 return $env;
2480 }
2481
2482 /**
2483 * Push environment
2484 *
2485 * @param \stdClass $block
2486 *
2487 * @return \stdClass
2488 */
2489 protected function pushEnv($block = null)
2490 {
2491 $env = new \stdClass;
2492 $env->parent = $this->env;
2493 $env->store = array();
2494 $env->block = $block;
2495 $env->depth = isset($this->env->depth) ? $this->env->depth + 1 : 0;
2496
2497 $this->env = $env;
2498
2499 return $env;
2500 }
2501
2502 /**
2503 * Pop environment
2504 */
2505 protected function popEnv()
2506 {
2507 $env = $this->env;
2508 $this->env = $this->env->parent;
2509
2510 return $env;
2511 }
2512
2513 /**
2514 * Get store environment
2515 *
2516 * @return \stdClass
2517 */
2518 protected function getStoreEnv()
2519 {
2520 return isset($this->storeEnv) ? $this->storeEnv : $this->env;
2521 }
2522
2523 /**
2524 * Set variable
2525 *
2526 * @param string $name
2527 * @param mixed $value
2528 * @param boolean $shadow
2529 * @param \stdClass $env
2530 */
2531 protected function set($name, $value, $shadow = false, $env = null)
2532 {
2533 $name = $this->normalizeName($name);
2534
2535 if ($shadow) {
2536 $this->setRaw($name, $value, $env);
2537 } else {
2538 $this->setExisting($name, $value, $env);
2539 }
2540 }
2541
2542 /**
2543 * Set existing variable
2544 *
2545 * @param string $name
2546 * @param mixed $value
2547 * @param \stdClass $env
2548 */
2549 protected function setExisting($name, $value, $env = null)
2550 {
2551 if (! isset($env)) {
2552 $env = $this->getStoreEnv();
2553 }
2554
2555 $storeEnv = $env;
2556
2557 $hasNamespace = $name[0] === '^' || $name[0] === '@' || $name[0] === '%';
2558
2559 for (;;) {
2560 if (array_key_exists($name, $env->store)) {
2561 break;
2562 }
2563
2564 if (! $hasNamespace && isset($env->marker)) {
2565 $env = $storeEnv;
2566 break;
2567 }
2568
2569 if (! isset($env->parent)) {
2570 $env = $storeEnv;
2571 break;
2572 }
2573
2574 $env = $env->parent;
2575 }
2576
2577 $env->store[$name] = $value;
2578 }
2579
2580 /**
2581 * Set raw variable
2582 *
2583 * @param string $name
2584 * @param mixed $value
2585 * @param \stdClass $env
2586 */
2587 protected function setRaw($name, $value, $env = null)
2588 {
2589 if (! isset($env)) {
2590 $env = $this->getStoreEnv();
2591 }
2592
2593 $env->store[$name] = $value;
2594 }
2595
2596 /**
2597 * Get variable
2598 *
2599 * @api
2600 *
2601 * @param string $name
2602 * @param boolean $shouldThrow
2603 * @param \stdClass $env
2604 *
2605 * @return mixed
2606 */
2607 public function get($name, $shouldThrow = true, $env = null)
2608 {
2609 $name = $this->normalizeName($name);
2610
2611 if (! isset($env)) {
2612 $env = $this->getStoreEnv();
2613 }
2614
2615 $hasNamespace = $name[0] === '^' || $name[0] === '@' || $name[0] === '%';
2616
2617 for (;;) {
2618 if (array_key_exists($name, $env->store)) {
2619 return $env->store[$name];
2620 }
2621
2622 if (! $hasNamespace && isset($env->marker)) {
2623 $env = $this->rootEnv;
2624 continue;
2625 }
2626
2627 if (! isset($env->parent)) {
2628 break;
2629 }
2630
2631 $env = $env->parent;
2632 }
2633
2634 if ($shouldThrow) {
2635 $this->throwError("Undefined variable \$$name");
2636 }
2637
2638 // found nothing
2639 }
2640
2641 /**
2642 * Has variable?
2643 *
2644 * @param string $name
2645 * @param \stdClass $env
2646 *
2647 * @return boolean
2648 */
2649 protected function has($name, $env = null)
2650 {
2651 return $this->get($name, false, $env) !== null;
2652 }
2653
2654 /**
2655 * Inject variables
2656 *
2657 * @param array $args
2658 */
2659 protected function injectVariables(array $args)
2660 {
2661 if (empty($args)) {
2662 return;
2663 }
2664
2665 $parser = new Parser(__METHOD__, false);
2666
2667 foreach ($args as $name => $strValue) {
2668 if ($name[0] === '$') {
2669 $name = substr($name, 1);
2670 }
2671
2672 if (! $parser->parseValue($strValue, $value)) {
2673 $value = $this->coerceValue($strValue);
2674 }
2675
2676 $this->set($name, $value);
2677 }
2678 }
2679
2680 /**
2681 * Set variables
2682 *
2683 * @api
2684 *
2685 * @param array $variables
2686 */
2687 public function setVariables(array $variables)
2688 {
2689 $this->registeredVars = array_merge($this->registeredVars, $variables);
2690 }
2691
2692 /**
2693 * Unset variable
2694 *
2695 * @api
2696 *
2697 * @param string $name
2698 */
2699 public function unsetVariable($name)
2700 {
2701 unset($this->registeredVars[$name]);
2702 }
2703
2704 /**
2705 * Adds to list of parsed files
2706 *
2707 * @api
2708 *
2709 * @param string $path
2710 */
2711 public function addParsedFile($path)
2712 {
2713 if (isset($path)) {
2714 $this->parsedFiles[realpath($path)] = filemtime($path);
2715 }
2716 }
2717
2718 /**
2719 * Returns list of parsed files
2720 *
2721 * @api
2722 *
2723 * @return array
2724 */
2725 public function getParsedFiles()
2726 {
2727 return $this->parsedFiles;
2728 }
2729
2730 /**
2731 * Add import path
2732 *
2733 * @api
2734 *
2735 * @param string $path
2736 */
2737 public function addImportPath($path)
2738 {
2739 if (! in_array($path, $this->importPaths)) {
2740 $this->importPaths[] = $path;
2741 }
2742 }
2743
2744 /**
2745 * Set import paths
2746 *
2747 * @api
2748 *
2749 * @param string|array $path
2750 */
2751 public function setImportPaths($path)
2752 {
2753 $this->importPaths = (array)$path;
2754 }
2755
2756 /**
2757 * Set number precision
2758 *
2759 * @api
2760 *
2761 * @param integer $numberPrecision
2762 */
2763 public function setNumberPrecision($numberPrecision)
2764 {
2765 $this->numberPrecision = $numberPrecision;
2766 }
2767
2768 /**
2769 * Set formatter
2770 *
2771 * @api
2772 *
2773 * @param string $formatterName
2774 */
2775 public function setFormatter($formatterName)
2776 {
2777 $this->formatter = $formatterName;
2778 }
2779
2780 /**
2781 * Set line number style
2782 *
2783 * @api
2784 *
2785 * @param string $lineNumberStyle
2786 */
2787 public function setLineNumberStyle($lineNumberStyle)
2788 {
2789 $this->lineNumberStyle = $lineNumberStyle;
2790 }
2791
2792 /**
2793 * Register function
2794 *
2795 * @api
2796 *
2797 * @param string $name
2798 * @param callable $func
2799 */
2800 public function registerFunction($name, $func)
2801 {
2802 $this->userFunctions[$this->normalizeName($name)] = $func;
2803 }
2804
2805 /**
2806 * Unregister function
2807 *
2808 * @api
2809 *
2810 * @param string $name
2811 */
2812 public function unregisterFunction($name)
2813 {
2814 unset($this->userFunctions[$this->normalizeName($name)]);
2815 }
2816
2817 /**
2818 * Import file
2819 *
2820 * @param string $path
2821 * @param array $out
2822 */
2823 protected function importFile($path, $out)
2824 {
2825 // see if tree is cached
2826 $realPath = realpath($path);
2827
2828 if (isset($this->importCache[$realPath])) {
2829 $this->handleImportLoop($realPath);
2830
2831 $tree = $this->importCache[$realPath];
2832 } else {
2833 $code = file_get_contents($path);
2834 $parser = new Parser($path, false);
2835 $tree = $parser->parse($code);
2836
2837 $this->addParsedFile($path);
2838 $this->importCache[$realPath] = $tree;
2839 }
2840
2841 $pi = pathinfo($path);
2842 array_unshift($this->importPaths, $pi['dirname']);
2843 $this->compileChildren($tree->children, $out);
2844 array_shift($this->importPaths);
2845 }
2846
2847 /**
2848 * Return the file path for an import url if it exists
2849 *
2850 * @api
2851 *
2852 * @param string $url
2853 *
2854 * @return string|null
2855 */
2856 public function findImport($url)
2857 {
2858 $urls = array();
2859
2860 // for "normal" scss imports (ignore vanilla css and external requests)
2861 if (! preg_match('/\.css$|^https?:\/\//', $url)) {
2862 // try both normal and the _partial filename
2863 $urls = array($url, preg_replace('/[^\/]+$/', '_\0', $url));
2864 }
2865
2866 foreach ($this->importPaths as $dir) {
2867 if (is_string($dir)) {
2868 // check urls for normal import paths
2869 foreach ($urls as $full) {
2870 $full = $dir
2871 . (! empty($dir) && substr($dir, -1) !== '/' ? '/' : '')
2872 . $full;
2873
2874 if ($this->fileExists($file = $full . '.scss') ||
2875 $this->fileExists($file = $full)
2876 ) {
2877 return $file;
2878 }
2879 }
2880 } elseif (is_callable($dir)) {
2881 // check custom callback for import path
2882 $file = call_user_func($dir, $url, $this);
2883
2884 if ($file !== null) {
2885 return $file;
2886 }
2887 }
2888 }
2889
2890 return null;
2891 }
2892
2893 /**
2894 * Throw error (exception)
2895 *
2896 * @api
2897 *
2898 * @param string $msg Message with optional sprintf()-style vararg parameters
2899 *
2900 * @throws \Exception
2901 */
2902 public function throwError($msg)
2903 {
2904 if (func_num_args() > 1) {
2905 $msg = call_user_func_array('sprintf', func_get_args());
2906 }
2907
2908 if ($this->sourcePos >= 0 && isset($this->sourceParser)) {
2909 $this->sourceParser->throwParseError($msg, $this->sourcePos);
2910 }
2911
2912 throw new \Exception($msg);
2913 }
2914
2915 /**
2916 * Handle import loop
2917 *
2918 * @param string $name
2919 *
2920 * @throws \Exception
2921 */
2922 private function handleImportLoop($name)
2923 {
2924 for ($env = $this->env; $env; $env = $env->parent) {
2925 $file = $env->block->sourceParser->getSourceName();
2926
2927 if (realpath($file) === $name) {
2928 $this->throwError(
2929 'An @import loop has been found: %s imports %s',
2930 $this->env->block->sourceParser->getSourceName(),
2931 basename($file)
2932 );
2933 }
2934 }
2935 }
2936
2937 /**
2938 * Does file exist?
2939 *
2940 * @param string $name
2941 *
2942 * @return boolean
2943 */
2944 protected function fileExists($name)
2945 {
2946 return is_file($name);
2947 }
2948
2949 /**
2950 * Call built-in and registered (PHP) functions
2951 *
2952 * @param string $name
2953 * @param array $args
2954 * @param array $returnValue
2955 *
2956 * @return boolean Returns true if returnValue is set; otherwise, false
2957 */
2958 protected function callBuiltin($name, $args, &$returnValue)
2959 {
2960 // try a lib function
2961 $name = $this->normalizeName($name);
2962
2963 if (isset($this->userFunctions[$name])) {
2964 // see if we can find a user function
2965 $fn = $this->userFunctions[$name];
2966
2967 if ($name !== 'if' && $name !== 'call') {
2968 foreach ($args as &$val) {
2969 $val = $this->reduce($val[1], true);
2970 }
2971 }
2972
2973 $returnValue = call_user_func($fn, $args, $this);
2974 } else {
2975 $f = $this->getBuiltinFunction($name);
2976
2977 if (is_callable($f)) {
2978 $libName = $f[1];
2979
2980 $prototype = isset(self::$$libName) ? self::$$libName : null;
2981 $sorted = $this->sortArgs($prototype, $args);
2982
2983 if ($name !== 'if' && $name !== 'call') {
2984 foreach ($sorted as &$val) {
2985 $val = $this->reduce($val, true);
2986 }
2987 }
2988
2989 $returnValue = call_user_func($f, $sorted, $this);
2990 }
2991 }
2992
2993 if (isset($returnValue)) {
2994 $returnValue = $this->coerceValue($returnValue);
2995
2996 return true;
2997 }
2998
2999 return false;
3000 }
3001
3002 /**
3003 * Get built-in function
3004 *
3005 * @param string $name Normalized name
3006 *
3007 * @return array
3008 */
3009 protected function getBuiltinFunction($name)
3010 {
3011 $libName = 'lib' . preg_replace_callback(
3012 '/_(.)/',
3013 function ($m) {
3014 return ucfirst($m[1]);
3015 },
3016 ucfirst($name)
3017 );
3018
3019 return array($this, $libName);
3020 }
3021
3022 /**
3023 * Sorts keyword arguments
3024 *
3025 * @todo Merge with applyArguments()?
3026 *
3027 * @param array $prototype
3028 * @param array $args
3029 *
3030 * @return array
3031 */
3032 protected function sortArgs($prototype, $args)
3033 {
3034 $keyArgs = array();
3035 $posArgs = array();
3036
3037 foreach ($args as $arg) {
3038 list($key, $value) = $arg;
3039
3040 $key = $key[1];
3041
3042 if (empty($key)) {
3043 $posArgs[] = $value;
3044 } else {
3045 $keyArgs[$key] = $value;
3046 }
3047 }
3048
3049 if (! isset($prototype)) {
3050 return $posArgs;
3051 }
3052
3053 $finalArgs = array();
3054
3055 foreach ($prototype as $i => $names) {
3056 if (isset($posArgs[$i])) {
3057 $finalArgs[] = $posArgs[$i];
3058 continue;
3059 }
3060
3061 $set = false;
3062
3063 foreach ((array)$names as $name) {
3064 if (isset($keyArgs[$name])) {
3065 $finalArgs[] = $keyArgs[$name];
3066 $set = true;
3067 break;
3068 }
3069 }
3070
3071 if (! $set) {
3072 $finalArgs[] = null;
3073 }
3074 }
3075
3076 return $finalArgs;
3077 }
3078
3079 /**
3080 * Apply argument values per definition
3081 *
3082 * @param array $argDef
3083 * @param array $argValues
3084 *
3085 * @throws \Exception
3086 */
3087 protected function applyArguments($argDef, $argValues)
3088 {
3089 $storeEnv = $this->getStoreEnv();
3090
3091 $env = new \stdClass;
3092 $env->store = $storeEnv->store;
3093
3094 $hasVariable = false;
3095 $args = array();
3096
3097 foreach ($argDef as $i => $arg) {
3098 list($name, $default, $isVariable) = $argDef[$i];
3099
3100 $args[$name] = array($i, $name, $default, $isVariable);
3101 $hasVariable |= $isVariable;
3102 }
3103
3104 $keywordArgs = array();
3105 $deferredKeywordArgs = array();
3106 $remaining = array();
3107
3108 // assign the keyword args
3109 foreach ((array) $argValues as $arg) {
3110 if (! empty($arg[0])) {
3111 if (! isset($args[$arg[0][1]])) {
3112 if ($hasVariable) {
3113 $deferredKeywordArgs[$arg[0][1]] = $arg[1];
3114 } else {
3115 $this->throwError("Mixin or function doesn't have an argument named $%s.", $arg[0][1]);
3116 }
3117 } elseif ($args[$arg[0][1]][0] < count($remaining)) {
3118 $this->throwError("The argument $%s was passed both by position and by name.", $arg[0][1]);
3119 } else {
3120 $keywordArgs[$arg[0][1]] = $arg[1];
3121 }
3122 } elseif (count($keywordArgs)) {
3123 $this->throwError('Positional arguments must come before keyword arguments.');
3124 } elseif ($arg[2] === true) {
3125 $val = $this->reduce($arg[1], true);
3126
3127 if ($val[0] === 'list') {
3128 foreach ($val[2] as $name => $item) {
3129 if (! is_numeric($name)) {
3130 $keywordArgs[$name] = $item;
3131 } else {
3132 $remaining[] = $item;
3133 }
3134 }
3135 } else {
3136 $remaining[] = $val;
3137 }
3138 } else {
3139 $remaining[] = $arg[1];
3140 }
3141 }
3142
3143 foreach ($args as $arg) {
3144 list($i, $name, $default, $isVariable) = $arg;
3145
3146 if ($isVariable) {
3147 $val = array('list', ',', array(), $isVariable);
3148
3149 for ($count = count($remaining); $i < $count; $i++) {
3150 $val[2][] = $remaining[$i];
3151 }
3152
3153 foreach ($deferredKeywordArgs as $itemName => $item) {
3154 $val[2][$itemName] = $item;
3155 }
3156 } elseif (isset($remaining[$i])) {
3157 $val = $remaining[$i];
3158 } elseif (isset($keywordArgs[$name])) {
3159 $val = $keywordArgs[$name];
3160 } elseif (! empty($default)) {
3161 continue;
3162 } else {
3163 $this->throwError("Missing argument $name");
3164 }
3165
3166 $this->set($name, $this->reduce($val, true), true, $env);
3167 }
3168
3169 $storeEnv->store = $env->store;
3170
3171 foreach ($args as $arg) {
3172 list($i, $name, $default, $isVariable) = $arg;
3173
3174 if ($isVariable || isset($remaining[$i]) || isset($keywordArgs[$name]) || empty($default)) {
3175 continue;
3176 }
3177
3178 $this->set($name, $this->reduce($default, true), true);
3179 }
3180 }
3181
3182 /**
3183 * Coerce a php value into a scss one
3184 *
3185 * @param mixed $value
3186 *
3187 * @return array
3188 */
3189 private function coerceValue($value)
3190 {
3191 if (is_array($value)) {
3192 return $value;
3193 }
3194
3195 if (is_bool($value)) {
3196 return $value ? self::$true : self::$false;
3197 }
3198
3199 if ($value === null) {
3200 $value = self::$null;
3201 }
3202
3203 if (is_numeric($value)) {
3204 return array('number', $value, '');
3205 }
3206
3207 if ($value === '') {
3208 return self::$emptyString;
3209 }
3210
3211 return array('keyword', $value);
3212 }
3213
3214 /**
3215 * Coerce unit on number to be normalized
3216 *
3217 * @param array $number
3218 * @param string $unit
3219 *
3220 * @return array
3221 */
3222 protected function coerceUnit($number, $unit)
3223 {
3224 list(, $value, $baseUnit) = $number;
3225
3226 if (isset(self::$unitTable[$baseUnit][$unit])) {
3227 $value = $value * self::$unitTable[$baseUnit][$unit];
3228 }
3229
3230 return array('number', $value, $unit);
3231 }
3232
3233 /**
3234 * Coerce something to map
3235 *
3236 * @param array $item
3237 *
3238 * @return array
3239 */
3240 protected function coerceMap($item)
3241 {
3242 if ($item[0] === 'map') {
3243 return $item;
3244 }
3245
3246 if ($item === self::$emptyList) {
3247 return self::$emptyMap;
3248 }
3249
3250 return array('map', array($item), array(self::$null));
3251 }
3252
3253 /**
3254 * Coerce something to list
3255 *
3256 * @param array $item
3257 *
3258 * @return array
3259 */
3260 protected function coerceList($item, $delim = ',')
3261 {
3262 if (isset($item) && $item[0] === 'list') {
3263 return $item;
3264 }
3265
3266 if (isset($item) && $item[0] === 'map') {
3267 $keys = $item[1];
3268 $values = $item[2];
3269 $list = array();
3270
3271 for ($i = 0, $s = count($keys); $i < $s; $i++) {
3272 $key = $keys[$i];
3273 $value = $values[$i];
3274
3275 $list[] = array('list', '', array(array('keyword', $this->compileValue($key)), $value));
3276 }
3277
3278 return array('list', ',', $list);
3279 }
3280
3281 return array('list', $delim, ! isset($item) ? array(): array($item));
3282 }
3283
3284 /**
3285 * Coerce color for expression
3286 *
3287 * @param array $value
3288 *
3289 * @return array|null
3290 */
3291 protected function coerceForExpression($value)
3292 {
3293 if ($color = $this->coerceColor($value)) {
3294 return $color;
3295 }
3296
3297 return $value;
3298 }
3299
3300 /**
3301 * Coerce value to color
3302 *
3303 * @param array $value
3304 *
3305 * @return array|null
3306 */
3307 protected function coerceColor($value)
3308 {
3309 switch ($value[0]) {
3310 case 'color':
3311 return $value;
3312
3313 case 'keyword':
3314 $name = strtolower($value[1]);
3315
3316 if (isset(Colors::$cssColors[$name])) {
3317 $rgba = explode(',', Colors::$cssColors[$name]);
3318
3319 return isset($rgba[3])
3320 ? array('color', (int) $rgba[0], (int) $rgba[1], (int) $rgba[2], (int) $rgba[3])
3321 : array('color', (int) $rgba[0], (int) $rgba[1], (int) $rgba[2]);
3322 }
3323
3324 return null;
3325 }
3326
3327 return null;
3328 }
3329
3330 /**
3331 * Coerce value to string
3332 *
3333 * @param array $value
3334 *
3335 * @return array|null
3336 */
3337 protected function coerceString($value)
3338 {
3339 if ($value[0] === 'string') {
3340 return $value;
3341 }
3342
3343 return array('string', '', array($this->compileValue($value)));
3344 }
3345
3346 /**
3347 * Coerce value to a percentage
3348 *
3349 * @param array $value
3350 *
3351 * @return integer|float
3352 */
3353 protected function coercePercent($value)
3354 {
3355 if ($value[0] === 'number') {
3356 if ($value[2] === '%') {
3357 return $value[1] / 100;
3358 }
3359
3360 return $value[1];
3361 }
3362
3363 return 0;
3364 }
3365
3366 /**
3367 * Assert value is a map
3368 *
3369 * @api
3370 *
3371 * @param array $value
3372 *
3373 * @return array
3374 *
3375 * @throws \Exception
3376 */
3377 public function assertMap($value)
3378 {
3379 $value = $this->coerceMap($value);
3380
3381 if ($value[0] !== 'map') {
3382 $this->throwError('expecting map');
3383 }
3384
3385 return $value;
3386 }
3387
3388 /**
3389 * Assert value is a list
3390 *
3391 * @api
3392 *
3393 * @param array $value
3394 *
3395 * @return array
3396 *
3397 * @throws \Exception
3398 */
3399 public function assertList($value)
3400 {
3401 if ($value[0] !== 'list') {
3402 $this->throwError('expecting list');
3403 }
3404
3405 return $value;
3406 }
3407
3408 /**
3409 * Assert value is a color
3410 *
3411 * @api
3412 *
3413 * @param array $value
3414 *
3415 * @return array
3416 *
3417 * @throws \Exception
3418 */
3419 public function assertColor($value)
3420 {
3421 if ($color = $this->coerceColor($value)) {
3422 return $color;
3423 }
3424
3425 $this->throwError('expecting color');
3426 }
3427
3428 /**
3429 * Assert value is a number
3430 *
3431 * @api
3432 *
3433 * @param array $value
3434 *
3435 * @return integer|float
3436 *
3437 * @throws \Exception
3438 */
3439 public function assertNumber($value)
3440 {
3441 if ($value[0] !== 'number') {
3442 $this->throwError('expecting number');
3443 }
3444
3445 return $value[1];
3446 }
3447
3448 /**
3449 * Make sure a color's components don't go out of bounds
3450 *
3451 * @param array $c
3452 *
3453 * @return array
3454 */
3455 protected function fixColor($c)
3456 {
3457 foreach (range(1, 3) as $i) {
3458 if ($c[$i] < 0) {
3459 $c[$i] = 0;
3460 }
3461
3462 if ($c[$i] > 255) {
3463 $c[$i] = 255;
3464 }
3465 }
3466
3467 return $c;
3468 }
3469
3470 /**
3471 * Convert RGB to HSL
3472 *
3473 * @api
3474 *
3475 * @param integer $red
3476 * @param integer $green
3477 * @param integer $blue
3478 *
3479 * @return array
3480 */
3481 public function toHSL($red, $green, $blue)
3482 {
3483 $min = min($red, $green, $blue);
3484 $max = max($red, $green, $blue);
3485
3486 $l = $min + $max;
3487 $d = $max - $min;
3488
3489 if ((int) $d === 0) {
3490 $h = $s = 0;
3491 } else {
3492 if ($l < 255) {
3493 $s = $d / $l;
3494 } else {
3495 $s = $d / (510 - $l);
3496 }
3497
3498 if ($red == $max) {
3499 $h = 60 * ($green - $blue) / $d;
3500 } elseif ($green == $max) {
3501 $h = 60 * ($blue - $red) / $d + 120;
3502 } elseif ($blue == $max) {
3503 $h = 60 * ($red - $green) / $d + 240;
3504 }
3505 }
3506
3507 return array('hsl', fmod($h, 360), $s * 100, $l / 5.1);
3508 }
3509
3510 /**
3511 * Hue to RGB helper
3512 *
3513 * @param float $m1
3514 * @param float $m2
3515 * @param float $h
3516 *
3517 * @return float
3518 */
3519 private function hueToRGB($m1, $m2, $h)
3520 {
3521 if ($h < 0) {
3522 $h += 1;
3523 } elseif ($h > 1) {
3524 $h -= 1;
3525 }
3526
3527 if ($h * 6 < 1) {
3528 return $m1 + ($m2 - $m1) * $h * 6;
3529 }
3530
3531 if ($h * 2 < 1) {
3532 return $m2;
3533 }
3534
3535 if ($h * 3 < 2) {
3536 return $m1 + ($m2 - $m1) * (2/3 - $h) * 6;
3537 }
3538
3539 return $m1;
3540 }
3541
3542 /**
3543 * Convert HSL to RGB
3544 *
3545 * @api
3546 *
3547 * @param integer $hue H from 0 to 360
3548 * @param integer $saturation S from 0 to 100
3549 * @param integer $lightness L from 0 to 100
3550 *
3551 * @return array
3552 */
3553 public function toRGB($hue, $saturation, $lightness)
3554 {
3555 if ($hue < 0) {
3556 $hue += 360;
3557 }
3558
3559 $h = $hue / 360;
3560 $s = min(100, max(0, $saturation)) / 100;
3561 $l = min(100, max(0, $lightness)) / 100;
3562
3563 $m2 = $l <= 0.5 ? $l * ($s + 1) : $l + $s - $l * $s;
3564 $m1 = $l * 2 - $m2;
3565
3566 $r = $this->hueToRGB($m1, $m2, $h + 1/3) * 255;
3567 $g = $this->hueToRGB($m1, $m2, $h) * 255;
3568 $b = $this->hueToRGB($m1, $m2, $h - 1/3) * 255;
3569
3570 $out = array('color', $r, $g, $b);
3571
3572 return $out;
3573 }
3574
3575 // Built in functions
3576
3577 //protected static $libCall = array('name', 'args...');
3578 protected function libCall($args)
3579 {
3580 $name = $this->compileStringContent($this->coerceString($this->reduce(array_shift($args), true)));
3581
3582 return $this->reduce(
3583 array(
3584 'fncall',
3585 $name,
3586 array_map(
3587 function ($a) {
3588 return array(null, $a);
3589 },
3590 $args
3591 )
3592 )
3593 );
3594 }
3595
3596 protected static $libIf = array('condition', 'if-true', 'if-false');
3597 protected function libIf($args)
3598 {
3599 list($cond, $t, $f) = $args;
3600
3601 if (! $this->isTruthy($this->reduce($cond, true))) {
3602 return $this->reduce($f, true);
3603 }
3604
3605 return $this->reduce($t, true);
3606 }
3607
3608 protected static $libIndex = array('list', 'value');
3609 protected function libIndex($args)
3610 {
3611 list($list, $value) = $args;
3612
3613 if ($value[0] === 'map') {
3614 return self::$null;
3615 }
3616
3617 if ($list[0] === 'map') {
3618 $list = $this->coerceList($list, ' ');
3619 }
3620
3621 if ($list[0] !== 'list') {
3622 return self::$null;
3623 }
3624
3625 $values = array();
3626
3627 foreach ($list[2] as $item) {
3628 $values[] = $this->normalizeValue($item);
3629 }
3630
3631 $key = array_search($this->normalizeValue($value), $values);
3632
3633 return false === $key ? self::$null : $key + 1;
3634 }
3635
3636 protected static $libRgb = array('red', 'green', 'blue');
3637 protected function libRgb($args)
3638 {
3639 list($r, $g, $b) = $args;
3640
3641 return array('color', $r[1], $g[1], $b[1]);
3642 }
3643
3644 protected static $libRgba = array(
3645 array('red', 'color'),
3646 'green', 'blue', 'alpha');
3647 protected function libRgba($args)
3648 {
3649 if ($color = $this->coerceColor($args[0])) {
3650 // workaround https://github.com/facebook/hhvm/issues/5457
3651 reset($args);
3652
3653 $num = ! isset($args[1]) ? $args[3] : $args[1];
3654 $alpha = $this->assertNumber($num);
3655 $color[4] = $alpha;
3656
3657 return $color;
3658 }
3659
3660 list($r, $g, $b, $a) = $args;
3661
3662 return array('color', $r[1], $g[1], $b[1], $a[1]);
3663 }
3664
3665 // helper function for adjust_color, change_color, and scale_color
3666 protected function alterColor($args, $fn)
3667 {
3668 $color = $this->assertColor($args[0]);
3669
3670 // workaround https://github.com/facebook/hhvm/issues/5457
3671 reset($args);
3672
3673 foreach (array(1, 2, 3, 7) as $i) {
3674 if (isset($args[$i])) {
3675 $val = $this->assertNumber($args[$i]);
3676 $ii = $i === 7 ? 4 : $i; // alpha
3677 $color[$ii] = call_user_func($fn, isset($color[$ii]) ? $color[$ii] : 0, $val, $i);
3678 }
3679 }
3680
3681 if (isset($args[4]) || isset($args[5]) || isset($args[6])) {
3682 $hsl = $this->toHSL($color[1], $color[2], $color[3]);
3683
3684 foreach (array(4, 5, 6) as $i) {
3685 if (isset($args[$i])) {
3686 $val = $this->assertNumber($args[$i]);
3687 $hsl[$i - 3] = call_user_func($fn, $hsl[$i - 3], $val, $i);
3688 }
3689 }
3690
3691 $rgb = $this->toRGB($hsl[1], $hsl[2], $hsl[3]);
3692
3693 if (isset($color[4])) {
3694 $rgb[4] = $color[4];
3695 }
3696
3697 $color = $rgb;
3698 }
3699
3700 return $color;
3701 }
3702
3703 protected static $libAdjustColor = array(
3704 'color', 'red', 'green', 'blue',
3705 'hue', 'saturation', 'lightness', 'alpha'
3706 );
3707 protected function libAdjustColor($args)
3708 {
3709 return $this->alterColor($args, function ($base, $alter, $i) {
3710 return $base + $alter;
3711 });
3712 }
3713
3714 protected static $libChangeColor = array(
3715 'color', 'red', 'green', 'blue',
3716 'hue', 'saturation', 'lightness', 'alpha'
3717 );
3718 protected function libChangeColor($args)
3719 {
3720 return $this->alterColor($args, function ($base, $alter, $i) {
3721 return $alter;
3722 });
3723 }
3724
3725 protected static $libScaleColor = array(
3726 'color', 'red', 'green', 'blue',
3727 'hue', 'saturation', 'lightness', 'alpha'
3728 );
3729 protected function libScaleColor($args)
3730 {
3731 return $this->alterColor($args, function ($base, $scale, $i) {
3732 // 1, 2, 3 - rgb
3733 // 4, 5, 6 - hsl
3734 // 7 - a
3735 switch ($i) {
3736 case 1:
3737 case 2:
3738 case 3:
3739 $max = 255;
3740 break;
3741
3742 case 4:
3743 $max = 360;
3744 break;
3745
3746 case 7:
3747 $max = 1;
3748 break;
3749
3750 default:
3751 $max = 100;
3752 }
3753
3754 $scale = $scale / 100;
3755
3756 if ($scale < 0) {
3757 return $base * $scale + $base;
3758 }
3759
3760 return ($max - $base) * $scale + $base;
3761 });
3762 }
3763
3764 protected static $libIeHexStr = array('color');
3765 protected function libIeHexStr($args)
3766 {
3767 $color = $this->coerceColor($args[0]);
3768 $color[4] = isset($color[4]) ? round(255*$color[4]) : 255;
3769
3770 return sprintf('#%02X%02X%02X%02X', $color[4], $color[1], $color[2], $color[3]);
3771 }
3772
3773 protected static $libRed = array('color');
3774 protected function libRed($args)
3775 {
3776 $color = $this->coerceColor($args[0]);
3777
3778 return $color[1];
3779 }
3780
3781 protected static $libGreen = array('color');
3782 protected function libGreen($args)
3783 {
3784 $color = $this->coerceColor($args[0]);
3785
3786 return $color[2];
3787 }
3788
3789 protected static $libBlue = array('color');
3790 protected function libBlue($args)
3791 {
3792 $color = $this->coerceColor($args[0]);
3793
3794 return $color[3];
3795 }
3796
3797 protected static $libAlpha = array('color');
3798 protected function libAlpha($args)
3799 {
3800 if ($color = $this->coerceColor($args[0])) {
3801 return isset($color[4]) ? $color[4] : 1;
3802 }
3803
3804 // this might be the IE function, so return value unchanged
3805 return null;
3806 }
3807
3808 protected static $libOpacity = array('color');
3809 protected function libOpacity($args)
3810 {
3811 $value = $args[0];
3812
3813 if ($value[0] === 'number') {
3814 return null;
3815 }
3816
3817 return $this->libAlpha($args);
3818 }
3819
3820 // mix two colors
3821 protected static $libMix = array('color-1', 'color-2', 'weight');
3822 protected function libMix($args)
3823 {
3824 list($first, $second, $weight) = $args;
3825
3826 $first = $this->assertColor($first);
3827 $second = $this->assertColor($second);
3828
3829 if (! isset($weight)) {
3830 $weight = 0.5;
3831 } else {
3832 $weight = $this->coercePercent($weight);
3833 }
3834
3835 $firstAlpha = isset($first[4]) ? $first[4] : 1;
3836 $secondAlpha = isset($second[4]) ? $second[4] : 1;
3837
3838 $w = $weight * 2 - 1;
3839 $a = $firstAlpha - $secondAlpha;
3840
3841 $w1 = (($w * $a === -1 ? $w : ($w + $a) / (1 + $w * $a)) + 1) / 2.0;
3842 $w2 = 1.0 - $w1;
3843
3844 $new = array('color',
3845 $w1 * $first[1] + $w2 * $second[1],
3846 $w1 * $first[2] + $w2 * $second[2],
3847 $w1 * $first[3] + $w2 * $second[3],
3848 );
3849
3850 if ($firstAlpha != 1.0 || $secondAlpha != 1.0) {
3851 $new[] = $firstAlpha * $weight + $secondAlpha * ($weight - 1);
3852 }
3853
3854 return $this->fixColor($new);
3855 }
3856
3857 protected static $libHsl = array('hue', 'saturation', 'lightness');
3858 protected function libHsl($args)
3859 {
3860 list($h, $s, $l) = $args;
3861
3862 return $this->toRGB($h[1], $s[1], $l[1]);
3863 }
3864
3865 protected static $libHsla = array('hue', 'saturation', 'lightness', 'alpha');
3866 protected function libHsla($args)
3867 {
3868 list($h, $s, $l, $a) = $args;
3869
3870 $color = $this->toRGB($h[1], $s[1], $l[1]);
3871 $color[4] = $a[1];
3872
3873 return $color;
3874 }
3875
3876 protected static $libHue = array('color');
3877 protected function libHue($args)
3878 {
3879 $color = $this->assertColor($args[0]);
3880 $hsl = $this->toHSL($color[1], $color[2], $color[3]);
3881
3882 return array('number', $hsl[1], 'deg');
3883 }
3884
3885 protected static $libSaturation = array('color');
3886 protected function libSaturation($args)
3887 {
3888 $color = $this->assertColor($args[0]);
3889 $hsl = $this->toHSL($color[1], $color[2], $color[3]);
3890
3891 return array('number', $hsl[2], '%');
3892 }
3893
3894 protected static $libLightness = array('color');
3895 protected function libLightness($args)
3896 {
3897 $color = $this->assertColor($args[0]);
3898 $hsl = $this->toHSL($color[1], $color[2], $color[3]);
3899
3900 return array('number', $hsl[3], '%');
3901 }
3902
3903 protected function adjustHsl($color, $idx, $amount)
3904 {
3905 $hsl = $this->toHSL($color[1], $color[2], $color[3]);
3906 $hsl[$idx] += $amount;
3907 $out = $this->toRGB($hsl[1], $hsl[2], $hsl[3]);
3908
3909 if (isset($color[4])) {
3910 $out[4] = $color[4];
3911 }
3912
3913 return $out;
3914 }
3915
3916 protected static $libAdjustHue = array('color', 'degrees');
3917 protected function libAdjustHue($args)
3918 {
3919 $color = $this->assertColor($args[0]);
3920 $degrees = $this->assertNumber($args[1]);
3921
3922 return $this->adjustHsl($color, 1, $degrees);
3923 }
3924
3925 protected static $libLighten = array('color', 'amount');
3926 protected function libLighten($args)
3927 {
3928 $color = $this->assertColor($args[0]);
3929 $amount = Util::checkRange('amount', new Range(0, 100), $args[1], '%');
3930
3931 return $this->adjustHsl($color, 3, $amount);
3932 }
3933
3934 protected static $libDarken = array('color', 'amount');
3935 protected function libDarken($args)
3936 {
3937 $color = $this->assertColor($args[0]);
3938 $amount = Util::checkRange('amount', new Range(0, 100), $args[1], '%');
3939
3940 return $this->adjustHsl($color, 3, -$amount);
3941 }
3942
3943 protected static $libSaturate = array('color', 'amount');
3944 protected function libSaturate($args)
3945 {
3946 $value = $args[0];
3947
3948 if ($value[0] === 'number') {
3949 return null;
3950 }
3951
3952 $color = $this->assertColor($value);
3953 $amount = 100*$this->coercePercent($args[1]);
3954
3955 return $this->adjustHsl($color, 2, $amount);
3956 }
3957
3958 protected static $libDesaturate = array('color', 'amount');
3959 protected function libDesaturate($args)
3960 {
3961 $color = $this->assertColor($args[0]);
3962 $amount = 100*$this->coercePercent($args[1]);
3963
3964 return $this->adjustHsl($color, 2, -$amount);
3965 }
3966
3967 protected static $libGrayscale = array('color');
3968 protected function libGrayscale($args)
3969 {
3970 $value = $args[0];
3971
3972 if ($value[0] === 'number') {
3973 return null;
3974 }
3975
3976 return $this->adjustHsl($this->assertColor($value), 2, -100);
3977 }
3978
3979 protected static $libComplement = array('color');
3980 protected function libComplement($args)
3981 {
3982 return $this->adjustHsl($this->assertColor($args[0]), 1, 180);
3983 }
3984
3985 protected static $libInvert = array('color');
3986 protected function libInvert($args)
3987 {
3988 $value = $args[0];
3989
3990 if ($value[0] === 'number') {
3991 return null;
3992 }
3993
3994 $color = $this->assertColor($value);
3995 $color[1] = 255 - $color[1];
3996 $color[2] = 255 - $color[2];
3997 $color[3] = 255 - $color[3];
3998
3999 return $color;
4000 }
4001
4002 // increases opacity by amount
4003 protected static $libOpacify = array('color', 'amount');
4004 protected function libOpacify($args)
4005 {
4006 $color = $this->assertColor($args[0]);
4007 $amount = $this->coercePercent($args[1]);
4008
4009 $color[4] = (isset($color[4]) ? $color[4] : 1) + $amount;
4010 $color[4] = min(1, max(0, $color[4]));
4011
4012 return $color;
4013 }
4014
4015 protected static $libFadeIn = array('color', 'amount');
4016 protected function libFadeIn($args)
4017 {
4018 return $this->libOpacify($args);
4019 }
4020
4021 // decreases opacity by amount
4022 protected static $libTransparentize = array('color', 'amount');
4023 protected function libTransparentize($args)
4024 {
4025 $color = $this->assertColor($args[0]);
4026 $amount = $this->coercePercent($args[1]);
4027
4028 $color[4] = (isset($color[4]) ? $color[4] : 1) - $amount;
4029 $color[4] = min(1, max(0, $color[4]));
4030
4031 return $color;
4032 }
4033
4034 protected static $libFadeOut = array('color', 'amount');
4035 protected function libFadeOut($args)
4036 {
4037 return $this->libTransparentize($args);
4038 }
4039
4040 protected static $libUnquote = array('string');
4041 protected function libUnquote($args)
4042 {
4043 $str = $args[0];
4044
4045 if ($str[0] === 'string') {
4046 $str[1] = '';
4047 }
4048
4049 return $str;
4050 }
4051
4052 protected static $libQuote = array('string');
4053 protected function libQuote($args)
4054 {
4055 $value = $args[0];
4056
4057 if ($value[0] === 'string' && ! empty($value[1])) {
4058 return $value;
4059 }
4060
4061 return array('string', '"', array($value));
4062 }
4063
4064 protected static $libPercentage = array('value');
4065 protected function libPercentage($args)
4066 {
4067 return array('number',
4068 $this->coercePercent($args[0]) * 100,
4069 '%');
4070 }
4071
4072 protected static $libRound = array('value');
4073 protected function libRound($args)
4074 {
4075 $num = $args[0];
4076 $num[1] = round($num[1]);
4077
4078 return $num;
4079 }
4080
4081 protected static $libFloor = array('value');
4082 protected function libFloor($args)
4083 {
4084 $num = $args[0];
4085 $num[1] = floor($num[1]);
4086
4087 return $num;
4088 }
4089
4090 protected static $libCeil = array('value');
4091 protected function libCeil($args)
4092 {
4093 $num = $args[0];
4094 $num[1] = ceil($num[1]);
4095
4096 return $num;
4097 }
4098
4099 protected static $libAbs = array('value');
4100 protected function libAbs($args)
4101 {
4102 $num = $args[0];
4103 $num[1] = abs($num[1]);
4104
4105 return $num;
4106 }
4107
4108 protected function libMin($args)
4109 {
4110 $numbers = $this->getNormalizedNumbers($args);
4111 $min = null;
4112
4113 foreach ($numbers as $key => $number) {
4114 if (null === $min || $number[1] <= $min[1]) {
4115 $min = array($key, $number[1]);
4116 }
4117 }
4118
4119 return $args[$min[0]];
4120 }
4121
4122 protected function libMax($args)
4123 {
4124 $numbers = $this->getNormalizedNumbers($args);
4125 $max = null;
4126
4127 foreach ($numbers as $key => $number) {
4128 if (null === $max || $number[1] >= $max[1]) {
4129 $max = array($key, $number[1]);
4130 }
4131 }
4132
4133 return $args[$max[0]];
4134 }
4135
4136 /**
4137 * Helper to normalize args containing numbers
4138 *
4139 * @param array $args
4140 *
4141 * @return array
4142 */
4143 protected function getNormalizedNumbers($args)
4144 {
4145 $unit = null;
4146 $originalUnit = null;
4147 $numbers = array();
4148
4149 foreach ($args as $key => $item) {
4150 if ('number' !== $item[0]) {
4151 $this->throwError('%s is not a number', $item[0]);
4152 }
4153
4154 $number = $this->normalizeNumber($item);
4155
4156 if (null === $unit) {
4157 $unit = $number[2];
4158 $originalUnit = $item[2];
4159 } elseif ($unit !== $number[2]) {
4160 $this->throwError('Incompatible units: "%s" and "%s".', $originalUnit, $item[2]);
4161 }
4162
4163 $numbers[$key] = $number;
4164 }
4165
4166 return $numbers;
4167 }
4168
4169 protected static $libLength = array('list');
4170 protected function libLength($args)
4171 {
4172 $list = $this->coerceList($args[0]);
4173
4174 return count($list[2]);
4175 }
4176
4177 // TODO: need a way to declare this built-in as varargs
4178 //protected static $libListSeparator = array('list...');
4179 protected function libListSeparator($args)
4180 {
4181 if (count($args) > 1) {
4182 return 'comma';
4183 }
4184
4185 $list = $this->coerceList($args[0]);
4186
4187 if (count($list[2]) <= 1) {
4188 return 'space';
4189 }
4190
4191 if ($list[1] === ',') {
4192 return 'comma';
4193 }
4194
4195 return 'space';
4196 }
4197
4198 protected static $libNth = array('list', 'n');
4199 protected function libNth($args)
4200 {
4201 $list = $this->coerceList($args[0]);
4202 $n = $this->assertNumber($args[1]) - 1;
4203
4204 return isset($list[2][$n]) ? $list[2][$n] : self::$defaultValue;
4205 }
4206
4207 protected static $libSetNth = array('list', 'n', 'value');
4208 protected function libSetNth($args)
4209 {
4210 $list = $this->coerceList($args[0]);
4211 $n = $this->assertNumber($args[1]) - 1;
4212
4213 if (! isset($list[2][$n])) {
4214 $this->throwError('Invalid argument for "n"');
4215 }
4216
4217 $list[2][$n] = $args[2];
4218
4219 return $list;
4220 }
4221
4222 protected static $libMapGet = array('map', 'key');
4223 protected function libMapGet($args)
4224 {
4225 $map = $this->assertMap($args[0]);
4226 $key = $this->compileStringContent($this->coerceString($args[1]));
4227
4228 for ($i = count($map[1]) - 1; $i >= 0; $i--) {
4229 if ($key === $this->compileStringContent($this->coerceString($map[1][$i]))) {
4230 return $map[2][$i];
4231 }
4232 }
4233
4234 return self::$null;
4235 }
4236
4237 protected static $libMapKeys = array('map');
4238 protected function libMapKeys($args)
4239 {
4240 $map = $this->assertMap($args[0]);
4241 $keys = $map[1];
4242
4243 return array('list', ',', $keys);
4244 }
4245
4246 protected static $libMapValues = array('map');
4247 protected function libMapValues($args)
4248 {
4249 $map = $this->assertMap($args[0]);
4250 $values = $map[2];
4251
4252 return array('list', ',', $values);
4253 }
4254
4255 protected static $libMapRemove = array('map', 'key');
4256 protected function libMapRemove($args)
4257 {
4258 $map = $this->assertMap($args[0]);
4259 $key = $this->compileStringContent($this->coerceString($args[1]));
4260
4261 for ($i = count($map[1]) - 1; $i >= 0; $i--) {
4262 if ($key === $this->compileStringContent($this->coerceString($map[1][$i]))) {
4263 array_splice($map[1], $i, 1);
4264 array_splice($map[2], $i, 1);
4265 }
4266 }
4267
4268 return $map;
4269 }
4270
4271 protected static $libMapHasKey = array('map', 'key');
4272 protected function libMapHasKey($args)
4273 {
4274 $map = $this->assertMap($args[0]);
4275 $key = $this->compileStringContent($this->coerceString($args[1]));
4276
4277 for ($i = count($map[1]) - 1; $i >= 0; $i--) {
4278 if ($key === $this->compileStringContent($this->coerceString($map[1][$i]))) {
4279 return self::$true;
4280 }
4281 }
4282
4283 return self::$false;
4284 }
4285
4286 protected static $libMapMerge = array('map-1', 'map-2');
4287 protected function libMapMerge($args)
4288 {
4289 $map1 = $this->assertMap($args[0]);
4290 $map2 = $this->assertMap($args[1]);
4291
4292 return array('map', array_merge($map1[1], $map2[1]), array_merge($map1[2], $map2[2]));
4293 }
4294
4295 protected function listSeparatorForJoin($list1, $sep)
4296 {
4297 if (! isset($sep)) {
4298 return $list1[1];
4299 }
4300
4301 switch ($this->compileValue($sep)) {
4302 case 'comma':
4303 return ',';
4304
4305 case 'space':
4306 return '';
4307
4308 default:
4309 return $list1[1];
4310 }
4311 }
4312
4313 protected static $libJoin = array('list1', 'list2', 'separator');
4314 protected function libJoin($args)
4315 {
4316 list($list1, $list2, $sep) = $args;
4317
4318 $list1 = $this->coerceList($list1, ' ');
4319 $list2 = $this->coerceList($list2, ' ');
4320 $sep = $this->listSeparatorForJoin($list1, $sep);
4321
4322 return array('list', $sep, array_merge($list1[2], $list2[2]));
4323 }
4324
4325 protected static $libAppend = array('list', 'val', 'separator');
4326 protected function libAppend($args)
4327 {
4328 list($list1, $value, $sep) = $args;
4329
4330 $list1 = $this->coerceList($list1, ' ');
4331 $sep = $this->listSeparatorForJoin($list1, $sep);
4332
4333 return array('list', $sep, array_merge($list1[2], array($value)));
4334 }
4335
4336 protected function libZip($args)
4337 {
4338 foreach ($args as $arg) {
4339 $this->assertList($arg);
4340 }
4341
4342 $lists = array();
4343 $firstList = array_shift($args);
4344
4345 foreach ($firstList[2] as $key => $item) {
4346 $list = array('list', '', array($item));
4347
4348 foreach ($args as $arg) {
4349 if (isset($arg[2][$key])) {
4350 $list[2][] = $arg[2][$key];
4351 } else {
4352 break 2;
4353 }
4354 }
4355
4356 $lists[] = $list;
4357 }
4358
4359 return array('list', ',', $lists);
4360 }
4361
4362 protected static $libTypeOf = array('value');
4363 protected function libTypeOf($args)
4364 {
4365 $value = $args[0];
4366
4367 switch ($value[0]) {
4368 case 'keyword':
4369 if ($value === self::$true || $value === self::$false) {
4370 return 'bool';
4371 }
4372
4373 if ($this->coerceColor($value)) {
4374 return 'color';
4375 }
4376
4377 // fall-thru
4378 case 'function':
4379 return 'string';
4380
4381 case 'list':
4382 if (isset($value[3]) && $value[3]) {
4383 return 'arglist';
4384 }
4385
4386 // fall-thru
4387 default:
4388 return $value[0];
4389 }
4390 }
4391
4392 protected static $libUnit = array('number');
4393 protected function libUnit($args)
4394 {
4395 $num = $args[0];
4396
4397 if ($num[0] === 'number') {
4398 return array('string', '"', array($num[2]));
4399 }
4400
4401 return '';
4402 }
4403
4404 protected static $libUnitless = array('number');
4405 protected function libUnitless($args)
4406 {
4407 $value = $args[0];
4408
4409 return $value[0] === 'number' && empty($value[2]);
4410 }
4411
4412 protected static $libComparable = array('number-1', 'number-2');
4413 protected function libComparable($args)
4414 {
4415 list($number1, $number2) = $args;
4416
4417 if (! isset($number1[0]) || $number1[0] !== 'number' || ! isset($number2[0]) || $number2[0] !== 'number') {
4418 $this->throwError('Invalid argument(s) for "comparable"');
4419 }
4420
4421 $number1 = $this->normalizeNumber($number1);
4422 $number2 = $this->normalizeNumber($number2);
4423
4424 return $number1[2] === $number2[2] || $number1[2] === '' || $number2[2] === '';
4425 }
4426
4427 protected static $libStrIndex = array('string', 'substring');
4428 protected function libStrIndex($args)
4429 {
4430 $string = $this->coerceString($args[0]);
4431 $stringContent = $this->compileStringContent($string);
4432
4433 $substring = $this->coerceString($args[1]);
4434 $substringContent = $this->compileStringContent($substring);
4435
4436 $result = strpos($stringContent, $substringContent);
4437
4438 return $result === false ? self::$null : array('number', $result + 1, '');
4439 }
4440
4441 protected static $libStrInsert = array('string', 'insert', 'index');
4442 protected function libStrInsert($args)
4443 {
4444 $string = $this->coerceString($args[0]);
4445 $stringContent = $this->compileStringContent($string);
4446
4447 $insert = $this->coerceString($args[1]);
4448 $insertContent = $this->compileStringContent($insert);
4449
4450 list(, $index) = $args[2];
4451
4452 $string[2] = array(substr_replace($stringContent, $insertContent, $index - 1, 0));
4453
4454 return $string;
4455 }
4456
4457 protected static $libStrLength = array('string');
4458 protected function libStrLength($args)
4459 {
4460 $string = $this->coerceString($args[0]);
4461 $stringContent = $this->compileStringContent($string);
4462
4463 return array('number', strlen($stringContent), '');
4464 }
4465
4466 protected static $libStrSlice = array('string', 'start-at', 'end-at');
4467 protected function libStrSlice($args)
4468 {
4469 if ($args[2][1] == 0) {
4470 return self::$emptyString;
4471 }
4472
4473 $string = $this->coerceString($args[0]);
4474 $stringContent = $this->compileStringContent($string);
4475
4476 $start = (int) $args[1][1] ?: 1;
4477 $end = (int) $args[2][1];
4478
4479 $string[2] = array(substr($stringContent, $start - 1, $end < 0 ? $end : $end - $start + 1));
4480
4481 return $string;
4482 }
4483
4484 protected static $libToLowerCase = array('string');
4485 protected function libToLowerCase($args)
4486 {
4487 $string = $this->coerceString($args[0]);
4488 $stringContent = $this->compileStringContent($string);
4489
4490 $string[2] = array(mb_strtolower($stringContent));
4491
4492 return $string;
4493 }
4494
4495 protected static $libToUpperCase = array('string');
4496 protected function libToUpperCase($args)
4497 {
4498 $string = $this->coerceString($args[0]);
4499 $stringContent = $this->compileStringContent($string);
4500
4501 $string[2] = array(mb_strtoupper($stringContent));
4502
4503 return $string;
4504 }
4505
4506 protected static $libFeatureExists = array('feature');
4507 protected function libFeatureExists($args)
4508 {
4509 /*
4510 * The following features not not (yet) supported:
4511 * - global-variable-shadowing
4512 * - extend-selector-pseudoclass
4513 * - units-level-3
4514 * - at-error
4515 */
4516 return self::$false;
4517 }
4518
4519 protected static $libFunctionExists = array('name');
4520 protected function libFunctionExists($args)
4521 {
4522 $string = $this->coerceString($args[0]);
4523 $name = $this->compileStringContent($string);
4524
4525 // user defined functions
4526 if ($this->has(self::$namespaces['function'] . $name)) {
4527 return self::$true;
4528 }
4529
4530 $name = $this->normalizeName($name);
4531
4532 if (isset($this->userFunctions[$name])) {
4533 return self::$true;
4534 }
4535
4536 // built-in functions
4537 $f = $this->getBuiltinFunction($name);
4538
4539 return $this->toBool(is_callable($f));
4540 }
4541
4542 protected static $libGlobalVariableExists = array('name');
4543 protected function libGlobalVariableExists($args)
4544 {
4545 $string = $this->coerceString($args[0]);
4546 $name = $this->compileStringContent($string);
4547
4548 return $this->has($name, $this->rootEnv) ? self::$true : self::$false;
4549 }
4550
4551 protected static $libMixinExists = array('name');
4552 protected function libMixinExists($args)
4553 {
4554 $string = $this->coerceString($args[0]);
4555 $name = $this->compileStringContent($string);
4556
4557 return $this->has(self::$namespaces['mixin'] . $name) ? self::$true : self::$false;
4558 }
4559
4560 protected static $libVariableExists = array('name');
4561 protected function libVariableExists($args)
4562 {
4563 $string = $this->coerceString($args[0]);
4564 $name = $this->compileStringContent($string);
4565
4566 return $this->has($name) ? self::$true : self::$false;
4567 }
4568
4569 /**
4570 * Workaround IE7's content counter bug.
4571 *
4572 * @param array $args
4573 */
4574 protected function libCounter($args)
4575 {
4576 $list = array_map(array($this, 'compileValue'), $args);
4577
4578 return array('string', '', array('counter(' . implode(',', $list) . ')'));
4579 }
4580
4581 protected function libRandom($args)
4582 {
4583 if (isset($args[0])) {
4584 $n = $this->assertNumber($args[0]);
4585
4586 if ($n < 1) {
4587 $this->throwError("limit must be greater than or equal to 1");
4588 }
4589
4590 return array('number', mt_rand(1, $n), '');
4591 }
4592
4593 return array('number', mt_rand(1, mt_getrandmax()), '');
4594 }
4595
4596 protected function libUniqueId()
4597 {
4598 static $id;
4599
4600 if (! isset($id)) {
4601 $id = mt_rand(0, pow(36, 8));
4602 }
4603
4604 $id += mt_rand(0, 10) + 1;
4605
4606 return array('string', '', array('u' . str_pad(base_convert($id, 10, 36), 8, '0', STR_PAD_LEFT)));
4607 }
4608 }
4609