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