PluginProbe
MaxButtons – Create buttons / 7.10
MaxButtons – Create buttons v7.10
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 7.10, at assets/libraries/scssphp/src/Compiler.php

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