PluginProbe
MaxButtons – Create buttons / 6.28
MaxButtons – Create buttons v6.28
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.28, at assets/libraries/scssphp/src/Compiler.php

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