PluginProbe
GetResponse Forms by Optin Cat / 1.5.4
GetResponse Forms by Optin Cat v1.5.4
1.3.4 1.3.5 1.3.6 1.3.8 1.3.9 1.4.0 1.4.1 1.4.2 1.5.0 1.5.1 1.5.2 1.5.4 1.5.5 1.5.6 1.5.7 1.5.8 1.6.1 1.6.2 1.6.3 1.7.0 1.7.1 1.7.2 1.8.0 1.8.1 2.0.0 All 36 releases
getresponse / includes / classes / scssphp / src / Compiler.php

Compiler.php in GetResponse Forms by Optin Cat 1.5.4, at includes/classes/scssphp/src/Compiler.php

3,007 lines 82.2 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-2014 Leaf Corcoran
6 *
7 * @license http://opensource.org/licenses/gpl-license GPL-3.0
8 * @license http://opensource.org/licenses/MIT MIT
9 *
10 * @link http://leafo.net/scssphp
11 */
12
13 namespace Leafo\ScssPhp;
14
15 use Leafo\ScssPhp\Colors;
16 use Leafo\ScssPhp\Parser;
17
18 /**
19 * The scss compiler and parser.
20 *
21 * Converting SCSS to CSS is a three stage process. The incoming file is parsed
22 * by `Parser` into a syntax tree, then it is compiled into another tree
23 * representing the CSS structure by `Compiler`. The CSS tree is fed into a
24 * formatter, like `Formatter` which then outputs CSS as a string.
25 *
26 * During the first compile, all values are *reduced*, which means that their
27 * types are brought to the lowest form before being dump as strings. This
28 * handles math equations, variable dereferences, and the like.
29 *
30 * The `parse` function of `Compiler` is the entry point.
31 *
32 * In summary:
33 *
34 * The `Compiler` class creates an instance of the parser, feeds it SCSS code,
35 * then transforms the resulting tree to a CSS tree. This class also holds the
36 * evaluation context, such as all available mixins and variables at any given
37 * time.
38 *
39 * The `Parser` class is only concerned with parsing its input.
40 *
41 * The `Formatter` takes a CSS tree, and dumps it to a formatted string,
42 * handling things like indentation.
43 */
44
45 /**
46 * SCSS compiler
47 *
48 * @author Leaf Corcoran <leafot@gmail.com>
49 */
50 class Compiler
51 {
52 static public $VERSION = 'v0.1.1';
53
54 static protected $operatorNames = array(
55 '+' => 'add',
56 '-' => 'sub',
57 '*' => 'mul',
58 '/' => 'div',
59 '%' => 'mod',
60
61 '==' => 'eq',
62 '!=' => 'neq',
63 '<' => 'lt',
64 '>' => 'gt',
65
66 '<=' => 'lte',
67 '>=' => 'gte',
68 );
69
70 static protected $namespaces = array(
71 'special' => '%',
72 'mixin' => '@',
73 'function' => '^',
74 );
75
76 static protected $unitTable = array(
77 'in' => array(
78 'in' => 1,
79 'pt' => 72,
80 'pc' => 6,
81 'cm' => 2.54,
82 'mm' => 25.4,
83 'px' => 96,
84 )
85 );
86
87 static public $true = array('keyword', 'true');
88 static public $false = array('keyword', 'false');
89 static public $null = array('null');
90
91 static public $defaultValue = array('keyword', '');
92 static public $selfSelector = array('self');
93
94 protected $importPaths = array('');
95 protected $importCache = array();
96
97 protected $userFunctions = array();
98 protected $registeredVars = array();
99
100 protected $numberPrecision = 5;
101
102 protected $formatter = 'Leafo\ScssPhp\Formatter\Nested';
103
104 /**
105 * Compile scss
106 *
107 * @param string $code
108 * @param string $name
109 *
110 * @return string
111 */
112 public function compile($code, $name = null)
113 {
114 $this->indentLevel = -1;
115 $this->commentsSeen = array();
116 $this->extends = array();
117 $this->extendsMap = array();
118 $this->parsedFiles = array();
119 $this->env = null;
120 $this->scope = null;
121
122 $locale = setlocale(LC_NUMERIC, 0);
123 setlocale(LC_NUMERIC, 'C');
124
125 $this->parser = new Parser($name);
126
127 $tree = $this->parser->parse($code);
128
129 $this->formatter = new $this->formatter();
130
131 $this->pushEnv($tree);
132 $this->injectVariables($this->registeredVars);
133 $this->compileRoot($tree);
134 $this->popEnv();
135
136 $out = $this->formatter->format($this->scope);
137
138 setlocale(LC_NUMERIC, $locale);
139
140 return $out;
141 }
142
143 protected function isSelfExtend($target, $origin)
144 {
145 foreach ($origin as $sel) {
146 if (in_array($target, $sel)) {
147 return true;
148 }
149 }
150
151 return false;
152 }
153
154 protected function pushExtends($target, $origin)
155 {
156 if ($this->isSelfExtend($target, $origin)) {
157 return;
158 }
159
160 $i = count($this->extends);
161 $this->extends[] = array($target, $origin);
162
163 foreach ($target as $part) {
164 if (isset($this->extendsMap[$part])) {
165 $this->extendsMap[$part][] = $i;
166 } else {
167 $this->extendsMap[$part] = array($i);
168 }
169 }
170 }
171
172 protected function makeOutputBlock($type, $selectors = null)
173 {
174 $out = new \stdClass;
175 $out->type = $type;
176 $out->lines = array();
177 $out->children = array();
178 $out->parent = $this->scope;
179 $out->selectors = $selectors;
180 $out->depth = $this->env->depth;
181
182 return $out;
183 }
184
185 protected function matchExtendsSingle($single, &$outOrigin)
186 {
187 $counts = array();
188 foreach ($single as $part) {
189 if (!is_string($part)) {
190 return false; // hmm
191 }
192
193 if (isset($this->extendsMap[$part])) {
194 foreach ($this->extendsMap[$part] as $idx) {
195 $counts[$idx] =
196 isset($counts[$idx]) ? $counts[$idx] + 1 : 1;
197 }
198 }
199 }
200
201 $outOrigin = array();
202 $found = false;
203
204 foreach ($counts as $idx => $count) {
205 list($target, $origin) = $this->extends[$idx];
206
207 // check count
208 if ($count != count($target)) {
209 continue;
210 }
211
212 // check if target is subset of single
213 if (array_diff(array_intersect($single, $target), $target)) {
214 continue;
215 }
216
217 $rem = array_diff($single, $target);
218
219 foreach ($origin as $j => $new) {
220 // prevent infinite loop when target extends itself
221 foreach ($new as $new_selector) {
222 if (!array_diff($single, $new_selector)) {
223 continue 2;
224 }
225 }
226
227 $origin[$j][count($origin[$j]) - 1] = $this->combineSelectorSingle(end($new), $rem);
228 }
229
230 $outOrigin = array_merge($outOrigin, $origin);
231
232 $found = true;
233 }
234
235 return $found;
236 }
237
238 protected function combineSelectorSingle($base, $other)
239 {
240 $tag = null;
241 $out = array();
242
243 foreach (array($base, $other) as $single) {
244 foreach ($single as $part) {
245 if (preg_match('/^[^\[.#:]/', $part)) {
246 $tag = $part;
247 } else {
248 $out[] = $part;
249 }
250 }
251 }
252
253 if ($tag) {
254 array_unshift($out, $tag);
255 }
256
257 return $out;
258 }
259
260 protected function matchExtends($selector, &$out, $from = 0, $initial = true)
261 {
262 foreach ($selector as $i => $part) {
263 if ($i < $from) {
264 continue;
265 }
266
267 if ($this->matchExtendsSingle($part, $origin)) {
268 $before = array_slice($selector, 0, $i);
269 $after = array_slice($selector, $i + 1);
270
271 foreach ($origin as $new) {
272 $k = 0;
273
274 // remove shared parts
275 if ($initial) {
276 foreach ($before as $k => $val) {
277 if (!isset($new[$k]) || $val != $new[$k]) {
278 break;
279 }
280 }
281 }
282
283 $result = array_merge(
284 $before,
285 $k > 0 ? array_slice($new, $k) : $new,
286 $after
287 );
288
289 if ($result == $selector) {
290 continue;
291 }
292
293 $out[] = $result;
294
295 // recursively check for more matches
296 $this->matchExtends($result, $out, $i, false);
297
298 // selector sequence merging
299 if (!empty($before) && count($new) > 1) {
300 $result2 = array_merge(
301 array_slice($new, 0, -1),
302 $k > 0 ? array_slice($before, $k) : $before,
303 array_slice($new, -1),
304 $after
305 );
306
307 $out[] = $result2;
308 }
309 }
310 }
311 }
312 }
313
314 protected function flattenSelectors($block, $parentKey = null)
315 {
316 if ($block->selectors) {
317 $selectors = array();
318
319 foreach ($block->selectors as $s) {
320 $selectors[] = $s;
321
322 if (!is_array($s)) {
323 continue;
324 }
325
326 // check extends
327 if (!empty($this->extendsMap)) {
328 $this->matchExtends($s, $selectors);
329 }
330 }
331
332 $block->selectors = array();
333 $placeholderSelector = false;
334
335 foreach ($selectors as $selector) {
336 if ($this->hasSelectorPlaceholder($selector)) {
337 $placeholderSelector = true;
338 continue;
339 }
340
341 $block->selectors[] = $this->compileSelector($selector);
342 }
343
344 if ($placeholderSelector && 0 == count($block->selectors) && null !== $parentKey) {
345 unset($block->parent->children[$parentKey]);
346
347 return;
348 }
349 }
350
351 foreach ($block->children as $key => $child) {
352 $this->flattenSelectors($child, $key);
353 }
354 }
355
356 protected function compileRoot($rootBlock)
357 {
358 $this->scope = $this->makeOutputBlock('root');
359
360 $this->compileChildren($rootBlock->children, $this->scope);
361 $this->flattenSelectors($this->scope);
362 }
363
364 protected function compileMedia($media)
365 {
366 $this->pushEnv($media);
367
368 $mediaQuery = $this->compileMediaQuery($this->multiplyMedia($this->env));
369
370 if (!empty($mediaQuery)) {
371 $this->scope = $this->makeOutputBlock('media', array($mediaQuery));
372
373 $parentScope = $this->mediaParent($this->scope);
374 $parentScope->children[] = $this->scope;
375
376 // top level properties in a media cause it to be wrapped
377 $needsWrap = false;
378
379 foreach ($media->children as $child) {
380 $type = $child[0];
381 if ($type !== 'block' && $type !== 'media' && $type !== 'directive') {
382 $needsWrap = true;
383 break;
384 }
385 }
386
387 if ($needsWrap) {
388 $wrapped = (object)array(
389 'selectors' => array(),
390 'children' => $media->children
391 );
392 $media->children = array(array('block', $wrapped));
393 }
394
395 $this->compileChildren($media->children, $this->scope);
396
397 $this->scope = $this->scope->parent;
398 }
399
400 $this->popEnv();
401 }
402
403 protected function mediaParent($scope)
404 {
405 while (!empty($scope->parent)) {
406 if (!empty($scope->type) && $scope->type != 'media') {
407 break;
408 }
409 $scope = $scope->parent;
410 }
411
412 return $scope;
413 }
414
415 // TODO refactor compileNestedBlock and compileMedia into same thing
416 protected function compileNestedBlock($block, $selectors)
417 {
418 $this->pushEnv($block);
419
420 $this->scope = $this->makeOutputBlock($block->type, $selectors);
421 $this->scope->parent->children[] = $this->scope;
422 $this->compileChildren($block->children, $this->scope);
423
424 $this->scope = $this->scope->parent;
425 $this->popEnv();
426 }
427
428 /**
429 * Recursively compiles a block.
430 *
431 * A block is analogous to a CSS block in most cases. A single SCSS document
432 * is encapsulated in a block when parsed, but it does not have parent tags
433 * so all of its children appear on the root level when compiled.
434 *
435 * Blocks are made up of selectors and children.
436 *
437 * The children of a block are just all the blocks that are defined within.
438 *
439 * Compiling the block involves pushing a fresh environment on the stack,
440 * and iterating through the props, compiling each one.
441 *
442 * @see Compiler::compileChild()
443 *
444 * @param \StdClass $block
445 */
446 protected function compileBlock($block)
447 {
448 $env = $this->pushEnv($block);
449
450 $env->selectors =
451 array_map(array($this, 'evalSelector'), $block->selectors);
452
453 $out = $this->makeOutputBlock(null, $this->multiplySelectors($env));
454 $this->scope->children[] = $out;
455 $this->compileChildren($block->children, $out);
456
457 $this->popEnv();
458 }
459
460 // root level comment
461 protected function compileComment($block)
462 {
463 $out = $this->makeOutputBlock('comment');
464 $out->lines[] = $block[1];
465 $this->scope->children[] = $out;
466 }
467
468 // joins together .classes and #ids
469 protected function flattenSelectorSingle($single)
470 {
471 $joined = array();
472 foreach ($single as $part) {
473 if (empty($joined) ||
474 !is_string($part) ||
475 preg_match('/[\[.:#%]/', $part)
476 ) {
477 $joined[] = $part;
478 continue;
479 }
480
481 if (is_array(end($joined))) {
482 $joined[] = $part;
483 } else {
484 $joined[count($joined) - 1] .= $part;
485 }
486 }
487
488 return $joined;
489 }
490
491 // replaces all the interpolates
492 protected function evalSelector($selector)
493 {
494 return array_map(array($this, 'evalSelectorPart'), $selector);
495 }
496
497 protected function evalSelectorPart($piece)
498 {
499 foreach ($piece as &$p) {
500 if (!is_array($p)) {
501 continue;
502 }
503
504 switch ($p[0]) {
505 case 'interpolate':
506 $p = $this->compileValue($p);
507 break;
508 case 'string':
509 $p = $this->compileValue($p);
510 break;
511 }
512 }
513
514 return $this->flattenSelectorSingle($piece);
515 }
516
517 // compiles to string
518 // self(&) should have been replaced by now
519 protected function compileSelector($selector)
520 {
521 if (!is_array($selector)) {
522 return $selector; // media and the like
523 }
524
525 return implode(
526 ' ',
527 array_map(
528 array($this, 'compileSelectorPart'),
529 $selector
530 )
531 );
532 }
533
534 protected function compileSelectorPart($piece)
535 {
536 foreach ($piece as &$p) {
537 if (!is_array($p)) {
538 continue;
539 }
540
541 switch ($p[0]) {
542 case 'self':
543 $p = '&';
544 break;
545 default:
546 $p = $this->compileValue($p);
547 break;
548 }
549 }
550
551 return implode($piece);
552 }
553
554 protected function hasSelectorPlaceholder($selector)
555 {
556 if (!is_array($selector)) {
557 return false;
558 }
559
560 foreach ($selector as $parts) {
561 foreach ($parts as $part) {
562 if ('%' == $part[0]) {
563 return true;
564 }
565 }
566 }
567
568 return false;
569 }
570
571 protected function compileChildren($stms, $out)
572 {
573 foreach ($stms as $stm) {
574 $ret = $this->compileChild($stm, $out);
575
576 if (isset($ret)) {
577 return $ret;
578 }
579 }
580 }
581
582 protected function compileMediaQuery($queryList)
583 {
584 $out = '@media';
585 $first = true;
586
587 foreach ($queryList as $query) {
588 $type = null;
589 $parts = array();
590
591 foreach ($query as $q) {
592 switch ($q[0]) {
593 case 'mediaType':
594 if ($type) {
595 $type = $this->mergeMediaTypes(
596 $type,
597 array_map(array($this, 'compileValue'), array_slice($q, 1))
598 );
599
600 if (empty($type)) { // merge failed
601 return null;
602 }
603 } else {
604 $type = array_map(array($this, 'compileValue'), array_slice($q, 1));
605 }
606 break;
607 case 'mediaExp':
608 if (isset($q[2])) {
609 $parts[] = '('
610 . $this->compileValue($q[1])
611 . $this->formatter->assignSeparator
612 . $this->compileValue($q[2])
613 . ')';
614 } else {
615 $parts[] = '('
616 . $this->compileValue($q[1])
617 . ')';
618 }
619 break;
620 }
621 }
622
623 if ($type) {
624 array_unshift($parts, implode(' ', array_filter($type)));
625 }
626
627 if (!empty($parts)) {
628 if ($first) {
629 $first = false;
630 $out .= ' ';
631 } else {
632 $out .= $this->formatter->tagSeparator;
633 }
634
635 $out .= implode(' and ', $parts);
636 }
637 }
638
639 return $out;
640 }
641
642 protected function mergeMediaTypes($type1, $type2)
643 {
644 if (empty($type1)) {
645 return $type2;
646 }
647
648 if (empty($type2)) {
649 return $type1;
650 }
651
652 $m1 = '';
653 $t1 = '';
654
655 if (count($type1) > 1) {
656 $m1= strtolower($type1[0]);
657 $t1= strtolower($type1[1]);
658 } else {
659 $t1 = strtolower($type1[0]);
660 }
661
662 $m2 = '';
663 $t2 = '';
664
665 if (count($type2) > 1) {
666 $m2 = strtolower($type2[0]);
667 $t2 = strtolower($type2[1]);
668 } else {
669 $t2 = strtolower($type2[0]);
670 }
671
672 if (($m1 == 'not') ^ ($m2 == 'not')) {
673 if ($t1 == $t2) {
674 return null;
675 }
676
677 return array(
678 $m1 == 'not' ? $m2 : $m1,
679 $m1 == 'not' ? $t2 : $t1
680 );
681 }
682
683 if ($m1 == 'not' && $m2 == 'not') {
684 # CSS has no way of representing "neither screen nor print"
685 if ($t1 != $t2) {
686 return null;
687 }
688
689 return array('not', $t1);
690 }
691
692 if ($t1 != $t2) {
693 return null;
694 }
695
696 // t1 == t2, neither m1 nor m2 are "not"
697 return array(empty($m1)? $m2 : $m1, $t1);
698 }
699
700 // returns true if the value was something that could be imported
701 protected function compileImport($rawPath, $out)
702 {
703 if ($rawPath[0] == 'string') {
704 $path = $this->compileStringContent($rawPath);
705
706 if ($path = $this->findImport($path)) {
707 $this->importFile($path, $out);
708
709 return true;
710 }
711
712 return false;
713 }
714 if ($rawPath[0] == 'list') {
715 // handle a list of strings
716 if (count($rawPath[2]) == 0) {
717 return false;
718 }
719
720 foreach ($rawPath[2] as $path) {
721 if ($path[0] != 'string') {
722 return false;
723 }
724 }
725
726 foreach ($rawPath[2] as $path) {
727 $this->compileImport($path, $out);
728 }
729
730 return true;
731 }
732
733 return false;
734 }
735
736 // return a value to halt execution
737 protected function compileChild($child, $out)
738 {
739 $this->sourcePos = isset($child[-1]) ? $child[-1] : -1;
740 $this->sourceParser = isset($child[-2]) ? $child[-2] : $this->parser;
741
742 switch ($child[0]) {
743 case 'import':
744 list(,$rawPath) = $child;
745
746 $rawPath = $this->reduce($rawPath);
747 if (!$this->compileImport($rawPath, $out)) {
748 $out->lines[] = '@import ' . $this->compileValue($rawPath) . ';';
749 }
750 break;
751 case 'directive':
752 list(, $directive) = $child;
753
754 $s = '@' . $directive->name;
755 if (!empty($directive->value)) {
756 $s .= ' ' . $this->compileValue($directive->value);
757 }
758 $this->compileNestedBlock($directive, array($s));
759 break;
760 case 'media':
761 $this->compileMedia($child[1]);
762 break;
763 case 'block':
764 $this->compileBlock($child[1]);
765 break;
766 case 'charset':
767 $out->lines[] = '@charset '.$this->compileValue($child[1]).';';
768 break;
769 case 'assign':
770 list(,$name, $value) = $child;
771
772 if ($name[0] == 'var') {
773 $isDefault = !empty($child[3]);
774
775 if ($isDefault) {
776 $existingValue = $this->get($name[1], true);
777 $shouldSet = $existingValue === true || $existingValue == self::$null;
778 }
779
780 if (! $isDefault || $shouldSet) {
781 $this->set($name[1], $this->reduce($value));
782 }
783 break;
784 }
785
786 // if the value reduces to null from something else then
787 // the property should be discarded
788 if ($value[0] != 'null') {
789 $value = $this->reduce($value);
790 if ($value[0] == 'null') {
791 break;
792 }
793 }
794
795 $compiledValue = $this->compileValue($value);
796 $out->lines[] = $this->formatter->property(
797 $this->compileValue($name),
798 $compiledValue
799 );
800 break;
801 case 'comment':
802 if ($out->type == 'root') {
803 $this->compileComment($child);
804 break;
805 }
806
807 $out->lines[] = $child[1];
808 break;
809 case 'mixin':
810 case 'function':
811 list(,$block) = $child;
812
813 $this->set(self::$namespaces[$block->type] . $block->name, $block);
814 break;
815 case 'extend':
816 list(, $selectors) = $child;
817
818 foreach ($selectors as $sel) {
819 // only use the first one
820 $sel = current($this->evalSelector($sel));
821 $this->pushExtends($sel, $out->selectors);
822 }
823 break;
824 case 'if':
825 list(, $if) = $child;
826
827 if ($this->isTruthy($this->reduce($if->cond, true))) {
828 return $this->compileChildren($if->children, $out);
829 } else {
830 foreach ($if->cases as $case) {
831 if ($case->type == 'else' ||
832 $case->type == 'elseif' && $this->isTruthy($this->reduce($case->cond))
833 ) {
834 return $this->compileChildren($case->children, $out);
835 }
836 }
837 }
838 break;
839 case 'return':
840 return $this->reduce($child[1], true);
841 case 'each':
842 list(,$each) = $child;
843
844 $list = $this->coerceList($this->reduce($each->list));
845 foreach ($list[2] as $item) {
846 $this->pushEnv();
847 $this->set($each->var, $item);
848 // TODO: allow return from here
849 $this->compileChildren($each->children, $out);
850 $this->popEnv();
851 }
852 break;
853 case 'while':
854 list(,$while) = $child;
855
856 while ($this->isTruthy($this->reduce($while->cond, true))) {
857 $ret = $this->compileChildren($while->children, $out);
858 if ($ret) {
859 return $ret;
860 }
861 }
862 break;
863 case 'for':
864 list(,$for) = $child;
865
866 $start = $this->reduce($for->start, true);
867 $start = $start[1];
868 $end = $this->reduce($for->end, true);
869 $end = $end[1];
870 $d = $start < $end ? 1 : -1;
871
872 while (true) {
873 if ((! $for->until && $start - $d == $end) ||
874 ($for->until && $start == $end)
875 ) {
876 break;
877 }
878
879 $this->set($for->var, array('number', $start, ''));
880 $start += $d;
881
882 $ret = $this->compileChildren($for->children, $out);
883
884 if ($ret) {
885 return $ret;
886 }
887 }
888
889 break;
890 case 'nestedprop':
891 list(,$prop) = $child;
892
893 $prefixed = array();
894 $prefix = $this->compileValue($prop->prefix) . '-';
895
896 foreach ($prop->children as $child) {
897 if ($child[0] == 'assign') {
898 array_unshift($child[1][2], $prefix);
899 }
900
901 if ($child[0] == 'nestedprop') {
902 array_unshift($child[1]->prefix[2], $prefix);
903 }
904
905 $prefixed[] = $child;
906 }
907
908 $this->compileChildren($prefixed, $out);
909 break;
910 case 'include': // including a mixin
911 list(,$name, $argValues, $content) = $child;
912
913 $mixin = $this->get(self::$namespaces['mixin'] . $name, false);
914
915 if (! $mixin) {
916 $this->throwError("Undefined mixin $name");
917 }
918
919 $callingScope = $this->env;
920
921 // push scope, apply args
922 $this->pushEnv();
923
924 if ($this->env->depth > 0) {
925 $this->env->depth--;
926 }
927
928 if (isset($content)) {
929 $content->scope = $callingScope;
930 $this->setRaw(self::$namespaces['special'] . 'content', $content);
931 }
932
933 if (isset($mixin->args)) {
934 $this->applyArguments($mixin->args, $argValues);
935 }
936
937 foreach ($mixin->children as $child) {
938 $this->compileChild($child, $out);
939 }
940
941 $this->popEnv();
942 break;
943 case 'mixin_content':
944 $content = $this->get(self::$namespaces['special'] . 'content');
945
946 if (!isset($content)) {
947 $this->throwError('Expected @content inside of mixin');
948 }
949
950 $strongTypes = array('include', 'block', 'for', 'while');
951
952 foreach ($content->children as $child) {
953 $this->storeEnv = (in_array($child[0], $strongTypes))
954 ? null
955 : $content->scope;
956
957 $this->compileChild($child, $out);
958 }
959
960 unset($this->storeEnv);
961 break;
962 case 'debug':
963 list(,$value, $pos) = $child;
964
965 $line = $this->parser->getLineNo($pos);
966 $value = $this->compileValue($this->reduce($value, true));
967 fwrite(STDERR, "Line $line DEBUG: $value\n");
968 break;
969 default:
970 $this->throwError("unknown child type: $child[0]");
971 }
972 }
973
974 protected function expToString($exp)
975 {
976 list(, $op, $left, $right, $inParens, $whiteLeft, $whiteRight) = $exp;
977
978 $content = array($this->reduce($left));
979
980 if ($whiteLeft) {
981 $content[] = ' ';
982 }
983
984 $content[] = $op;
985
986 if ($whiteRight) {
987 $content[] = ' ';
988 }
989
990 $content[] = $this->reduce($right);
991
992 return array('string', '', $content);
993 }
994
995 protected function isTruthy($value)
996 {
997 return $value != self::$false && $value != self::$null;
998 }
999
1000 // should $value cause its operand to eval
1001 protected function shouldEval($value)
1002 {
1003 switch ($value[0]) {
1004 case 'exp':
1005 if ($value[1] == '/') {
1006 return $this->shouldEval($value[2], $value[3]);
1007 }
1008
1009 // fall-thru
1010 case 'var':
1011 case 'fncall':
1012 return true;
1013 }
1014 return false;
1015 }
1016
1017 protected function reduce($value, $inExp = false)
1018 {
1019 list($type) = $value;
1020 switch ($type) {
1021 case 'exp':
1022 list(, $op, $left, $right, $inParens) = $value;
1023
1024 $opName = isset(self::$operatorNames[$op]) ? self::$operatorNames[$op] : $op;
1025 $inExp = $inExp || $this->shouldEval($left) || $this->shouldEval($right);
1026
1027 $left = $this->reduce($left, true);
1028 $right = $this->reduce($right, true);
1029
1030 // only do division in special cases
1031 if ($opName == 'div' && !$inParens && !$inExp) {
1032 if ($left[0] != 'color' && $right[0] != 'color') {
1033 return $this->expToString($value);
1034 }
1035 }
1036
1037 $left = $this->coerceForExpression($left);
1038 $right = $this->coerceForExpression($right);
1039
1040 $ltype = $left[0];
1041 $rtype = $right[0];
1042
1043 $ucOpName = ucfirst($opName);
1044 $ucLType = ucfirst($ltype);
1045 $ucRType = ucfirst($rtype);
1046
1047 // this tries:
1048 // 1. op[op name][left type][right type]
1049 // 2. op[left type][right type] (passing the op as first arg
1050 // 3. op[op name]
1051 $fn = "op${ucOpName}${ucLType}${ucRType}";
1052 if (is_callable(array($this, $fn)) ||
1053 (($fn = "op${ucLType}${ucRType}") &&
1054 is_callable(array($this, $fn)) &&
1055 $passOp = true) ||
1056 (($fn = "op${ucOpName}") &&
1057 is_callable(array($this, $fn)) &&
1058 $genOp = true)
1059 ) {
1060 $unitChange = false;
1061
1062 if (!isset($genOp) &&
1063 $left[0] == 'number' && $right[0] == 'number'
1064 ) {
1065 if ($opName == 'mod' && $right[2] != '') {
1066 $this->throwError("Cannot modulo by a number with units: $right[1]$right[2].");
1067 }
1068
1069 $unitChange = true;
1070 $emptyUnit = $left[2] == '' || $right[2] == '';
1071 $targetUnit = '' != $left[2] ? $left[2] : $right[2];
1072
1073 if ($opName != 'mul') {
1074 $left[2] = '' != $left[2] ? $left[2] : $targetUnit;
1075 $right[2] = '' != $right[2] ? $right[2] : $targetUnit;
1076 }
1077
1078 if ($opName != 'mod') {
1079 $left = $this->normalizeNumber($left);
1080 $right = $this->normalizeNumber($right);
1081 }
1082
1083 if ($opName == 'div' && !$emptyUnit && $left[2] == $right[2]) {
1084 $targetUnit = '';
1085 }
1086
1087 if ($opName == 'mul') {
1088 $left[2] = '' != $left[2] ? $left[2] : $right[2];
1089 $right[2] = '' != $right[2] ? $right[2] : $left[2];
1090 } elseif ($opName == 'div' && $left[2] == $right[2]) {
1091 $left[2] = '';
1092 $right[2] = '';
1093 }
1094 }
1095
1096 $shouldEval = $inParens || $inExp;
1097
1098 if (isset($passOp)) {
1099 $out = $this->$fn($op, $left, $right, $shouldEval);
1100 } else {
1101 $out = $this->$fn($left, $right, $shouldEval);
1102 }
1103
1104 if (isset($out)) {
1105 if ($unitChange && $out[0] == 'number') {
1106 $out = $this->coerceUnit($out, $targetUnit);
1107 }
1108
1109 return $out;
1110 }
1111 }
1112
1113 return $this->expToString($value);
1114 case 'unary':
1115 list(, $op, $exp, $inParens) = $value;
1116
1117 $inExp = $inExp || $this->shouldEval($exp);
1118 $exp = $this->reduce($exp);
1119
1120 if ($exp[0] == 'number') {
1121 switch ($op) {
1122 case '+':
1123 return $exp;
1124 case '-':
1125 $exp[1] *= -1;
1126
1127 return $exp;
1128 }
1129 }
1130
1131 if ($op == 'not') {
1132 if ($inExp || $inParens) {
1133 if ($exp == self::$false) {
1134 return self::$true;
1135 }
1136
1137 return self::$false;
1138 } else {
1139 $op = $op . ' ';
1140 }
1141 }
1142
1143 return array('string', '', array($op, $exp));
1144 case 'var':
1145 list(, $name) = $value;
1146
1147 return $this->reduce($this->get($name));
1148 case 'list':
1149 foreach ($value[2] as &$item) {
1150 $item = $this->reduce($item);
1151 }
1152
1153 return $value;
1154 case 'string':
1155 foreach ($value[2] as &$item) {
1156 if (is_array($item)) {
1157 $item = $this->reduce($item);
1158 }
1159 }
1160 return $value;
1161 case 'interpolate':
1162 $value[1] = $this->reduce($value[1]);
1163
1164 return $value;
1165 case 'fncall':
1166 list(,$name, $argValues) = $value;
1167
1168 // user defined function?
1169 $func = $this->get(self::$namespaces['function'] . $name, false);
1170
1171 if ($func) {
1172 $this->pushEnv();
1173
1174 // set the args
1175 if (isset($func->args)) {
1176 $this->applyArguments($func->args, $argValues);
1177 }
1178
1179 // throw away lines and children
1180 $tmp = (object)array(
1181 'lines' => array(),
1182 'children' => array()
1183 );
1184
1185 $ret = $this->compileChildren($func->children, $tmp);
1186
1187 $this->popEnv();
1188
1189 return !isset($ret) ? self::$defaultValue : $ret;
1190 }
1191
1192 // built in function
1193 if ($this->callBuiltin($name, $argValues, $returnValue)) {
1194 return $returnValue;
1195 }
1196
1197 // need to flatten the arguments into a list
1198 $listArgs = array();
1199
1200 foreach ((array)$argValues as $arg) {
1201 if (empty($arg[0])) {
1202 $listArgs[] = $this->reduce($arg[1]);
1203 }
1204 }
1205
1206 return array('function', $name, array('list', ',', $listArgs));
1207 default:
1208 return $value;
1209 }
1210 }
1211
1212 public function normalizeValue($value)
1213 {
1214 $value = $this->coerceForExpression($this->reduce($value));
1215 list($type) = $value;
1216
1217 switch ($type) {
1218 case 'list':
1219 $value = $this->extractInterpolation($value);
1220
1221 if ($value[0] != 'list') {
1222 return array('keyword', $this->compileValue($value));
1223 }
1224
1225 foreach ($value[2] as $key => $item) {
1226 $value[2][$key] = $this->normalizeValue($item);
1227 }
1228
1229 return $value;
1230
1231 case 'number':
1232 return $this->normalizeNumber($value);
1233
1234 default:
1235 return $value;
1236 }
1237 }
1238
1239 // just does physical lengths for now
1240 protected function normalizeNumber($number)
1241 {
1242 list(, $value, $unit) = $number;
1243
1244 if (isset(self::$unitTable['in'][$unit])) {
1245 $conv = self::$unitTable['in'][$unit];
1246
1247 return array('number', $value / $conv, 'in');
1248 }
1249
1250 return $number;
1251 }
1252
1253 // $number should be normalized
1254 protected function coerceUnit($number, $unit)
1255 {
1256 list(, $value, $baseUnit) = $number;
1257
1258 if (isset(self::$unitTable[$baseUnit][$unit])) {
1259 $value = $value * self::$unitTable[$baseUnit][$unit];
1260 }
1261
1262 return array('number', $value, $unit);
1263 }
1264
1265 protected function opAddNumberNumber($left, $right)
1266 {
1267 return array('number', $left[1] + $right[1], $left[2]);
1268 }
1269
1270 protected function opMulNumberNumber($left, $right)
1271 {
1272 return array('number', $left[1] * $right[1], $left[2]);
1273 }
1274
1275 protected function opSubNumberNumber($left, $right)
1276 {
1277 return array('number', $left[1] - $right[1], $left[2]);
1278 }
1279
1280 protected function opDivNumberNumber($left, $right)
1281 {
1282 return array('number', $left[1] / $right[1], $left[2]);
1283 }
1284
1285 protected function opModNumberNumber($left, $right)
1286 {
1287 return array('number', $left[1] % $right[1], $left[2]);
1288 }
1289
1290 // adding strings
1291 protected function opAdd($left, $right)
1292 {
1293 if ($strLeft = $this->coerceString($left)) {
1294 if ($right[0] == 'string') {
1295 $right[1] = '';
1296 }
1297
1298 $strLeft[2][] = $right;
1299
1300 return $strLeft;
1301 }
1302
1303 if ($strRight = $this->coerceString($right)) {
1304 if ($left[0] == 'string') {
1305 $left[1] = '';
1306 }
1307
1308 array_unshift($strRight[2], $left);
1309
1310 return $strRight;
1311 }
1312 }
1313
1314 protected function opAnd($left, $right, $shouldEval)
1315 {
1316 if (!$shouldEval) {
1317 return;
1318 }
1319
1320 if ($left != self::$false) {
1321 return $right;
1322 }
1323
1324 return $left;
1325 }
1326
1327 protected function opOr($left, $right, $shouldEval)
1328 {
1329 if (!$shouldEval) {
1330 return;
1331 }
1332
1333 if ($left != self::$false) {
1334 return $left;
1335 }
1336
1337 return $right;
1338 }
1339
1340 protected function opColorColor($op, $left, $right)
1341 {
1342 $out = array('color');
1343
1344 foreach (range(1, 3) as $i) {
1345 $lval = isset($left[$i]) ? $left[$i] : 0;
1346 $rval = isset($right[$i]) ? $right[$i] : 0;
1347
1348 switch ($op) {
1349 case '+':
1350 $out[] = $lval + $rval;
1351 break;
1352 case '-':
1353 $out[] = $lval - $rval;
1354 break;
1355 case '*':
1356 $out[] = $lval * $rval;
1357 break;
1358 case '%':
1359 $out[] = $lval % $rval;
1360 break;
1361 case '/':
1362 if ($rval == 0) {
1363 $this->throwError("color: Can't divide by zero");
1364 }
1365 $out[] = $lval / $rval;
1366 break;
1367 case '==':
1368 return $this->opEq($left, $right);
1369 case '!=':
1370 return $this->opNeq($left, $right);
1371 default:
1372 $this->throwError("color: unknown op $op");
1373 }
1374 }
1375
1376 if (isset($left[4])) {
1377 $out[4] = $left[4];
1378 } elseif (isset($right[4])) {
1379 $out[4] = $right[4];
1380 }
1381
1382 return $this->fixColor($out);
1383 }
1384
1385 protected function opColorNumber($op, $left, $right)
1386 {
1387 $value = $right[1];
1388
1389 return $this->opColorColor(
1390 $op,
1391 $left,
1392 array('color', $value, $value, $value)
1393 );
1394 }
1395
1396 protected function opNumberColor($op, $left, $right)
1397 {
1398 $value = $left[1];
1399
1400 return $this->opColorColor(
1401 $op,
1402 array('color', $value, $value, $value),
1403 $right
1404 );
1405 }
1406
1407 protected function opEq($left, $right)
1408 {
1409 if (($lStr = $this->coerceString($left)) && ($rStr = $this->coerceString($right))) {
1410 $lStr[1] = '';
1411 $rStr[1] = '';
1412
1413 return $this->toBool($this->compileValue($lStr) == $this->compileValue($rStr));
1414 }
1415
1416 return $this->toBool($left == $right);
1417 }
1418
1419 protected function opNeq($left, $right)
1420 {
1421 return $this->toBool($left != $right);
1422 }
1423
1424 protected function opGteNumberNumber($left, $right)
1425 {
1426 return $this->toBool($left[1] >= $right[1]);
1427 }
1428
1429 protected function opGtNumberNumber($left, $right)
1430 {
1431 return $this->toBool($left[1] > $right[1]);
1432 }
1433
1434 protected function opLteNumberNumber($left, $right)
1435 {
1436 return $this->toBool($left[1] <= $right[1]);
1437 }
1438
1439 protected function opLtNumberNumber($left, $right)
1440 {
1441 return $this->toBool($left[1] < $right[1]);
1442 }
1443
1444 public function toBool($thing)
1445 {
1446 return $thing ? self::$true : self::$false;
1447 }
1448
1449 /**
1450 * Compiles a primitive value into a CSS property value.
1451 *
1452 * Values in scssphp are typed by being wrapped in arrays, their format is
1453 * typically:
1454 *
1455 * array(type, contents [, additional_contents]*)
1456 *
1457 * The input is expected to be reduced. This function will not work on
1458 * things like expressions and variables.
1459 *
1460 * @param array $value
1461 */
1462 protected function compileValue($value)
1463 {
1464 $value = $this->reduce($value);
1465
1466 list($type) = $value;
1467
1468 switch ($type) {
1469 case 'keyword':
1470 return $value[1];
1471 case 'color':
1472 // [1] - red component (either number for a %)
1473 // [2] - green component
1474 // [3] - blue component
1475 // [4] - optional alpha component
1476 list(, $r, $g, $b) = $value;
1477
1478 $r = round($r);
1479 $g = round($g);
1480 $b = round($b);
1481
1482 if (count($value) == 5 && $value[4] != 1) { // rgba
1483 return 'rgba('.$r.', '.$g.', '.$b.', '.$value[4].')';
1484 }
1485
1486 $h = sprintf('#%02x%02x%02x', $r, $g, $b);
1487
1488 // Converting hex color to short notation (e.g. #003399 to #039)
1489 if ($h[1] === $h[2] && $h[3] === $h[4] && $h[5] === $h[6]) {
1490 $h = '#' . $h[1] . $h[3] . $h[5];
1491 }
1492
1493 return $h;
1494 case 'number':
1495 return round($value[1], $this->numberPrecision) . $value[2];
1496 case 'string':
1497 return $value[1] . $this->compileStringContent($value) . $value[1];
1498 case 'function':
1499 $args = !empty($value[2]) ? $this->compileValue($value[2]) : '';
1500 return "$value[1]($args)";
1501 case 'list':
1502 $value = $this->extractInterpolation($value);
1503
1504 if ($value[0] != 'list') {
1505 return $this->compileValue($value);
1506 }
1507
1508 list(, $delim, $items) = $value;
1509
1510 $filtered = array();
1511 foreach ($items as $item) {
1512 if ($item[0] == 'null') {
1513 continue;
1514 }
1515
1516 $filtered[] = $this->compileValue($item);
1517 }
1518
1519 return implode("$delim ", $filtered);
1520 case 'interpolated': # node created by extractInterpolation
1521 list(, $interpolate, $left, $right) = $value;
1522 list(,, $whiteLeft, $whiteRight) = $interpolate;
1523
1524 $left = count($left[2]) > 0 ?
1525 $this->compileValue($left).$whiteLeft : '';
1526
1527 $right = count($right[2]) > 0 ?
1528 $whiteRight.$this->compileValue($right) : '';
1529
1530 return $left.$this->compileValue($interpolate).$right;
1531
1532 case 'interpolate': # raw parse node
1533 list(, $exp) = $value;
1534
1535 // strip quotes if it's a string
1536 $reduced = $this->reduce($exp);
1537 switch ($reduced[0]) {
1538 case 'string':
1539 $reduced = array('keyword',
1540 $this->compileStringContent($reduced));
1541 break;
1542 case 'null':
1543 $reduced = array('keyword', '');
1544 }
1545
1546 return $this->compileValue($reduced);
1547 case 'null':
1548 return 'null';
1549 default:
1550 $this->throwError("unknown value type: $type");
1551 }
1552 }
1553
1554 protected function compileStringContent($string)
1555 {
1556 $parts = array();
1557
1558 foreach ($string[2] as $part) {
1559 if (is_array($part)) {
1560 $parts[] = $this->compileValue($part);
1561 } else {
1562 $parts[] = $part;
1563 }
1564 }
1565
1566 return implode($parts);
1567 }
1568
1569 // doesn't need to be recursive, compileValue will handle that
1570 protected function extractInterpolation($list)
1571 {
1572 $items = $list[2];
1573
1574 foreach ($items as $i => $item) {
1575 if ($item[0] == 'interpolate') {
1576 $before = array('list', $list[1], array_slice($items, 0, $i));
1577 $after = array('list', $list[1], array_slice($items, $i + 1));
1578
1579 return array('interpolated', $item, $before, $after);
1580 }
1581 }
1582
1583 return $list;
1584 }
1585
1586 // find the final set of selectors
1587 protected function multiplySelectors($env)
1588 {
1589 $envs = array();
1590
1591 while (null !== $env) {
1592 if (!empty($env->selectors)) {
1593 $envs[] = $env;
1594 }
1595
1596 $env = $env->parent;
1597 };
1598
1599 $selectors = array();
1600 $parentSelectors = array(array());
1601
1602 while ($env = array_pop($envs)) {
1603 $selectors = array();
1604
1605 foreach ($env->selectors as $selector) {
1606 foreach ($parentSelectors as $parent) {
1607 $selectors[] = $this->joinSelectors($parent, $selector);
1608 }
1609 }
1610
1611 $parentSelectors = $selectors;
1612 }
1613
1614 return $selectors;
1615 }
1616
1617 // looks for & to replace, or append parent before child
1618 protected function joinSelectors($parent, $child)
1619 {
1620 $setSelf = false;
1621 $out = array();
1622
1623 foreach ($child as $part) {
1624 $newPart = array();
1625
1626 foreach ($part as $p) {
1627 if ($p == self::$selfSelector) {
1628 $setSelf = true;
1629
1630 foreach ($parent as $i => $parentPart) {
1631 if ($i > 0) {
1632 $out[] = $newPart;
1633 $newPart = array();
1634 }
1635
1636 foreach ($parentPart as $pp) {
1637 $newPart[] = $pp;
1638 }
1639 }
1640 } else {
1641 $newPart[] = $p;
1642 }
1643 }
1644
1645 $out[] = $newPart;
1646 }
1647
1648 return $setSelf ? $out : array_merge($parent, $child);
1649 }
1650
1651 protected function multiplyMedia($env, $childQueries = null)
1652 {
1653 if (!isset($env) ||
1654 !empty($env->block->type) && $env->block->type != 'media'
1655 ) {
1656 return $childQueries;
1657 }
1658
1659 // plain old block, skip
1660 if (empty($env->block->type)) {
1661 return $this->multiplyMedia($env->parent, $childQueries);
1662 }
1663
1664 $parentQueries = $env->block->queryList;
1665 if ($childQueries == null) {
1666 $childQueries = $parentQueries;
1667 } else {
1668 $originalQueries = $childQueries;
1669 $childQueries = array();
1670
1671 foreach ($parentQueries as $parentQuery) {
1672 foreach ($originalQueries as $childQuery) {
1673 $childQueries []= array_merge($parentQuery, $childQuery);
1674 }
1675 }
1676 }
1677
1678 return $this->multiplyMedia($env->parent, $childQueries);
1679 }
1680
1681 // convert something to list
1682 protected function coerceList($item, $delim = ',')
1683 {
1684 if (isset($item) && $item[0] == 'list') {
1685 return $item;
1686 }
1687
1688 return array('list', $delim, !isset($item) ? array(): array($item));
1689 }
1690
1691 protected function applyArguments($argDef, $argValues)
1692 {
1693 $storeEnv = $this->getStoreEnv();
1694
1695 $env = new \stdClass;
1696 $env->store = $storeEnv->store;
1697
1698 $hasVariable = false;
1699 $args = array();
1700
1701 foreach ($argDef as $i => $arg) {
1702 list($name, $default, $isVariable) = $argDef[$i];
1703
1704 $args[$name] = array($i, $name, $default, $isVariable);
1705 $hasVariable |= $isVariable;
1706 }
1707
1708 $keywordArgs = array();
1709 $deferredKeywordArgs = array();
1710 $remaining = array();
1711
1712 // assign the keyword args
1713 foreach ((array) $argValues as $arg) {
1714 if (!empty($arg[0])) {
1715 if (!isset($args[$arg[0][1]])) {
1716 if ($hasVariable) {
1717 $deferredKeywordArgs[$arg[0][1]] = $arg[1];
1718 } else {
1719 $this->throwError("Mixin or function doesn't have an argument named $%s.", $arg[0][1]);
1720 }
1721 } elseif ($args[$arg[0][1]][0] < count($remaining)) {
1722 $this->throwError("The argument $%s was passed both by position and by name.", $arg[0][1]);
1723 } else {
1724 $keywordArgs[$arg[0][1]] = $arg[1];
1725 }
1726 } elseif (count($keywordArgs)) {
1727 $this->throwError('Positional arguments must come before keyword arguments.');
1728 } elseif ($arg[2] == true) {
1729 $val = $this->reduce($arg[1], true);
1730
1731 if ($val[0] == 'list') {
1732 foreach ($val[2] as $name => $item) {
1733 if (!is_numeric($name)) {
1734 $keywordArgs[$name] = $item;
1735 } else {
1736 $remaining[] = $item;
1737 }
1738 }
1739 } else {
1740 $remaining[] = $val;
1741 }
1742 } else {
1743 $remaining[] = $arg[1];
1744 }
1745 }
1746
1747 foreach ($args as $arg) {
1748 list($i, $name, $default, $isVariable) = $arg;
1749
1750 if ($isVariable) {
1751 $val = array('list', ',', array());
1752 for ($count = count($remaining); $i < $count; $i++) {
1753 $val[2][] = $remaining[$i];
1754 }
1755 foreach ($deferredKeywordArgs as $itemName => $item) {
1756 $val[2][$itemName] = $item;
1757 }
1758 } elseif (isset($remaining[$i])) {
1759 $val = $remaining[$i];
1760 } elseif (isset($keywordArgs[$name])) {
1761 $val = $keywordArgs[$name];
1762 } elseif (!empty($default)) {
1763 continue;
1764 } else {
1765 $this->throwError("Missing argument $name");
1766 }
1767
1768 $this->set($name, $this->reduce($val, true), true, $env);
1769 }
1770
1771 $storeEnv->store = $env->store;
1772
1773 foreach ($args as $arg) {
1774 list($i, $name, $default, $isVariable) = $arg;
1775
1776 if ($isVariable || isset($remaining[$i]) || isset($keywordArgs[$name]) || empty($default)) {
1777 continue;
1778 }
1779
1780 $this->set($name, $this->reduce($default, true), true);
1781 }
1782 }
1783
1784 protected function pushEnv($block = null)
1785 {
1786 $env = new \stdClass;
1787 $env->parent = $this->env;
1788 $env->store = array();
1789 $env->block = $block;
1790 $env->depth = isset($this->env->depth) ? $this->env->depth + 1 : 0;
1791
1792 $this->env = $env;
1793
1794 return $env;
1795 }
1796
1797 protected function normalizeName($name)
1798 {
1799 return str_replace('-', '_', $name);
1800 }
1801
1802 protected function getStoreEnv()
1803 {
1804 return isset($this->storeEnv) ? $this->storeEnv : $this->env;
1805 }
1806
1807 protected function set($name, $value, $shadow = false, $env = null)
1808 {
1809 $name = $this->normalizeName($name);
1810
1811 if ($shadow) {
1812 $this->setRaw($name, $value, $env);
1813 } else {
1814 $this->setExisting($name, $value, $env);
1815 }
1816 }
1817
1818 protected function setExisting($name, $value, $env = null)
1819 {
1820 if (!isset($env)) {
1821 $env = $this->getStoreEnv();
1822 }
1823
1824 if (isset($env->store[$name]) || !isset($env->parent)) {
1825 $env->store[$name] = $value;
1826 } else {
1827 $this->setExisting($name, $value, $env->parent);
1828 }
1829 }
1830
1831 protected function setRaw($name, $value, $env = null)
1832 {
1833 if (!isset($env)) {
1834 $env = $this->getStoreEnv();
1835 }
1836
1837 $env->store[$name] = $value;
1838 }
1839
1840 public function get($name, $defaultValue = null, $env = null)
1841 {
1842 $name = $this->normalizeName($name);
1843
1844 if (!isset($env)) {
1845 $env = $this->getStoreEnv();
1846 }
1847
1848 if (! isset($defaultValue)) {
1849 $defaultValue = self::$defaultValue;
1850 }
1851
1852 if (isset($env->store[$name])) {
1853 return $env->store[$name];
1854 }
1855
1856 if (isset($env->parent)) {
1857 return $this->get($name, $defaultValue, $env->parent);
1858 }
1859
1860 return $defaultValue; // found nothing
1861 }
1862
1863 protected function injectVariables(array $args)
1864 {
1865 if (empty($args)) {
1866 return;
1867 }
1868
1869 $parser = new Parser(__METHOD__, false);
1870
1871 foreach ($args as $name => $strValue) {
1872 if ($name[0] === '$') {
1873 $name = substr($name, 1);
1874 }
1875
1876 $parser->env = null;
1877 $parser->count = 0;
1878 $parser->buffer = (string) $strValue;
1879 $parser->inParens = false;
1880 $parser->eatWhiteDefault = true;
1881 $parser->insertComments = true;
1882
1883 if (! $parser->valueList($value)) {
1884 throw new \Exception("failed to parse passed in variable $name: $strValue");
1885 }
1886
1887 $this->set($name, $value);
1888 }
1889 }
1890
1891 /**
1892 * Set variables
1893 *
1894 * @param array $variables
1895 */
1896 public function setVariables(array $variables)
1897 {
1898 $this->registeredVars = array_merge($this->registeredVars, $variables);
1899 }
1900
1901 /**
1902 * Unset variable
1903 *
1904 * @param string $name
1905 */
1906 public function unsetVariable($name)
1907 {
1908 unset($this->registeredVars[$name]);
1909 }
1910
1911 protected function popEnv()
1912 {
1913 $env = $this->env;
1914 $this->env = $this->env->parent;
1915
1916 return $env;
1917 }
1918
1919 public function getParsedFiles()
1920 {
1921 return $this->parsedFiles;
1922 }
1923
1924 public function addImportPath($path)
1925 {
1926 $this->importPaths[] = $path;
1927 }
1928
1929 public function setImportPaths($path)
1930 {
1931 $this->importPaths = (array)$path;
1932 }
1933
1934 public function setNumberPrecision($numberPrecision)
1935 {
1936 $this->numberPrecision = $numberPrecision;
1937 }
1938
1939 public function setFormatter($formatterName)
1940 {
1941 $this->formatter = $formatterName;
1942 }
1943
1944 public function registerFunction($name, $func)
1945 {
1946 $this->userFunctions[$this->normalizeName($name)] = $func;
1947 }
1948
1949 public function unregisterFunction($name)
1950 {
1951 unset($this->userFunctions[$this->normalizeName($name)]);
1952 }
1953
1954 protected function importFile($path, $out)
1955 {
1956 // see if tree is cached
1957 $realPath = realpath($path);
1958 if (isset($this->importCache[$realPath])) {
1959 $tree = $this->importCache[$realPath];
1960 } else {
1961 $code = file_get_contents($path);
1962 $parser = new Parser($path, false);
1963 $tree = $parser->parse($code);
1964 $this->parsedFiles[] = $path;
1965
1966 $this->importCache[$realPath] = $tree;
1967 }
1968
1969 $pi = pathinfo($path);
1970 array_unshift($this->importPaths, $pi['dirname']);
1971 $this->compileChildren($tree->children, $out);
1972 array_shift($this->importPaths);
1973 }
1974
1975 // results the file path for an import url if it exists
1976 public function findImport($url)
1977 {
1978 $urls = array();
1979
1980 // for "normal" scss imports (ignore vanilla css and external requests)
1981 if (!preg_match('/\.css|^http:\/\/$/', $url)) {
1982 // try both normal and the _partial filename
1983 $urls = array($url, preg_replace('/[^\/]+$/', '_\0', $url));
1984 }
1985
1986 foreach ($this->importPaths as $dir) {
1987 if (is_string($dir)) {
1988 // check urls for normal import paths
1989 foreach ($urls as $full) {
1990 $full = $dir .
1991 (!empty($dir) && substr($dir, -1) != '/' ? '/' : '') .
1992 $full;
1993
1994 if ($this->fileExists($file = $full.'.scss') ||
1995 $this->fileExists($file = $full)
1996 ) {
1997 return $file;
1998 }
1999 }
2000 } else {
2001 // check custom callback for import path
2002 $file = call_user_func($dir, $url, $this);
2003
2004 if ($file !== null) {
2005 return $file;
2006 }
2007 }
2008 }
2009
2010 return null;
2011 }
2012
2013 protected function fileExists($name)
2014 {
2015 return is_file($name);
2016 }
2017
2018 protected function callBuiltin($name, $args, &$returnValue)
2019 {
2020 // try a lib function
2021 $name = $this->normalizeName($name);
2022 $libName = 'lib' . preg_replace_callback(
2023 '/_(.)/',
2024 function ($m) {
2025 return ucfirst($m[1]);
2026 },
2027 ucfirst($name)
2028 );
2029
2030 $f = array($this, $libName);
2031
2032 if (is_callable($f)) {
2033 $prototype = isset(self::$$libName) ? self::$$libName : null;
2034 $sorted = $this->sortArgs($prototype, $args);
2035
2036 foreach ($sorted as &$val) {
2037 $val = $this->reduce($val, true);
2038 }
2039
2040 $returnValue = call_user_func($f, $sorted, $this);
2041 } elseif (isset($this->userFunctions[$name])) {
2042 // see if we can find a user function
2043 $fn = $this->userFunctions[$name];
2044
2045 foreach ($args as &$val) {
2046 $val = $this->reduce($val[1], true);
2047 }
2048
2049 $returnValue = call_user_func($fn, $args, $this);
2050 }
2051
2052 if (isset($returnValue)) {
2053 // coerce a php value into a scss one
2054 if (is_numeric($returnValue)) {
2055 $returnValue = array('number', $returnValue, '');
2056 } elseif (is_bool($returnValue)) {
2057 $returnValue = $returnValue ? self::$true : self::$false;
2058 } elseif (!is_array($returnValue)) {
2059 $returnValue = array('keyword', $returnValue);
2060 }
2061
2062 return true;
2063 }
2064
2065 return false;
2066 }
2067
2068 // sorts any keyword arguments
2069 // TODO: merge with apply arguments
2070 protected function sortArgs($prototype, $args)
2071 {
2072 $keyArgs = array();
2073 $posArgs = array();
2074
2075 foreach ($args as $arg) {
2076 list($key, $value) = $arg;
2077 $key = $key[1];
2078 if (empty($key)) {
2079 $posArgs[] = $value;
2080 } else {
2081 $keyArgs[$key] = $value;
2082 }
2083 }
2084
2085 if (!isset($prototype)) {
2086 return $posArgs;
2087 }
2088
2089 $finalArgs = array();
2090
2091 foreach ($prototype as $i => $names) {
2092 if (isset($posArgs[$i])) {
2093 $finalArgs[] = $posArgs[$i];
2094 continue;
2095 }
2096
2097 $set = false;
2098 foreach ((array)$names as $name) {
2099 if (isset($keyArgs[$name])) {
2100 $finalArgs[] = $keyArgs[$name];
2101 $set = true;
2102 break;
2103 }
2104 }
2105
2106 if (!$set) {
2107 $finalArgs[] = null;
2108 }
2109 }
2110
2111 return $finalArgs;
2112 }
2113
2114 protected function coerceForExpression($value)
2115 {
2116 if ($color = $this->coerceColor($value)) {
2117 return $color;
2118 }
2119
2120 return $value;
2121 }
2122
2123 protected function coerceColor($value)
2124 {
2125 switch ($value[0]) {
2126 case 'color':
2127 return $value;
2128
2129 case 'keyword':
2130 $name = $value[1];
2131
2132 if (isset(Colors::$cssColors[$name])) {
2133 $rgba = explode(',', Colors::$cssColors[$name]);
2134
2135 return isset($rgba[3])
2136 ? array('color', (int) $rgba[0], (int) $rgba[1], (int) $rgba[2], (int) $rgba[3])
2137 : array('color', (int) $rgba[0], (int) $rgba[1], (int) $rgba[2]);
2138 }
2139
2140 return null;
2141 }
2142
2143 return null;
2144 }
2145
2146 protected function coerceString($value)
2147 {
2148 switch ($value[0]) {
2149 case 'string':
2150 return $value;
2151 case 'keyword':
2152 return array('string', '', array($value[1]));
2153 }
2154 return null;
2155 }
2156
2157 public function assertList($value)
2158 {
2159 if ($value[0] != 'list') {
2160 $this->throwError('expecting list');
2161 }
2162
2163 return $value;
2164 }
2165
2166 public function assertColor($value)
2167 {
2168 if ($color = $this->coerceColor($value)) {
2169 return $color;
2170 }
2171
2172 $this->throwError('expecting color');
2173 }
2174
2175 public function assertNumber($value)
2176 {
2177 if ($value[0] != 'number') {
2178 $this->throwError('expecting number');
2179 }
2180
2181 return $value[1];
2182 }
2183
2184 protected function coercePercent($value)
2185 {
2186 if ($value[0] == 'number') {
2187 if ($value[2] == '%') {
2188 return $value[1] / 100;
2189 }
2190
2191 return $value[1];
2192 }
2193
2194 return 0;
2195 }
2196
2197 // make sure a color's components don't go out of bounds
2198 protected function fixColor($c)
2199 {
2200 foreach (range(1, 3) as $i) {
2201 if ($c[$i] < 0) {
2202 $c[$i] = 0;
2203 }
2204
2205 if ($c[$i] > 255) {
2206 $c[$i] = 255;
2207 }
2208 }
2209
2210 return $c;
2211 }
2212
2213 public function toHSL($red, $green, $blue)
2214 {
2215 $min = min($red, $green, $blue);
2216 $max = max($red, $green, $blue);
2217
2218 $l = $min + $max;
2219
2220 if ($min == $max) {
2221 $s = $h = 0;
2222 } else {
2223 $d = $max - $min;
2224
2225 if ($l < 255) {
2226 $s = $d / $l;
2227 } else {
2228 $s = $d / (510 - $l);
2229 }
2230
2231 if ($red == $max) {
2232 $h = 60 * ($green - $blue) / $d;
2233 } elseif ($green == $max) {
2234 $h = 60 * ($blue - $red) / $d + 120;
2235 } elseif ($blue == $max) {
2236 $h = 60 * ($red - $green) / $d + 240;
2237 }
2238 }
2239
2240 return array('hsl', fmod($h, 360), $s * 100, $l / 5.1);
2241 }
2242
2243 public function hueToRGB($m1, $m2, $h)
2244 {
2245 if ($h < 0) {
2246 $h += 1;
2247 } elseif ($h > 1) {
2248 $h -= 1;
2249 }
2250
2251 if ($h * 6 < 1) {
2252 return $m1 + ($m2 - $m1) * $h * 6;
2253 }
2254
2255 if ($h * 2 < 1) {
2256 return $m2;
2257 }
2258
2259 if ($h * 3 < 2) {
2260 return $m1 + ($m2 - $m1) * (2/3 - $h) * 6;
2261 }
2262
2263 return $m1;
2264 }
2265
2266 // H from 0 to 360, S and L from 0 to 100
2267 public function toRGB($hue, $saturation, $lightness)
2268 {
2269 if ($hue < 0) {
2270 $hue += 360;
2271 }
2272
2273 $h = $hue / 360;
2274 $s = min(100, max(0, $saturation)) / 100;
2275 $l = min(100, max(0, $lightness)) / 100;
2276
2277 $m2 = $l <= 0.5 ? $l * ($s + 1) : $l + $s - $l * $s;
2278 $m1 = $l * 2 - $m2;
2279
2280 $r = $this->hueToRGB($m1, $m2, $h + 1/3) * 255;
2281 $g = $this->hueToRGB($m1, $m2, $h) * 255;
2282 $b = $this->hueToRGB($m1, $m2, $h - 1/3) * 255;
2283
2284 $out = array('color', $r, $g, $b);
2285
2286 return $out;
2287 }
2288
2289 // Built in functions
2290
2291 protected static $libIf = array('condition', 'if-true', 'if-false');
2292 protected function libIf($args)
2293 {
2294 list($cond,$t, $f) = $args;
2295
2296 if (!$this->isTruthy($cond)) {
2297 return $f;
2298 }
2299
2300 return $t;
2301 }
2302
2303 protected static $libIndex = array('list', 'value');
2304 protected function libIndex($args)
2305 {
2306 list($list, $value) = $args;
2307
2308 $list = $this->assertList($list);
2309
2310 $values = array();
2311
2312 foreach ($list[2] as $item) {
2313 $values[] = $this->normalizeValue($item);
2314 }
2315
2316 $key = array_search($this->normalizeValue($value), $values);
2317
2318 return false === $key ? false : $key + 1;
2319 }
2320
2321 protected static $libRgb = array('red', 'green', 'blue');
2322 protected function libRgb($args)
2323 {
2324 list($r,$g,$b) = $args;
2325
2326 return array('color', $r[1], $g[1], $b[1]);
2327 }
2328
2329 protected static $libRgba = array(
2330 array('red', 'color'),
2331 'green', 'blue', 'alpha');
2332 protected function libRgba($args)
2333 {
2334 if ($color = $this->coerceColor($args[0])) {
2335 $num = !isset($args[1]) ? $args[3] : $args[1];
2336 $alpha = $this->assertNumber($num);
2337 $color[4] = $alpha;
2338
2339 return $color;
2340 }
2341
2342 list($r,$g,$b, $a) = $args;
2343
2344 return array('color', $r[1], $g[1], $b[1], $a[1]);
2345 }
2346
2347 // helper function for adjust_color, change_color, and scale_color
2348 protected function alterColor($args, $fn)
2349 {
2350 $color = $this->assertColor($args[0]);
2351
2352 foreach (array(1,2,3,7) as $i) {
2353 if (isset($args[$i])) {
2354 $val = $this->assertNumber($args[$i]);
2355 $ii = $i == 7 ? 4 : $i; // alpha
2356 $color[$ii] =
2357 $this->$fn(isset($color[$ii]) ? $color[$ii] : 0, $val, $i);
2358 }
2359 }
2360
2361 if (isset($args[4]) || isset($args[5]) || isset($args[6])) {
2362 $hsl = $this->toHSL($color[1], $color[2], $color[3]);
2363
2364 foreach (array(4,5,6) as $i) {
2365 if (isset($args[$i])) {
2366 $val = $this->assertNumber($args[$i]);
2367 $hsl[$i - 3] = $this->$fn($hsl[$i - 3], $val, $i);
2368 }
2369 }
2370
2371 $rgb = $this->toRGB($hsl[1], $hsl[2], $hsl[3]);
2372
2373 if (isset($color[4])) {
2374 $rgb[4] = $color[4];
2375 }
2376
2377 $color = $rgb;
2378 }
2379
2380 return $color;
2381 }
2382
2383 protected static $libAdjustColor = array(
2384 'color', 'red', 'green', 'blue',
2385 'hue', 'saturation', 'lightness', 'alpha'
2386 );
2387 protected function adjustColorHelper($base, $alter, $i)
2388 {
2389 return $base += $alter;
2390 }
2391 protected function libAdjustColor($args)
2392 {
2393 return $this->alterColor($args, 'adjustColorHelper');
2394 }
2395
2396 protected static $libChangeColor = array(
2397 'color', 'red', 'green', 'blue',
2398 'hue', 'saturation', 'lightness', 'alpha'
2399 );
2400 protected function changeColorHelper($base, $alter, $i)
2401 {
2402 return $alter;
2403 }
2404 protected function libChangeColor($args)
2405 {
2406 return $this->alterColor($args, 'changeColorHelper');
2407 }
2408
2409 protected static $libScaleColor = array(
2410 'color', 'red', 'green', 'blue',
2411 'hue', 'saturation', 'lightness', 'alpha'
2412 );
2413 protected function scaleColorHelper($base, $scale, $i)
2414 {
2415 // 1,2,3 - rgb
2416 // 4, 5, 6 - hsl
2417 // 7 - a
2418 switch ($i) {
2419 case 1:
2420 case 2:
2421 case 3:
2422 $max = 255;
2423 break;
2424 case 4:
2425 $max = 360;
2426 break;
2427 case 7:
2428 $max = 1;
2429 break;
2430 default:
2431 $max = 100;
2432 }
2433
2434 $scale = $scale / 100;
2435
2436 if ($scale < 0) {
2437 return $base * $scale + $base;
2438 }
2439
2440 return ($max - $base) * $scale + $base;
2441 }
2442 protected function libScaleColor($args)
2443 {
2444 return $this->alterColor($args, 'scaleColorHelper');
2445 }
2446
2447 protected static $libIeHexStr = array('color');
2448 protected function libIeHexStr($args)
2449 {
2450 $color = $this->coerceColor($args[0]);
2451 $color[4] = isset($color[4]) ? round(255*$color[4]) : 255;
2452
2453 return sprintf('#%02X%02X%02X%02X', $color[4], $color[1], $color[2], $color[3]);
2454 }
2455
2456 protected static $libRed = array('color');
2457 protected function libRed($args)
2458 {
2459 $color = $this->coerceColor($args[0]);
2460
2461 return $color[1];
2462 }
2463
2464 protected static $libGreen = array('color');
2465 protected function libGreen($args)
2466 {
2467 $color = $this->coerceColor($args[0]);
2468
2469 return $color[2];
2470 }
2471
2472 protected static $libBlue = array('color');
2473 protected function libBlue($args)
2474 {
2475 $color = $this->coerceColor($args[0]);
2476
2477 return $color[3];
2478 }
2479
2480 protected static $libAlpha = array('color');
2481 protected function libAlpha($args)
2482 {
2483 if ($color = $this->coerceColor($args[0])) {
2484 return isset($color[4]) ? $color[4] : 1;
2485 }
2486
2487 // this might be the IE function, so return value unchanged
2488 return null;
2489 }
2490
2491 protected static $libOpacity = array('color');
2492 protected function libOpacity($args)
2493 {
2494 $value = $args[0];
2495
2496 if ($value[0] === 'number') {
2497 return null;
2498 }
2499
2500 return $this->libAlpha($args);
2501 }
2502
2503 // mix two colors
2504 protected static $libMix = array('color-1', 'color-2', 'weight');
2505 protected function libMix($args)
2506 {
2507 list($first, $second, $weight) = $args;
2508 $first = $this->assertColor($first);
2509 $second = $this->assertColor($second);
2510
2511 if (!isset($weight)) {
2512 $weight = 0.5;
2513 } else {
2514 $weight = $this->coercePercent($weight);
2515 }
2516
2517 $firstAlpha = isset($first[4]) ? $first[4] : 1;
2518 $secondAlpha = isset($second[4]) ? $second[4] : 1;
2519
2520 $w = $weight * 2 - 1;
2521 $a = $firstAlpha - $secondAlpha;
2522
2523 $w1 = (($w * $a == -1 ? $w : ($w + $a)/(1 + $w * $a)) + 1) / 2.0;
2524 $w2 = 1.0 - $w1;
2525
2526 $new = array('color',
2527 $w1 * $first[1] + $w2 * $second[1],
2528 $w1 * $first[2] + $w2 * $second[2],
2529 $w1 * $first[3] + $w2 * $second[3],
2530 );
2531
2532 if ($firstAlpha != 1.0 || $secondAlpha != 1.0) {
2533 $new[] = $firstAlpha * $weight + $secondAlpha * ($weight - 1);
2534 }
2535
2536 return $this->fixColor($new);
2537 }
2538
2539 protected static $libHsl = array('hue', 'saturation', 'lightness');
2540 protected function libHsl($args)
2541 {
2542 list($h, $s, $l) = $args;
2543
2544 return $this->toRGB($h[1], $s[1], $l[1]);
2545 }
2546
2547 protected static $libHsla = array('hue', 'saturation',
2548 'lightness', 'alpha');
2549 protected function libHsla($args)
2550 {
2551 list($h, $s, $l, $a) = $args;
2552
2553 $color = $this->toRGB($h[1], $s[1], $l[1]);
2554 $color[4] = $a[1];
2555
2556 return $color;
2557 }
2558
2559 protected static $libHue = array('color');
2560 protected function libHue($args)
2561 {
2562 $color = $this->assertColor($args[0]);
2563 $hsl = $this->toHSL($color[1], $color[2], $color[3]);
2564
2565 return array('number', $hsl[1], 'deg');
2566 }
2567
2568 protected static $libSaturation = array('color');
2569 protected function libSaturation($args)
2570 {
2571 $color = $this->assertColor($args[0]);
2572 $hsl = $this->toHSL($color[1], $color[2], $color[3]);
2573
2574 return array('number', $hsl[2], '%');
2575 }
2576
2577 protected static $libLightness = array('color');
2578 protected function libLightness($args)
2579 {
2580 $color = $this->assertColor($args[0]);
2581 $hsl = $this->toHSL($color[1], $color[2], $color[3]);
2582
2583 return array('number', $hsl[3], '%');
2584 }
2585
2586 protected function adjustHsl($color, $idx, $amount)
2587 {
2588 $hsl = $this->toHSL($color[1], $color[2], $color[3]);
2589 $hsl[$idx] += $amount;
2590 $out = $this->toRGB($hsl[1], $hsl[2], $hsl[3]);
2591
2592 if (isset($color[4])) {
2593 $out[4] = $color[4];
2594 }
2595
2596 return $out;
2597 }
2598
2599 protected static $libAdjustHue = array('color', 'degrees');
2600 protected function libAdjustHue($args)
2601 {
2602 $color = $this->assertColor($args[0]);
2603 $degrees = $this->assertNumber($args[1]);
2604
2605 return $this->adjustHsl($color, 1, $degrees);
2606 }
2607
2608 protected static $libLighten = array('color', 'amount');
2609 protected function libLighten($args)
2610 {
2611 $color = $this->assertColor($args[0]);
2612 $amount = 100*$this->coercePercent($args[1]);
2613
2614 return $this->adjustHsl($color, 3, $amount);
2615 }
2616
2617 protected static $libDarken = array('color', 'amount');
2618 protected function libDarken($args)
2619 {
2620 $color = $this->assertColor($args[0]);
2621 $amount = 100*$this->coercePercent($args[1]);
2622
2623 return $this->adjustHsl($color, 3, -$amount);
2624 }
2625
2626 protected static $libSaturate = array('color', 'amount');
2627 protected function libSaturate($args)
2628 {
2629 $value = $args[0];
2630
2631 if ($value[0] === 'number') {
2632 return null;
2633 }
2634
2635 $color = $this->assertColor($value);
2636 $amount = 100*$this->coercePercent($args[1]);
2637
2638 return $this->adjustHsl($color, 2, $amount);
2639 }
2640
2641 protected static $libDesaturate = array('color', 'amount');
2642 protected function libDesaturate($args)
2643 {
2644 $color = $this->assertColor($args[0]);
2645 $amount = 100*$this->coercePercent($args[1]);
2646
2647 return $this->adjustHsl($color, 2, -$amount);
2648 }
2649
2650 protected static $libGrayscale = array('color');
2651 protected function libGrayscale($args)
2652 {
2653 $value = $args[0];
2654
2655 if ($value[0] === 'number') {
2656 return null;
2657 }
2658
2659 return $this->adjustHsl($this->assertColor($value), 2, -100);
2660 }
2661
2662 protected static $libComplement = array('color');
2663 protected function libComplement($args)
2664 {
2665 return $this->adjustHsl($this->assertColor($args[0]), 1, 180);
2666 }
2667
2668 protected static $libInvert = array('color');
2669 protected function libInvert($args)
2670 {
2671 $value = $args[0];
2672
2673 if ($value[0] === 'number') {
2674 return null;
2675 }
2676
2677 $color = $this->assertColor($value);
2678 $color[1] = 255 - $color[1];
2679 $color[2] = 255 - $color[2];
2680 $color[3] = 255 - $color[3];
2681
2682 return $color;
2683 }
2684
2685 // increases opacity by amount
2686 protected static $libOpacify = array('color', 'amount');
2687 protected function libOpacify($args)
2688 {
2689 $color = $this->assertColor($args[0]);
2690 $amount = $this->coercePercent($args[1]);
2691
2692 $color[4] = (isset($color[4]) ? $color[4] : 1) + $amount;
2693 $color[4] = min(1, max(0, $color[4]));
2694
2695 return $color;
2696 }
2697
2698 protected static $libFadeIn = array('color', 'amount');
2699 protected function libFadeIn($args)
2700 {
2701 return $this->libOpacify($args);
2702 }
2703
2704 // decreases opacity by amount
2705 protected static $libTransparentize = array('color', 'amount');
2706 protected function libTransparentize($args)
2707 {
2708 $color = $this->assertColor($args[0]);
2709 $amount = $this->coercePercent($args[1]);
2710
2711 $color[4] = (isset($color[4]) ? $color[4] : 1) - $amount;
2712 $color[4] = min(1, max(0, $color[4]));
2713
2714 return $color;
2715 }
2716
2717 protected static $libFadeOut = array('color', 'amount');
2718 protected function libFadeOut($args)
2719 {
2720 return $this->libTransparentize($args);
2721 }
2722
2723 protected static $libUnquote = array('string');
2724 protected function libUnquote($args)
2725 {
2726 $str = $args[0];
2727
2728 if ($str[0] == 'string') {
2729 $str[1] = '';
2730 }
2731
2732 return $str;
2733 }
2734
2735 protected static $libQuote = array('string');
2736 protected function libQuote($args)
2737 {
2738 $value = $args[0];
2739
2740 if ($value[0] == 'string' && !empty($value[1])) {
2741 return $value;
2742 }
2743
2744 return array('string', '"', array($value));
2745 }
2746
2747 protected static $libPercentage = array('value');
2748 protected function libPercentage($args)
2749 {
2750 return array('number',
2751 $this->coercePercent($args[0]) * 100,
2752 '%');
2753 }
2754
2755 protected static $libRound = array('value');
2756 protected function libRound($args)
2757 {
2758 $num = $args[0];
2759 $num[1] = round($num[1]);
2760
2761 return $num;
2762 }
2763
2764 protected static $libFloor = array('value');
2765 protected function libFloor($args)
2766 {
2767 $num = $args[0];
2768 $num[1] = floor($num[1]);
2769
2770 return $num;
2771 }
2772
2773 protected static $libCeil = array('value');
2774 protected function libCeil($args)
2775 {
2776 $num = $args[0];
2777 $num[1] = ceil($num[1]);
2778
2779 return $num;
2780 }
2781
2782 protected static $libAbs = array('value');
2783 protected function libAbs($args)
2784 {
2785 $num = $args[0];
2786 $num[1] = abs($num[1]);
2787
2788 return $num;
2789 }
2790
2791 protected function libMin($args)
2792 {
2793 $numbers = $this->getNormalizedNumbers($args);
2794 $min = null;
2795
2796 foreach ($numbers as $key => $number) {
2797 if (null === $min || $number[1] <= $min[1]) {
2798 $min = array($key, $number[1]);
2799 }
2800 }
2801
2802 return $args[$min[0]];
2803 }
2804
2805 protected function libMax($args)
2806 {
2807 $numbers = $this->getNormalizedNumbers($args);
2808 $max = null;
2809
2810 foreach ($numbers as $key => $number) {
2811 if (null === $max || $number[1] >= $max[1]) {
2812 $max = array($key, $number[1]);
2813 }
2814 }
2815
2816 return $args[$max[0]];
2817 }
2818
2819 protected function getNormalizedNumbers($args)
2820 {
2821 $unit = null;
2822 $originalUnit = null;
2823 $numbers = array();
2824
2825 foreach ($args as $key => $item) {
2826 if ('number' != $item[0]) {
2827 $this->throwError('%s is not a number', $item[0]);
2828 }
2829
2830 $number = $this->normalizeNumber($item);
2831
2832 if (null === $unit) {
2833 $unit = $number[2];
2834 $originalUnit = $item[2];
2835 } elseif ($unit !== $number[2]) {
2836 $this->throwError('Incompatible units: "%s" and "%s".', $originalUnit, $item[2]);
2837 }
2838
2839 $numbers[$key] = $number;
2840 }
2841
2842 return $numbers;
2843 }
2844
2845 protected static $libLength = array('list');
2846 protected function libLength($args)
2847 {
2848 $list = $this->coerceList($args[0]);
2849
2850 return count($list[2]);
2851 }
2852
2853 protected static $libNth = array('list', 'n');
2854 protected function libNth($args)
2855 {
2856 $list = $this->coerceList($args[0]);
2857 $n = $this->assertNumber($args[1]) - 1;
2858
2859 return isset($list[2][$n]) ? $list[2][$n] : self::$defaultValue;
2860 }
2861
2862 protected function listSeparatorForJoin($list1, $sep)
2863 {
2864 if (!isset($sep)) {
2865 return $list1[1];
2866 }
2867
2868 switch ($this->compileValue($sep)) {
2869 case 'comma':
2870 return ',';
2871 case 'space':
2872 return '';
2873 default:
2874 return $list1[1];
2875 }
2876 }
2877
2878 protected static $libJoin = array('list1', 'list2', 'separator');
2879 protected function libJoin($args)
2880 {
2881 list($list1, $list2, $sep) = $args;
2882
2883 $list1 = $this->coerceList($list1, ' ');
2884 $list2 = $this->coerceList($list2, ' ');
2885 $sep = $this->listSeparatorForJoin($list1, $sep);
2886
2887 return array('list', $sep, array_merge($list1[2], $list2[2]));
2888 }
2889
2890 protected static $libAppend = array('list', 'val', 'separator');
2891 protected function libAppend($args)
2892 {
2893 list($list1, $value, $sep) = $args;
2894
2895 $list1 = $this->coerceList($list1, ' ');
2896 $sep = $this->listSeparatorForJoin($list1, $sep);
2897
2898 return array('list', $sep, array_merge($list1[2], array($value)));
2899 }
2900
2901 protected function libZip($args)
2902 {
2903 foreach ($args as $arg) {
2904 $this->assertList($arg);
2905 }
2906
2907 $lists = array();
2908 $firstList = array_shift($args);
2909
2910 foreach ($firstList[2] as $key => $item) {
2911 $list = array('list', '', array($item));
2912
2913 foreach ($args as $arg) {
2914 if (isset($arg[2][$key])) {
2915 $list[2][] = $arg[2][$key];
2916 } else {
2917 break 2;
2918 }
2919 }
2920 $lists[] = $list;
2921 }
2922
2923 return array('list', ',', $lists);
2924 }
2925
2926 protected static $libTypeOf = array('value');
2927 protected function libTypeOf($args)
2928 {
2929 $value = $args[0];
2930
2931 switch ($value[0]) {
2932 case 'keyword':
2933 if ($value == self::$true || $value == self::$false) {
2934 return 'bool';
2935 }
2936
2937 if ($this->coerceColor($value)) {
2938 return 'color';
2939 }
2940
2941 return 'string';
2942 default:
2943 return $value[0];
2944 }
2945 }
2946
2947 protected static $libUnit = array('number');
2948 protected function libUnit($args)
2949 {
2950 $num = $args[0];
2951
2952 if ($num[0] == 'number') {
2953 return array('string', '"', array($num[2]));
2954 }
2955
2956 return '';
2957 }
2958
2959 protected static $libUnitless = array('number');
2960 protected function libUnitless($args)
2961 {
2962 $value = $args[0];
2963
2964 return $value[0] == 'number' && empty($value[2]);
2965 }
2966
2967 protected static $libComparable = array('number-1', 'number-2');
2968 protected function libComparable($args)
2969 {
2970 list($number1, $number2) = $args;
2971
2972 if (!isset($number1[0]) || $number1[0] != 'number' || !isset($number2[0]) || $number2[0] != 'number') {
2973 $this->throwError('Invalid argument(s) for "comparable"');
2974 }
2975
2976 $number1 = $this->normalizeNumber($number1);
2977 $number2 = $this->normalizeNumber($number2);
2978
2979 return $number1[2] == $number2[2] || $number1[2] == '' || $number2[2] == '';
2980 }
2981
2982 /**
2983 * Workaround IE7's content counter bug.
2984 *
2985 * @param array $args
2986 */
2987 protected function libCounter($args)
2988 {
2989 $list = array_map(array($this, 'compileValue'), $args);
2990
2991 return array('string', '', array('counter(' . implode(',', $list) . ')'));
2992 }
2993
2994 public function throwError($msg = null)
2995 {
2996 if (func_num_args() > 1) {
2997 $msg = call_user_func_array('sprintf', func_get_args());
2998 }
2999
3000 if ($this->sourcePos >= 0 && isset($this->sourceParser)) {
3001 $this->sourceParser->throwParseError($msg, $this->sourcePos);
3002 }
3003
3004 throw new \Exception($msg);
3005 }
3006 }
3007