Total Complexity | 1549 |
Total Lines | 8208 |
Duplicated Lines | 0 % |
Changes | 1 | ||
Bugs | 0 | Features | 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 |
||
60 | class Compiler |
||
61 | { |
||
62 | const LINE_COMMENTS = 1; |
||
63 | const DEBUG_INFO = 2; |
||
64 | |||
65 | const WITH_RULE = 1; |
||
66 | const WITH_MEDIA = 2; |
||
67 | const WITH_SUPPORTS = 4; |
||
68 | const WITH_ALL = 7; |
||
69 | |||
70 | const SOURCE_MAP_NONE = 0; |
||
71 | const SOURCE_MAP_INLINE = 1; |
||
72 | const SOURCE_MAP_FILE = 2; |
||
73 | |||
74 | /** |
||
75 | * @var array |
||
76 | */ |
||
77 | protected static $operatorNames = [ |
||
78 | '+' => 'add', |
||
79 | '-' => 'sub', |
||
80 | '*' => 'mul', |
||
81 | '/' => 'div', |
||
82 | '%' => 'mod', |
||
83 | |||
84 | '==' => 'eq', |
||
85 | '!=' => 'neq', |
||
86 | '<' => 'lt', |
||
87 | '>' => 'gt', |
||
88 | |||
89 | '<=' => 'lte', |
||
90 | '>=' => 'gte', |
||
91 | '<=>' => 'cmp', |
||
92 | ]; |
||
93 | |||
94 | /** |
||
95 | * @var array |
||
96 | */ |
||
97 | protected static $namespaces = [ |
||
98 | 'special' => '%', |
||
99 | 'mixin' => '@', |
||
100 | 'function' => '^', |
||
101 | ]; |
||
102 | |||
103 | public static $true = [Type::T_KEYWORD, 'true']; |
||
104 | public static $false = [Type::T_KEYWORD, 'false']; |
||
105 | public static $NaN = [Type::T_KEYWORD, 'NaN']; |
||
106 | public static $Infinity = [Type::T_KEYWORD, 'Infinity']; |
||
107 | public static $null = [Type::T_NULL]; |
||
108 | public static $nullString = [Type::T_STRING, '', []]; |
||
109 | public static $defaultValue = [Type::T_KEYWORD, '']; |
||
110 | public static $selfSelector = [Type::T_SELF]; |
||
111 | public static $emptyList = [Type::T_LIST, '', []]; |
||
112 | public static $emptyMap = [Type::T_MAP, [], []]; |
||
113 | public static $emptyString = [Type::T_STRING, '"', []]; |
||
114 | public static $with = [Type::T_KEYWORD, 'with']; |
||
115 | public static $without = [Type::T_KEYWORD, 'without']; |
||
116 | |||
117 | protected $importPaths = ['']; |
||
118 | protected $importCache = []; |
||
119 | protected $importedFiles = []; |
||
120 | protected $userFunctions = []; |
||
121 | protected $registeredVars = []; |
||
122 | protected $registeredFeatures = [ |
||
123 | 'extend-selector-pseudoclass' => false, |
||
124 | 'at-error' => true, |
||
125 | 'units-level-3' => false, |
||
126 | 'global-variable-shadowing' => false, |
||
127 | ]; |
||
128 | |||
129 | protected $encoding = null; |
||
130 | protected $lineNumberStyle = null; |
||
131 | |||
132 | protected $sourceMap = self::SOURCE_MAP_NONE; |
||
133 | protected $sourceMapOptions = []; |
||
134 | |||
135 | /** |
||
136 | * @var string|\ScssPhp\ScssPhp\Formatter |
||
137 | */ |
||
138 | protected $formatter = 'ScssPhp\ScssPhp\Formatter\Nested'; |
||
139 | |||
140 | protected $rootEnv; |
||
141 | protected $rootBlock; |
||
142 | |||
143 | /** |
||
144 | * @var \ScssPhp\ScssPhp\Compiler\Environment |
||
145 | */ |
||
146 | protected $env; |
||
147 | protected $scope; |
||
148 | protected $storeEnv; |
||
149 | protected $charsetSeen; |
||
150 | protected $sourceNames; |
||
151 | |||
152 | protected $cache; |
||
153 | |||
154 | protected $indentLevel; |
||
155 | protected $extends; |
||
156 | protected $extendsMap; |
||
157 | protected $parsedFiles; |
||
158 | protected $parser; |
||
159 | protected $sourceIndex; |
||
160 | protected $sourceLine; |
||
161 | protected $sourceColumn; |
||
162 | protected $stderr; |
||
163 | protected $shouldEvaluate; |
||
164 | protected $ignoreErrors; |
||
165 | protected $ignoreCallStackMessage = false; |
||
166 | |||
167 | protected $callStack = []; |
||
168 | |||
169 | /** |
||
170 | * Constructor |
||
171 | * |
||
172 | * @param array|null $cacheOptions |
||
173 | */ |
||
174 | public function __construct($cacheOptions = null) |
||
175 | { |
||
176 | $this->parsedFiles = []; |
||
177 | $this->sourceNames = []; |
||
178 | |||
179 | if ($cacheOptions) { |
||
180 | $this->cache = new Cache($cacheOptions); |
||
181 | } |
||
182 | |||
183 | $this->stderr = fopen('php://stderr', 'w'); |
||
184 | } |
||
185 | |||
186 | /** |
||
187 | * Get compiler options |
||
188 | * |
||
189 | * @return array |
||
190 | */ |
||
191 | public function getCompileOptions() |
||
192 | { |
||
193 | $options = [ |
||
194 | 'importPaths' => $this->importPaths, |
||
195 | 'registeredVars' => $this->registeredVars, |
||
196 | 'registeredFeatures' => $this->registeredFeatures, |
||
197 | 'encoding' => $this->encoding, |
||
198 | 'sourceMap' => serialize($this->sourceMap), |
||
199 | 'sourceMapOptions' => $this->sourceMapOptions, |
||
200 | 'formatter' => $this->formatter, |
||
201 | ]; |
||
202 | |||
203 | return $options; |
||
204 | } |
||
205 | |||
206 | /** |
||
207 | * Set an alternative error output stream, for testing purpose only |
||
208 | * |
||
209 | * @param resource $handle |
||
210 | */ |
||
211 | public function setErrorOuput($handle) |
||
212 | { |
||
213 | $this->stderr = $handle; |
||
214 | } |
||
215 | |||
216 | /** |
||
217 | * Compile scss |
||
218 | * |
||
219 | * @api |
||
220 | * |
||
221 | * @param string $code |
||
222 | * @param string $path |
||
223 | * |
||
224 | * @return string |
||
225 | */ |
||
226 | public function compile($code, $path = null) |
||
227 | { |
||
228 | if ($this->cache) { |
||
229 | $cacheKey = ($path ? $path : '(stdin)') . ':' . md5($code); |
||
230 | $compileOptions = $this->getCompileOptions(); |
||
231 | $cache = $this->cache->getCache('compile', $cacheKey, $compileOptions); |
||
232 | |||
233 | if (\is_array($cache) && isset($cache['dependencies']) && isset($cache['out'])) { |
||
234 | // check if any dependency file changed before accepting the cache |
||
235 | foreach ($cache['dependencies'] as $file => $mtime) { |
||
236 | if (! is_file($file) || filemtime($file) !== $mtime) { |
||
237 | unset($cache); |
||
238 | break; |
||
239 | } |
||
240 | } |
||
241 | |||
242 | if (isset($cache)) { |
||
243 | return $cache['out']; |
||
244 | } |
||
245 | } |
||
246 | } |
||
247 | |||
248 | |||
249 | $this->indentLevel = -1; |
||
250 | $this->extends = []; |
||
251 | $this->extendsMap = []; |
||
252 | $this->sourceIndex = null; |
||
253 | $this->sourceLine = null; |
||
254 | $this->sourceColumn = null; |
||
255 | $this->env = null; |
||
256 | $this->scope = null; |
||
257 | $this->storeEnv = null; |
||
258 | $this->charsetSeen = null; |
||
259 | $this->shouldEvaluate = null; |
||
260 | $this->ignoreCallStackMessage = false; |
||
261 | |||
262 | $this->parser = $this->parserFactory($path); |
||
263 | $tree = $this->parser->parse($code); |
||
264 | $this->parser = null; |
||
265 | |||
266 | $this->formatter = new $this->formatter(); |
||
267 | $this->rootBlock = null; |
||
268 | $this->rootEnv = $this->pushEnv($tree); |
||
269 | |||
270 | $this->injectVariables($this->registeredVars); |
||
271 | $this->compileRoot($tree); |
||
272 | $this->popEnv(); |
||
273 | |||
274 | $sourceMapGenerator = null; |
||
275 | |||
276 | if ($this->sourceMap) { |
||
277 | if (\is_object($this->sourceMap) && $this->sourceMap instanceof SourceMapGenerator) { |
||
|
|||
278 | $sourceMapGenerator = $this->sourceMap; |
||
279 | $this->sourceMap = self::SOURCE_MAP_FILE; |
||
280 | } elseif ($this->sourceMap !== self::SOURCE_MAP_NONE) { |
||
281 | $sourceMapGenerator = new SourceMapGenerator($this->sourceMapOptions); |
||
282 | } |
||
283 | } |
||
284 | |||
285 | $out = $this->formatter->format($this->scope, $sourceMapGenerator); |
||
286 | |||
287 | if (! empty($out) && $this->sourceMap && $this->sourceMap !== self::SOURCE_MAP_NONE) { |
||
288 | $sourceMap = $sourceMapGenerator->generateJson(); |
||
289 | $sourceMapUrl = null; |
||
290 | |||
291 | switch ($this->sourceMap) { |
||
292 | case self::SOURCE_MAP_INLINE: |
||
293 | $sourceMapUrl = sprintf('data:application/json,%s', Util::encodeURIComponent($sourceMap)); |
||
294 | break; |
||
295 | |||
296 | case self::SOURCE_MAP_FILE: |
||
297 | $sourceMapUrl = $sourceMapGenerator->saveMap($sourceMap); |
||
298 | break; |
||
299 | } |
||
300 | |||
301 | $out .= sprintf('/*# sourceMappingURL=%s */', $sourceMapUrl); |
||
302 | } |
||
303 | |||
304 | if ($this->cache && isset($cacheKey) && isset($compileOptions)) { |
||
305 | $v = [ |
||
306 | 'dependencies' => $this->getParsedFiles(), |
||
307 | 'out' => &$out, |
||
308 | ]; |
||
309 | |||
310 | $this->cache->setCache('compile', $cacheKey, $v, $compileOptions); |
||
311 | } |
||
312 | |||
313 | return $out; |
||
314 | } |
||
315 | |||
316 | /** |
||
317 | * Instantiate parser |
||
318 | * |
||
319 | * @param string $path |
||
320 | * |
||
321 | * @return \ScssPhp\ScssPhp\Parser |
||
322 | */ |
||
323 | protected function parserFactory($path) |
||
324 | { |
||
325 | // https://sass-lang.com/documentation/at-rules/import |
||
326 | // CSS files imported by Sass don’t allow any special Sass features. |
||
327 | // In order to make sure authors don’t accidentally write Sass in their CSS, |
||
328 | // all Sass features that aren’t also valid CSS will produce errors. |
||
329 | // Otherwise, the CSS will be rendered as-is. It can even be extended! |
||
330 | $cssOnly = false; |
||
331 | |||
332 | if (substr($path, '-4') === '.css') { |
||
333 | $cssOnly = true; |
||
334 | } |
||
335 | |||
336 | $parser = new Parser($path, \count($this->sourceNames), $this->encoding, $this->cache, $cssOnly); |
||
337 | |||
338 | $this->sourceNames[] = $path; |
||
339 | $this->addParsedFile($path); |
||
340 | |||
341 | return $parser; |
||
342 | } |
||
343 | |||
344 | /** |
||
345 | * Is self extend? |
||
346 | * |
||
347 | * @param array $target |
||
348 | * @param array $origin |
||
349 | * |
||
350 | * @return boolean |
||
351 | */ |
||
352 | protected function isSelfExtend($target, $origin) |
||
353 | { |
||
354 | foreach ($origin as $sel) { |
||
355 | if (\in_array($target, $sel)) { |
||
356 | return true; |
||
357 | } |
||
358 | } |
||
359 | |||
360 | return false; |
||
361 | } |
||
362 | |||
363 | /** |
||
364 | * Push extends |
||
365 | * |
||
366 | * @param array $target |
||
367 | * @param array $origin |
||
368 | * @param array|null $block |
||
369 | */ |
||
370 | protected function pushExtends($target, $origin, $block) |
||
371 | { |
||
372 | $i = \count($this->extends); |
||
373 | $this->extends[] = [$target, $origin, $block]; |
||
374 | |||
375 | foreach ($target as $part) { |
||
376 | if (isset($this->extendsMap[$part])) { |
||
377 | $this->extendsMap[$part][] = $i; |
||
378 | } else { |
||
379 | $this->extendsMap[$part] = [$i]; |
||
380 | } |
||
381 | } |
||
382 | } |
||
383 | |||
384 | /** |
||
385 | * Make output block |
||
386 | * |
||
387 | * @param string $type |
||
388 | * @param array $selectors |
||
389 | * |
||
390 | * @return \ScssPhp\ScssPhp\Formatter\OutputBlock |
||
391 | */ |
||
392 | protected function makeOutputBlock($type, $selectors = null) |
||
393 | { |
||
394 | $out = new OutputBlock(); |
||
395 | $out->type = $type; |
||
396 | $out->lines = []; |
||
397 | $out->children = []; |
||
398 | $out->parent = $this->scope; |
||
399 | $out->selectors = $selectors; |
||
400 | $out->depth = $this->env->depth; |
||
401 | |||
402 | if ($this->env->block instanceof Block) { |
||
403 | $out->sourceName = $this->env->block->sourceName; |
||
404 | $out->sourceLine = $this->env->block->sourceLine; |
||
405 | $out->sourceColumn = $this->env->block->sourceColumn; |
||
406 | } else { |
||
407 | $out->sourceName = null; |
||
408 | $out->sourceLine = null; |
||
409 | $out->sourceColumn = null; |
||
410 | } |
||
411 | |||
412 | return $out; |
||
413 | } |
||
414 | |||
415 | /** |
||
416 | * Compile root |
||
417 | * |
||
418 | * @param \ScssPhp\ScssPhp\Block $rootBlock |
||
419 | */ |
||
420 | protected function compileRoot(Block $rootBlock) |
||
421 | { |
||
422 | $this->rootBlock = $this->scope = $this->makeOutputBlock(Type::T_ROOT); |
||
423 | |||
424 | $this->compileChildrenNoReturn($rootBlock->children, $this->scope); |
||
425 | $this->flattenSelectors($this->scope); |
||
426 | $this->missingSelectors(); |
||
427 | } |
||
428 | |||
429 | /** |
||
430 | * Report missing selectors |
||
431 | */ |
||
432 | protected function missingSelectors() |
||
433 | { |
||
434 | foreach ($this->extends as $extend) { |
||
435 | if (isset($extend[3])) { |
||
436 | continue; |
||
437 | } |
||
438 | |||
439 | list($target, $origin, $block) = $extend; |
||
440 | |||
441 | // ignore if !optional |
||
442 | if ($block[2]) { |
||
443 | continue; |
||
444 | } |
||
445 | |||
446 | $target = implode(' ', $target); |
||
447 | $origin = $this->collapseSelectors($origin); |
||
448 | |||
449 | $this->sourceLine = $block[Parser::SOURCE_LINE]; |
||
450 | throw $this->error("\"$origin\" failed to @extend \"$target\". The selector \"$target\" was not found."); |
||
451 | } |
||
452 | } |
||
453 | |||
454 | /** |
||
455 | * Flatten selectors |
||
456 | * |
||
457 | * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $block |
||
458 | * @param string $parentKey |
||
459 | */ |
||
460 | protected function flattenSelectors(OutputBlock $block, $parentKey = null) |
||
461 | { |
||
462 | if ($block->selectors) { |
||
463 | $selectors = []; |
||
464 | |||
465 | foreach ($block->selectors as $s) { |
||
466 | $selectors[] = $s; |
||
467 | |||
468 | if (! \is_array($s)) { |
||
469 | continue; |
||
470 | } |
||
471 | |||
472 | // check extends |
||
473 | if (! empty($this->extendsMap)) { |
||
474 | $this->matchExtends($s, $selectors); |
||
475 | |||
476 | // remove duplicates |
||
477 | array_walk($selectors, function (&$value) { |
||
478 | $value = serialize($value); |
||
479 | }); |
||
480 | |||
481 | $selectors = array_unique($selectors); |
||
482 | |||
483 | array_walk($selectors, function (&$value) { |
||
484 | $value = unserialize($value); |
||
485 | }); |
||
486 | } |
||
487 | } |
||
488 | |||
489 | $block->selectors = []; |
||
490 | $placeholderSelector = false; |
||
491 | |||
492 | foreach ($selectors as $selector) { |
||
493 | if ($this->hasSelectorPlaceholder($selector)) { |
||
494 | $placeholderSelector = true; |
||
495 | continue; |
||
496 | } |
||
497 | |||
498 | $block->selectors[] = $this->compileSelector($selector); |
||
499 | } |
||
500 | |||
501 | if ($placeholderSelector && 0 === \count($block->selectors) && null !== $parentKey) { |
||
502 | unset($block->parent->children[$parentKey]); |
||
503 | |||
504 | return; |
||
505 | } |
||
506 | } |
||
507 | |||
508 | foreach ($block->children as $key => $child) { |
||
509 | $this->flattenSelectors($child, $key); |
||
510 | } |
||
511 | } |
||
512 | |||
513 | /** |
||
514 | * Glue parts of :not( or :nth-child( ... that are in general splitted in selectors parts |
||
515 | * |
||
516 | * @param array $parts |
||
517 | * |
||
518 | * @return array |
||
519 | */ |
||
520 | protected function glueFunctionSelectors($parts) |
||
521 | { |
||
522 | $new = []; |
||
523 | |||
524 | foreach ($parts as $part) { |
||
525 | if (\is_array($part)) { |
||
526 | $part = $this->glueFunctionSelectors($part); |
||
527 | $new[] = $part; |
||
528 | } else { |
||
529 | // a selector part finishing with a ) is the last part of a :not( or :nth-child( |
||
530 | // and need to be joined to this |
||
531 | if ( |
||
532 | \count($new) && \is_string($new[\count($new) - 1]) && |
||
533 | \strlen($part) && substr($part, -1) === ')' && strpos($part, '(') === false |
||
534 | ) { |
||
535 | while (\count($new) > 1 && substr($new[\count($new) - 1], -1) !== '(') { |
||
536 | $part = array_pop($new) . $part; |
||
537 | } |
||
538 | $new[\count($new) - 1] .= $part; |
||
539 | } else { |
||
540 | $new[] = $part; |
||
541 | } |
||
542 | } |
||
543 | } |
||
544 | |||
545 | return $new; |
||
546 | } |
||
547 | |||
548 | /** |
||
549 | * Match extends |
||
550 | * |
||
551 | * @param array $selector |
||
552 | * @param array $out |
||
553 | * @param integer $from |
||
554 | * @param boolean $initial |
||
555 | */ |
||
556 | protected function matchExtends($selector, &$out, $from = 0, $initial = true) |
||
557 | { |
||
558 | static $partsPile = []; |
||
559 | $selector = $this->glueFunctionSelectors($selector); |
||
560 | |||
561 | if (\count($selector) == 1 && \in_array(reset($selector), $partsPile)) { |
||
562 | return; |
||
563 | } |
||
564 | |||
565 | $outRecurs = []; |
||
566 | |||
567 | foreach ($selector as $i => $part) { |
||
568 | if ($i < $from) { |
||
569 | continue; |
||
570 | } |
||
571 | |||
572 | // check that we are not building an infinite loop of extensions |
||
573 | // if the new part is just including a previous part don't try to extend anymore |
||
574 | if (\count($part) > 1) { |
||
575 | foreach ($partsPile as $previousPart) { |
||
576 | if (! \count(array_diff($previousPart, $part))) { |
||
577 | continue 2; |
||
578 | } |
||
579 | } |
||
580 | } |
||
581 | |||
582 | $partsPile[] = $part; |
||
583 | |||
584 | if ($this->matchExtendsSingle($part, $origin, $initial)) { |
||
585 | $after = \array_slice($selector, $i + 1); |
||
586 | $before = \array_slice($selector, 0, $i); |
||
587 | list($before, $nonBreakableBefore) = $this->extractRelationshipFromFragment($before); |
||
588 | |||
589 | foreach ($origin as $new) { |
||
590 | $k = 0; |
||
591 | |||
592 | // remove shared parts |
||
593 | if (\count($new) > 1) { |
||
594 | while ($k < $i && isset($new[$k]) && $selector[$k] === $new[$k]) { |
||
595 | $k++; |
||
596 | } |
||
597 | } |
||
598 | |||
599 | if (\count($nonBreakableBefore) && $k === \count($new)) { |
||
600 | $k--; |
||
601 | } |
||
602 | |||
603 | $replacement = []; |
||
604 | $tempReplacement = $k > 0 ? \array_slice($new, $k) : $new; |
||
605 | |||
606 | for ($l = \count($tempReplacement) - 1; $l >= 0; $l--) { |
||
607 | $slice = []; |
||
608 | |||
609 | foreach ($tempReplacement[$l] as $chunk) { |
||
610 | if (! \in_array($chunk, $slice)) { |
||
611 | $slice[] = $chunk; |
||
612 | } |
||
613 | } |
||
614 | |||
615 | array_unshift($replacement, $slice); |
||
616 | |||
617 | if (! $this->isImmediateRelationshipCombinator(end($slice))) { |
||
618 | break; |
||
619 | } |
||
620 | } |
||
621 | |||
622 | $afterBefore = $l != 0 ? \array_slice($tempReplacement, 0, $l) : []; |
||
623 | |||
624 | // Merge shared direct relationships. |
||
625 | $mergedBefore = $this->mergeDirectRelationships($afterBefore, $nonBreakableBefore); |
||
626 | |||
627 | $result = array_merge( |
||
628 | $before, |
||
629 | $mergedBefore, |
||
630 | $replacement, |
||
631 | $after |
||
632 | ); |
||
633 | |||
634 | if ($result === $selector) { |
||
635 | continue; |
||
636 | } |
||
637 | |||
638 | $this->pushOrMergeExtentedSelector($out, $result); |
||
639 | |||
640 | // recursively check for more matches |
||
641 | $startRecurseFrom = \count($before) + min(\count($nonBreakableBefore), \count($mergedBefore)); |
||
642 | |||
643 | if (\count($origin) > 1) { |
||
644 | $this->matchExtends($result, $out, $startRecurseFrom, false); |
||
645 | } else { |
||
646 | $this->matchExtends($result, $outRecurs, $startRecurseFrom, false); |
||
647 | } |
||
648 | |||
649 | // selector sequence merging |
||
650 | if (! empty($before) && \count($new) > 1) { |
||
651 | $preSharedParts = $k > 0 ? \array_slice($before, 0, $k) : []; |
||
652 | $postSharedParts = $k > 0 ? \array_slice($before, $k) : $before; |
||
653 | |||
654 | list($betweenSharedParts, $nonBreakabl2) = $this->extractRelationshipFromFragment($afterBefore); |
||
655 | |||
656 | $result2 = array_merge( |
||
657 | $preSharedParts, |
||
658 | $betweenSharedParts, |
||
659 | $postSharedParts, |
||
660 | $nonBreakabl2, |
||
661 | $nonBreakableBefore, |
||
662 | $replacement, |
||
663 | $after |
||
664 | ); |
||
665 | |||
666 | $this->pushOrMergeExtentedSelector($out, $result2); |
||
667 | } |
||
668 | } |
||
669 | } |
||
670 | array_pop($partsPile); |
||
671 | } |
||
672 | |||
673 | while (\count($outRecurs)) { |
||
674 | $result = array_shift($outRecurs); |
||
675 | $this->pushOrMergeExtentedSelector($out, $result); |
||
676 | } |
||
677 | } |
||
678 | |||
679 | /** |
||
680 | * Test a part for being a pseudo selector |
||
681 | * |
||
682 | * @param string $part |
||
683 | * @param array $matches |
||
684 | * |
||
685 | * @return boolean |
||
686 | */ |
||
687 | protected function isPseudoSelector($part, &$matches) |
||
688 | { |
||
689 | if ( |
||
690 | strpos($part, ':') === 0 && |
||
691 | preg_match(",^::?([\w-]+)\((.+)\)$,", $part, $matches) |
||
692 | ) { |
||
693 | return true; |
||
694 | } |
||
695 | |||
696 | return false; |
||
697 | } |
||
698 | |||
699 | /** |
||
700 | * Push extended selector except if |
||
701 | * - this is a pseudo selector |
||
702 | * - same as previous |
||
703 | * - in a white list |
||
704 | * in this case we merge the pseudo selector content |
||
705 | * |
||
706 | * @param array $out |
||
707 | * @param array $extended |
||
708 | */ |
||
709 | protected function pushOrMergeExtentedSelector(&$out, $extended) |
||
710 | { |
||
711 | if (\count($out) && \count($extended) === 1 && \count(reset($extended)) === 1) { |
||
712 | $single = reset($extended); |
||
713 | $part = reset($single); |
||
714 | |||
715 | if ( |
||
716 | $this->isPseudoSelector($part, $matchesExtended) && |
||
717 | \in_array($matchesExtended[1], [ 'slotted' ]) |
||
718 | ) { |
||
719 | $prev = end($out); |
||
720 | $prev = $this->glueFunctionSelectors($prev); |
||
721 | |||
722 | if (\count($prev) === 1 && \count(reset($prev)) === 1) { |
||
723 | $single = reset($prev); |
||
724 | $part = reset($single); |
||
725 | |||
726 | if ( |
||
727 | $this->isPseudoSelector($part, $matchesPrev) && |
||
728 | $matchesPrev[1] === $matchesExtended[1] |
||
729 | ) { |
||
730 | $extended = explode($matchesExtended[1] . '(', $matchesExtended[0], 2); |
||
731 | $extended[1] = $matchesPrev[2] . ', ' . $extended[1]; |
||
732 | $extended = implode($matchesExtended[1] . '(', $extended); |
||
733 | $extended = [ [ $extended ]]; |
||
734 | array_pop($out); |
||
735 | } |
||
736 | } |
||
737 | } |
||
738 | } |
||
739 | $out[] = $extended; |
||
740 | } |
||
741 | |||
742 | /** |
||
743 | * Match extends single |
||
744 | * |
||
745 | * @param array $rawSingle |
||
746 | * @param array $outOrigin |
||
747 | * @param boolean $initial |
||
748 | * |
||
749 | * @return boolean |
||
750 | */ |
||
751 | protected function matchExtendsSingle($rawSingle, &$outOrigin, $initial = true) |
||
752 | { |
||
753 | $counts = []; |
||
754 | $single = []; |
||
755 | |||
756 | // simple usual cases, no need to do the whole trick |
||
757 | if (\in_array($rawSingle, [['>'],['+'],['~']])) { |
||
758 | return false; |
||
759 | } |
||
760 | |||
761 | foreach ($rawSingle as $part) { |
||
762 | // matches Number |
||
763 | if (! \is_string($part)) { |
||
764 | return false; |
||
765 | } |
||
766 | |||
767 | if (! preg_match('/^[\[.:#%]/', $part) && \count($single)) { |
||
768 | $single[\count($single) - 1] .= $part; |
||
769 | } else { |
||
770 | $single[] = $part; |
||
771 | } |
||
772 | } |
||
773 | |||
774 | $extendingDecoratedTag = false; |
||
775 | |||
776 | if (\count($single) > 1) { |
||
777 | $matches = null; |
||
778 | $extendingDecoratedTag = preg_match('/^[a-z0-9]+$/i', $single[0], $matches) ? $matches[0] : false; |
||
779 | } |
||
780 | |||
781 | $outOrigin = []; |
||
782 | $found = false; |
||
783 | |||
784 | foreach ($single as $k => $part) { |
||
785 | if (isset($this->extendsMap[$part])) { |
||
786 | foreach ($this->extendsMap[$part] as $idx) { |
||
787 | $counts[$idx] = isset($counts[$idx]) ? $counts[$idx] + 1 : 1; |
||
788 | } |
||
789 | } |
||
790 | |||
791 | if ( |
||
792 | $initial && |
||
793 | $this->isPseudoSelector($part, $matches) && |
||
794 | ! \in_array($matches[1], [ 'not' ]) |
||
795 | ) { |
||
796 | $buffer = $matches[2]; |
||
797 | $parser = $this->parserFactory(__METHOD__); |
||
798 | |||
799 | if ($parser->parseSelector($buffer, $subSelectors)) { |
||
800 | foreach ($subSelectors as $ksub => $subSelector) { |
||
801 | $subExtended = []; |
||
802 | $this->matchExtends($subSelector, $subExtended, 0, false); |
||
803 | |||
804 | if ($subExtended) { |
||
805 | $subSelectorsExtended = $subSelectors; |
||
806 | $subSelectorsExtended[$ksub] = $subExtended; |
||
807 | |||
808 | foreach ($subSelectorsExtended as $ksse => $sse) { |
||
809 | $subSelectorsExtended[$ksse] = $this->collapseSelectors($sse); |
||
810 | } |
||
811 | |||
812 | $subSelectorsExtended = implode(', ', $subSelectorsExtended); |
||
813 | $singleExtended = $single; |
||
814 | $singleExtended[$k] = str_replace('(' . $buffer . ')', "($subSelectorsExtended)", $part); |
||
815 | $outOrigin[] = [ $singleExtended ]; |
||
816 | $found = true; |
||
817 | } |
||
818 | } |
||
819 | } |
||
820 | } |
||
821 | } |
||
822 | |||
823 | foreach ($counts as $idx => $count) { |
||
824 | list($target, $origin, /* $block */) = $this->extends[$idx]; |
||
825 | |||
826 | $origin = $this->glueFunctionSelectors($origin); |
||
827 | |||
828 | // check count |
||
829 | if ($count !== \count($target)) { |
||
830 | continue; |
||
831 | } |
||
832 | |||
833 | $this->extends[$idx][3] = true; |
||
834 | |||
835 | $rem = array_diff($single, $target); |
||
836 | |||
837 | foreach ($origin as $j => $new) { |
||
838 | // prevent infinite loop when target extends itself |
||
839 | if ($this->isSelfExtend($single, $origin) && ! $initial) { |
||
840 | return false; |
||
841 | } |
||
842 | |||
843 | $replacement = end($new); |
||
844 | |||
845 | // Extending a decorated tag with another tag is not possible. |
||
846 | if ( |
||
847 | $extendingDecoratedTag && $replacement[0] != $extendingDecoratedTag && |
||
848 | preg_match('/^[a-z0-9]+$/i', $replacement[0]) |
||
849 | ) { |
||
850 | unset($origin[$j]); |
||
851 | continue; |
||
852 | } |
||
853 | |||
854 | $combined = $this->combineSelectorSingle($replacement, $rem); |
||
855 | |||
856 | if (\count(array_diff($combined, $origin[$j][\count($origin[$j]) - 1]))) { |
||
857 | $origin[$j][\count($origin[$j]) - 1] = $combined; |
||
858 | } |
||
859 | } |
||
860 | |||
861 | $outOrigin = array_merge($outOrigin, $origin); |
||
862 | |||
863 | $found = true; |
||
864 | } |
||
865 | |||
866 | return $found; |
||
867 | } |
||
868 | |||
869 | /** |
||
870 | * Extract a relationship from the fragment. |
||
871 | * |
||
872 | * When extracting the last portion of a selector we will be left with a |
||
873 | * fragment which may end with a direction relationship combinator. This |
||
874 | * method will extract the relationship fragment and return it along side |
||
875 | * the rest. |
||
876 | * |
||
877 | * @param array $fragment The selector fragment maybe ending with a direction relationship combinator. |
||
878 | * |
||
879 | * @return array The selector without the relationship fragment if any, the relationship fragment. |
||
880 | */ |
||
881 | protected function extractRelationshipFromFragment(array $fragment) |
||
882 | { |
||
883 | $parents = []; |
||
884 | $children = []; |
||
885 | |||
886 | $j = $i = \count($fragment); |
||
887 | |||
888 | for (;;) { |
||
889 | $children = $j != $i ? \array_slice($fragment, $j, $i - $j) : []; |
||
890 | $parents = \array_slice($fragment, 0, $j); |
||
891 | $slice = end($parents); |
||
892 | |||
893 | if (empty($slice) || ! $this->isImmediateRelationshipCombinator($slice[0])) { |
||
894 | break; |
||
895 | } |
||
896 | |||
897 | $j -= 2; |
||
898 | } |
||
899 | |||
900 | return [$parents, $children]; |
||
901 | } |
||
902 | |||
903 | /** |
||
904 | * Combine selector single |
||
905 | * |
||
906 | * @param array $base |
||
907 | * @param array $other |
||
908 | * |
||
909 | * @return array |
||
910 | */ |
||
911 | protected function combineSelectorSingle($base, $other) |
||
912 | { |
||
913 | $tag = []; |
||
914 | $out = []; |
||
915 | $wasTag = false; |
||
916 | $pseudo = []; |
||
917 | |||
918 | while (\count($other) && strpos(end($other), ':') === 0) { |
||
919 | array_unshift($pseudo, array_pop($other)); |
||
920 | } |
||
921 | |||
922 | foreach ([array_reverse($base), array_reverse($other)] as $single) { |
||
923 | $rang = count($single); |
||
924 | |||
925 | foreach ($single as $part) { |
||
926 | if (preg_match('/^[\[:]/', $part)) { |
||
927 | $out[] = $part; |
||
928 | $wasTag = false; |
||
929 | } elseif (preg_match('/^[\.#]/', $part)) { |
||
930 | array_unshift($out, $part); |
||
931 | $wasTag = false; |
||
932 | } elseif (preg_match('/^[^_-]/', $part) && $rang === 1) { |
||
933 | $tag[] = $part; |
||
934 | $wasTag = true; |
||
935 | } elseif ($wasTag) { |
||
936 | $tag[\count($tag) - 1] .= $part; |
||
937 | } else { |
||
938 | array_unshift($out, $part); |
||
939 | } |
||
940 | $rang--; |
||
941 | } |
||
942 | } |
||
943 | |||
944 | if (\count($tag)) { |
||
945 | array_unshift($out, $tag[0]); |
||
946 | } |
||
947 | |||
948 | while (\count($pseudo)) { |
||
949 | $out[] = array_shift($pseudo); |
||
950 | } |
||
951 | |||
952 | return $out; |
||
953 | } |
||
954 | |||
955 | /** |
||
956 | * Compile media |
||
957 | * |
||
958 | * @param \ScssPhp\ScssPhp\Block $media |
||
959 | */ |
||
960 | protected function compileMedia(Block $media) |
||
961 | { |
||
962 | $this->pushEnv($media); |
||
963 | |||
964 | $mediaQueries = $this->compileMediaQuery($this->multiplyMedia($this->env)); |
||
965 | |||
966 | if (! empty($mediaQueries) && $mediaQueries) { |
||
967 | $previousScope = $this->scope; |
||
968 | $parentScope = $this->mediaParent($this->scope); |
||
969 | |||
970 | foreach ($mediaQueries as $mediaQuery) { |
||
971 | $this->scope = $this->makeOutputBlock(Type::T_MEDIA, [$mediaQuery]); |
||
972 | |||
973 | $parentScope->children[] = $this->scope; |
||
974 | $parentScope = $this->scope; |
||
975 | } |
||
976 | |||
977 | // top level properties in a media cause it to be wrapped |
||
978 | $needsWrap = false; |
||
979 | |||
980 | foreach ($media->children as $child) { |
||
981 | $type = $child[0]; |
||
982 | |||
983 | if ( |
||
984 | $type !== Type::T_BLOCK && |
||
985 | $type !== Type::T_MEDIA && |
||
986 | $type !== Type::T_DIRECTIVE && |
||
987 | $type !== Type::T_IMPORT |
||
988 | ) { |
||
989 | $needsWrap = true; |
||
990 | break; |
||
991 | } |
||
992 | } |
||
993 | |||
994 | if ($needsWrap) { |
||
995 | $wrapped = new Block(); |
||
996 | $wrapped->sourceName = $media->sourceName; |
||
997 | $wrapped->sourceIndex = $media->sourceIndex; |
||
998 | $wrapped->sourceLine = $media->sourceLine; |
||
999 | $wrapped->sourceColumn = $media->sourceColumn; |
||
1000 | $wrapped->selectors = []; |
||
1001 | $wrapped->comments = []; |
||
1002 | $wrapped->parent = $media; |
||
1003 | $wrapped->children = $media->children; |
||
1004 | |||
1005 | $media->children = [[Type::T_BLOCK, $wrapped]]; |
||
1006 | |||
1007 | if (isset($this->lineNumberStyle)) { |
||
1008 | $annotation = $this->makeOutputBlock(Type::T_COMMENT); |
||
1009 | $annotation->depth = 0; |
||
1010 | |||
1011 | $file = $this->sourceNames[$media->sourceIndex]; |
||
1012 | $line = $media->sourceLine; |
||
1013 | |||
1014 | switch ($this->lineNumberStyle) { |
||
1015 | case static::LINE_COMMENTS: |
||
1016 | $annotation->lines[] = '/* line ' . $line |
||
1017 | . ($file ? ', ' . $file : '') |
||
1018 | . ' */'; |
||
1019 | break; |
||
1020 | |||
1021 | case static::DEBUG_INFO: |
||
1022 | $annotation->lines[] = '@media -sass-debug-info{' |
||
1023 | . ($file ? 'filename{font-family:"' . $file . '"}' : '') |
||
1024 | . 'line{font-family:' . $line . '}}'; |
||
1025 | break; |
||
1026 | } |
||
1027 | |||
1028 | $this->scope->children[] = $annotation; |
||
1029 | } |
||
1030 | } |
||
1031 | |||
1032 | $this->compileChildrenNoReturn($media->children, $this->scope); |
||
1033 | |||
1034 | $this->scope = $previousScope; |
||
1035 | } |
||
1036 | |||
1037 | $this->popEnv(); |
||
1038 | } |
||
1039 | |||
1040 | /** |
||
1041 | * Media parent |
||
1042 | * |
||
1043 | * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $scope |
||
1044 | * |
||
1045 | * @return \ScssPhp\ScssPhp\Formatter\OutputBlock |
||
1046 | */ |
||
1047 | protected function mediaParent(OutputBlock $scope) |
||
1048 | { |
||
1049 | while (! empty($scope->parent)) { |
||
1050 | if (! empty($scope->type) && $scope->type !== Type::T_MEDIA) { |
||
1051 | break; |
||
1052 | } |
||
1053 | |||
1054 | $scope = $scope->parent; |
||
1055 | } |
||
1056 | |||
1057 | return $scope; |
||
1058 | } |
||
1059 | |||
1060 | /** |
||
1061 | * Compile directive |
||
1062 | * |
||
1063 | * @param \ScssPhp\ScssPhp\Block|array $block |
||
1064 | * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $out |
||
1065 | */ |
||
1066 | protected function compileDirective($directive, OutputBlock $out) |
||
1067 | { |
||
1068 | if (\is_array($directive)) { |
||
1069 | $s = '@' . $directive[0]; |
||
1070 | |||
1071 | if (! empty($directive[1])) { |
||
1072 | $s .= ' ' . $this->compileValue($directive[1]); |
||
1073 | } |
||
1074 | |||
1075 | $this->appendRootDirective($s . ';', $out); |
||
1076 | } else { |
||
1077 | $s = '@' . $directive->name; |
||
1078 | |||
1079 | if (! empty($directive->value)) { |
||
1080 | $s .= ' ' . $this->compileValue($directive->value); |
||
1081 | } |
||
1082 | |||
1083 | if ($directive->name === 'keyframes' || substr($directive->name, -10) === '-keyframes') { |
||
1084 | $this->compileKeyframeBlock($directive, [$s]); |
||
1085 | } else { |
||
1086 | $this->compileNestedBlock($directive, [$s]); |
||
1087 | } |
||
1088 | } |
||
1089 | } |
||
1090 | |||
1091 | /** |
||
1092 | * Compile at-root |
||
1093 | * |
||
1094 | * @param \ScssPhp\ScssPhp\Block $block |
||
1095 | */ |
||
1096 | protected function compileAtRoot(Block $block) |
||
1097 | { |
||
1098 | $env = $this->pushEnv($block); |
||
1099 | $envs = $this->compactEnv($env); |
||
1100 | list($with, $without) = $this->compileWith(isset($block->with) ? $block->with : null); |
||
1101 | |||
1102 | // wrap inline selector |
||
1103 | if ($block->selector) { |
||
1104 | $wrapped = new Block(); |
||
1105 | $wrapped->sourceName = $block->sourceName; |
||
1106 | $wrapped->sourceIndex = $block->sourceIndex; |
||
1107 | $wrapped->sourceLine = $block->sourceLine; |
||
1108 | $wrapped->sourceColumn = $block->sourceColumn; |
||
1109 | $wrapped->selectors = $block->selector; |
||
1110 | $wrapped->comments = []; |
||
1111 | $wrapped->parent = $block; |
||
1112 | $wrapped->children = $block->children; |
||
1113 | $wrapped->selfParent = $block->selfParent; |
||
1114 | |||
1115 | $block->children = [[Type::T_BLOCK, $wrapped]]; |
||
1116 | $block->selector = null; |
||
1117 | } |
||
1118 | |||
1119 | $selfParent = $block->selfParent; |
||
1120 | |||
1121 | if ( |
||
1122 | ! $block->selfParent->selectors && |
||
1123 | isset($block->parent) && $block->parent && |
||
1124 | isset($block->parent->selectors) && $block->parent->selectors |
||
1125 | ) { |
||
1126 | $selfParent = $block->parent; |
||
1127 | } |
||
1128 | |||
1129 | $this->env = $this->filterWithWithout($envs, $with, $without); |
||
1130 | |||
1131 | $saveScope = $this->scope; |
||
1132 | $this->scope = $this->filterScopeWithWithout($saveScope, $with, $without); |
||
1133 | |||
1134 | // propagate selfParent to the children where they still can be useful |
||
1135 | $this->compileChildrenNoReturn($block->children, $this->scope, $selfParent); |
||
1136 | |||
1137 | $this->scope = $this->completeScope($this->scope, $saveScope); |
||
1138 | $this->scope = $saveScope; |
||
1139 | $this->env = $this->extractEnv($envs); |
||
1140 | |||
1141 | $this->popEnv(); |
||
1142 | } |
||
1143 | |||
1144 | /** |
||
1145 | * Filter at-root scope depending of with/without option |
||
1146 | * |
||
1147 | * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $scope |
||
1148 | * @param array $with |
||
1149 | * @param array $without |
||
1150 | * |
||
1151 | * @return mixed |
||
1152 | */ |
||
1153 | protected function filterScopeWithWithout($scope, $with, $without) |
||
1154 | { |
||
1155 | $filteredScopes = []; |
||
1156 | $childStash = []; |
||
1157 | |||
1158 | if ($scope->type === TYPE::T_ROOT) { |
||
1159 | return $scope; |
||
1160 | } |
||
1161 | |||
1162 | // start from the root |
||
1163 | while ($scope->parent && $scope->parent->type !== TYPE::T_ROOT) { |
||
1164 | array_unshift($childStash, $scope); |
||
1165 | $scope = $scope->parent; |
||
1166 | } |
||
1167 | |||
1168 | for (;;) { |
||
1169 | if (! $scope) { |
||
1170 | break; |
||
1171 | } |
||
1172 | |||
1173 | if ($this->isWith($scope, $with, $without)) { |
||
1174 | $s = clone $scope; |
||
1175 | $s->children = []; |
||
1176 | $s->lines = []; |
||
1177 | $s->parent = null; |
||
1178 | |||
1179 | if ($s->type !== Type::T_MEDIA && $s->type !== Type::T_DIRECTIVE) { |
||
1180 | $s->selectors = []; |
||
1181 | } |
||
1182 | |||
1183 | $filteredScopes[] = $s; |
||
1184 | } |
||
1185 | |||
1186 | if (\count($childStash)) { |
||
1187 | $scope = array_shift($childStash); |
||
1188 | } elseif ($scope->children) { |
||
1189 | $scope = end($scope->children); |
||
1190 | } else { |
||
1191 | $scope = null; |
||
1192 | } |
||
1193 | } |
||
1194 | |||
1195 | if (! \count($filteredScopes)) { |
||
1196 | return $this->rootBlock; |
||
1197 | } |
||
1198 | |||
1199 | $newScope = array_shift($filteredScopes); |
||
1200 | $newScope->parent = $this->rootBlock; |
||
1201 | |||
1202 | $this->rootBlock->children[] = $newScope; |
||
1203 | |||
1204 | $p = &$newScope; |
||
1205 | |||
1206 | while (\count($filteredScopes)) { |
||
1207 | $s = array_shift($filteredScopes); |
||
1208 | $s->parent = $p; |
||
1209 | $p->children[] = $s; |
||
1210 | $newScope = &$p->children[0]; |
||
1211 | $p = &$p->children[0]; |
||
1212 | } |
||
1213 | |||
1214 | return $newScope; |
||
1215 | } |
||
1216 | |||
1217 | /** |
||
1218 | * found missing selector from a at-root compilation in the previous scope |
||
1219 | * (if at-root is just enclosing a property, the selector is in the parent tree) |
||
1220 | * |
||
1221 | * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $scope |
||
1222 | * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $previousScope |
||
1223 | * |
||
1224 | * @return mixed |
||
1225 | */ |
||
1226 | protected function completeScope($scope, $previousScope) |
||
1227 | { |
||
1228 | if (! $scope->type && (! $scope->selectors || ! \count($scope->selectors)) && \count($scope->lines)) { |
||
1229 | $scope->selectors = $this->findScopeSelectors($previousScope, $scope->depth); |
||
1230 | } |
||
1231 | |||
1232 | if ($scope->children) { |
||
1233 | foreach ($scope->children as $k => $c) { |
||
1234 | $scope->children[$k] = $this->completeScope($c, $previousScope); |
||
1235 | } |
||
1236 | } |
||
1237 | |||
1238 | return $scope; |
||
1239 | } |
||
1240 | |||
1241 | /** |
||
1242 | * Find a selector by the depth node in the scope |
||
1243 | * |
||
1244 | * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $scope |
||
1245 | * @param integer $depth |
||
1246 | * |
||
1247 | * @return array |
||
1248 | */ |
||
1249 | protected function findScopeSelectors($scope, $depth) |
||
1250 | { |
||
1251 | if ($scope->depth === $depth && $scope->selectors) { |
||
1252 | return $scope->selectors; |
||
1253 | } |
||
1254 | |||
1255 | if ($scope->children) { |
||
1256 | foreach (array_reverse($scope->children) as $c) { |
||
1257 | if ($s = $this->findScopeSelectors($c, $depth)) { |
||
1258 | return $s; |
||
1259 | } |
||
1260 | } |
||
1261 | } |
||
1262 | |||
1263 | return []; |
||
1264 | } |
||
1265 | |||
1266 | /** |
||
1267 | * Compile @at-root's with: inclusion / without: exclusion into 2 lists uses to filter scope/env later |
||
1268 | * |
||
1269 | * @param array $withCondition |
||
1270 | * |
||
1271 | * @return array |
||
1272 | */ |
||
1273 | protected function compileWith($withCondition) |
||
1274 | { |
||
1275 | // just compile what we have in 2 lists |
||
1276 | $with = []; |
||
1277 | $without = ['rule' => true]; |
||
1278 | |||
1279 | if ($withCondition) { |
||
1280 | if ($withCondition[0] === Type::T_INTERPOLATE) { |
||
1281 | $w = $this->compileValue($withCondition); |
||
1282 | |||
1283 | $buffer = "($w)"; |
||
1284 | $parser = $this->parserFactory(__METHOD__); |
||
1285 | |||
1286 | if ($parser->parseValue($buffer, $reParsedWith)) { |
||
1287 | $withCondition = $reParsedWith; |
||
1288 | } |
||
1289 | } |
||
1290 | |||
1291 | if ($this->libMapHasKey([$withCondition, static::$with])) { |
||
1292 | $without = []; // cancel the default |
||
1293 | $list = $this->coerceList($this->libMapGet([$withCondition, static::$with])); |
||
1294 | |||
1295 | foreach ($list[2] as $item) { |
||
1296 | $keyword = $this->compileStringContent($this->coerceString($item)); |
||
1297 | |||
1298 | $with[$keyword] = true; |
||
1299 | } |
||
1300 | } |
||
1301 | |||
1302 | if ($this->libMapHasKey([$withCondition, static::$without])) { |
||
1303 | $without = []; // cancel the default |
||
1304 | $list = $this->coerceList($this->libMapGet([$withCondition, static::$without])); |
||
1305 | |||
1306 | foreach ($list[2] as $item) { |
||
1307 | $keyword = $this->compileStringContent($this->coerceString($item)); |
||
1308 | |||
1309 | $without[$keyword] = true; |
||
1310 | } |
||
1311 | } |
||
1312 | } |
||
1313 | |||
1314 | return [$with, $without]; |
||
1315 | } |
||
1316 | |||
1317 | /** |
||
1318 | * Filter env stack |
||
1319 | * |
||
1320 | * @param array $envs |
||
1321 | * @param array $with |
||
1322 | * @param array $without |
||
1323 | * |
||
1324 | * @return \ScssPhp\ScssPhp\Compiler\Environment |
||
1325 | */ |
||
1326 | protected function filterWithWithout($envs, $with, $without) |
||
1327 | { |
||
1328 | $filtered = []; |
||
1329 | |||
1330 | foreach ($envs as $e) { |
||
1331 | if ($e->block && ! $this->isWith($e->block, $with, $without)) { |
||
1332 | $ec = clone $e; |
||
1333 | $ec->block = null; |
||
1334 | $ec->selectors = []; |
||
1335 | |||
1336 | $filtered[] = $ec; |
||
1337 | } else { |
||
1338 | $filtered[] = $e; |
||
1339 | } |
||
1340 | } |
||
1341 | |||
1342 | return $this->extractEnv($filtered); |
||
1343 | } |
||
1344 | |||
1345 | /** |
||
1346 | * Filter WITH rules |
||
1347 | * |
||
1348 | * @param \ScssPhp\ScssPhp\Block|\ScssPhp\ScssPhp\Formatter\OutputBlock $block |
||
1349 | * @param array $with |
||
1350 | * @param array $without |
||
1351 | * |
||
1352 | * @return boolean |
||
1353 | */ |
||
1354 | protected function isWith($block, $with, $without) |
||
1355 | { |
||
1356 | if (isset($block->type)) { |
||
1357 | if ($block->type === Type::T_MEDIA) { |
||
1358 | return $this->testWithWithout('media', $with, $without); |
||
1359 | } |
||
1360 | |||
1361 | if ($block->type === Type::T_DIRECTIVE) { |
||
1362 | if (isset($block->name)) { |
||
1363 | return $this->testWithWithout($block->name, $with, $without); |
||
1364 | } elseif (isset($block->selectors) && preg_match(',@(\w+),ims', json_encode($block->selectors), $m)) { |
||
1365 | return $this->testWithWithout($m[1], $with, $without); |
||
1366 | } else { |
||
1367 | return $this->testWithWithout('???', $with, $without); |
||
1368 | } |
||
1369 | } |
||
1370 | } elseif (isset($block->selectors)) { |
||
1371 | // a selector starting with number is a keyframe rule |
||
1372 | if (\count($block->selectors)) { |
||
1373 | $s = reset($block->selectors); |
||
1374 | |||
1375 | while (\is_array($s)) { |
||
1376 | $s = reset($s); |
||
1377 | } |
||
1378 | |||
1379 | if (\is_object($s) && $s instanceof Node\Number) { |
||
1380 | return $this->testWithWithout('keyframes', $with, $without); |
||
1381 | } |
||
1382 | } |
||
1383 | |||
1384 | return $this->testWithWithout('rule', $with, $without); |
||
1385 | } |
||
1386 | |||
1387 | return true; |
||
1388 | } |
||
1389 | |||
1390 | /** |
||
1391 | * Test a single type of block against with/without lists |
||
1392 | * |
||
1393 | * @param string $what |
||
1394 | * @param array $with |
||
1395 | * @param array $without |
||
1396 | * |
||
1397 | * @return boolean |
||
1398 | * true if the block should be kept, false to reject |
||
1399 | */ |
||
1400 | protected function testWithWithout($what, $with, $without) |
||
1401 | { |
||
1402 | // if without, reject only if in the list (or 'all' is in the list) |
||
1403 | if (\count($without)) { |
||
1404 | return (isset($without[$what]) || isset($without['all'])) ? false : true; |
||
1405 | } |
||
1406 | |||
1407 | // otherwise reject all what is not in the with list |
||
1408 | return (isset($with[$what]) || isset($with['all'])) ? true : false; |
||
1409 | } |
||
1410 | |||
1411 | |||
1412 | /** |
||
1413 | * Compile keyframe block |
||
1414 | * |
||
1415 | * @param \ScssPhp\ScssPhp\Block $block |
||
1416 | * @param array $selectors |
||
1417 | */ |
||
1418 | protected function compileKeyframeBlock(Block $block, $selectors) |
||
1419 | { |
||
1420 | $env = $this->pushEnv($block); |
||
1421 | |||
1422 | $envs = $this->compactEnv($env); |
||
1423 | |||
1424 | $this->env = $this->extractEnv(array_filter($envs, function (Environment $e) { |
||
1425 | return ! isset($e->block->selectors); |
||
1426 | })); |
||
1427 | |||
1428 | $this->scope = $this->makeOutputBlock($block->type, $selectors); |
||
1429 | $this->scope->depth = 1; |
||
1430 | $this->scope->parent->children[] = $this->scope; |
||
1431 | |||
1432 | $this->compileChildrenNoReturn($block->children, $this->scope); |
||
1433 | |||
1434 | $this->scope = $this->scope->parent; |
||
1435 | $this->env = $this->extractEnv($envs); |
||
1436 | |||
1437 | $this->popEnv(); |
||
1438 | } |
||
1439 | |||
1440 | /** |
||
1441 | * Compile nested properties lines |
||
1442 | * |
||
1443 | * @param \ScssPhp\ScssPhp\Block $block |
||
1444 | * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $out |
||
1445 | */ |
||
1446 | protected function compileNestedPropertiesBlock(Block $block, OutputBlock $out) |
||
1447 | { |
||
1448 | $prefix = $this->compileValue($block->prefix) . '-'; |
||
1449 | |||
1450 | $nested = $this->makeOutputBlock($block->type); |
||
1451 | $nested->parent = $out; |
||
1452 | |||
1453 | if ($block->hasValue) { |
||
1454 | $nested->depth = $out->depth + 1; |
||
1455 | } |
||
1456 | |||
1457 | $out->children[] = $nested; |
||
1458 | |||
1459 | foreach ($block->children as $child) { |
||
1460 | switch ($child[0]) { |
||
1461 | case Type::T_ASSIGN: |
||
1462 | array_unshift($child[1][2], $prefix); |
||
1463 | break; |
||
1464 | |||
1465 | case Type::T_NESTED_PROPERTY: |
||
1466 | array_unshift($child[1]->prefix[2], $prefix); |
||
1467 | break; |
||
1468 | } |
||
1469 | |||
1470 | $this->compileChild($child, $nested); |
||
1471 | } |
||
1472 | } |
||
1473 | |||
1474 | /** |
||
1475 | * Compile nested block |
||
1476 | * |
||
1477 | * @param \ScssPhp\ScssPhp\Block $block |
||
1478 | * @param array $selectors |
||
1479 | */ |
||
1480 | protected function compileNestedBlock(Block $block, $selectors) |
||
1481 | { |
||
1482 | $this->pushEnv($block); |
||
1483 | |||
1484 | $this->scope = $this->makeOutputBlock($block->type, $selectors); |
||
1485 | $this->scope->parent->children[] = $this->scope; |
||
1486 | |||
1487 | // wrap assign children in a block |
||
1488 | // except for @font-face |
||
1489 | if ($block->type !== Type::T_DIRECTIVE || $block->name !== 'font-face') { |
||
1490 | // need wrapping? |
||
1491 | $needWrapping = false; |
||
1492 | |||
1493 | foreach ($block->children as $child) { |
||
1494 | if ($child[0] === Type::T_ASSIGN) { |
||
1495 | $needWrapping = true; |
||
1496 | break; |
||
1497 | } |
||
1498 | } |
||
1499 | |||
1500 | if ($needWrapping) { |
||
1501 | $wrapped = new Block(); |
||
1502 | $wrapped->sourceName = $block->sourceName; |
||
1503 | $wrapped->sourceIndex = $block->sourceIndex; |
||
1504 | $wrapped->sourceLine = $block->sourceLine; |
||
1505 | $wrapped->sourceColumn = $block->sourceColumn; |
||
1506 | $wrapped->selectors = []; |
||
1507 | $wrapped->comments = []; |
||
1508 | $wrapped->parent = $block; |
||
1509 | $wrapped->children = $block->children; |
||
1510 | $wrapped->selfParent = $block->selfParent; |
||
1511 | |||
1512 | $block->children = [[Type::T_BLOCK, $wrapped]]; |
||
1513 | } |
||
1514 | } |
||
1515 | |||
1516 | $this->compileChildrenNoReturn($block->children, $this->scope); |
||
1517 | |||
1518 | $this->scope = $this->scope->parent; |
||
1519 | |||
1520 | $this->popEnv(); |
||
1521 | } |
||
1522 | |||
1523 | /** |
||
1524 | * Recursively compiles a block. |
||
1525 | * |
||
1526 | * A block is analogous to a CSS block in most cases. A single SCSS document |
||
1527 | * is encapsulated in a block when parsed, but it does not have parent tags |
||
1528 | * so all of its children appear on the root level when compiled. |
||
1529 | * |
||
1530 | * Blocks are made up of selectors and children. |
||
1531 | * |
||
1532 | * The children of a block are just all the blocks that are defined within. |
||
1533 | * |
||
1534 | * Compiling the block involves pushing a fresh environment on the stack, |
||
1535 | * and iterating through the props, compiling each one. |
||
1536 | * |
||
1537 | * @see Compiler::compileChild() |
||
1538 | * |
||
1539 | * @param \ScssPhp\ScssPhp\Block $block |
||
1540 | */ |
||
1541 | protected function compileBlock(Block $block) |
||
1542 | { |
||
1543 | $env = $this->pushEnv($block); |
||
1544 | $env->selectors = $this->evalSelectors($block->selectors); |
||
1545 | |||
1546 | $out = $this->makeOutputBlock(null); |
||
1547 | |||
1548 | if (isset($this->lineNumberStyle) && \count($env->selectors) && \count($block->children)) { |
||
1549 | $annotation = $this->makeOutputBlock(Type::T_COMMENT); |
||
1550 | $annotation->depth = 0; |
||
1551 | |||
1552 | $file = $this->sourceNames[$block->sourceIndex]; |
||
1553 | $line = $block->sourceLine; |
||
1554 | |||
1555 | switch ($this->lineNumberStyle) { |
||
1556 | case static::LINE_COMMENTS: |
||
1557 | $annotation->lines[] = '/* line ' . $line |
||
1558 | . ($file ? ', ' . $file : '') |
||
1559 | . ' */'; |
||
1560 | break; |
||
1561 | |||
1562 | case static::DEBUG_INFO: |
||
1563 | $annotation->lines[] = '@media -sass-debug-info{' |
||
1564 | . ($file ? 'filename{font-family:"' . $file . '"}' : '') |
||
1565 | . 'line{font-family:' . $line . '}}'; |
||
1566 | break; |
||
1567 | } |
||
1568 | |||
1569 | $this->scope->children[] = $annotation; |
||
1570 | } |
||
1571 | |||
1572 | $this->scope->children[] = $out; |
||
1573 | |||
1574 | if (\count($block->children)) { |
||
1575 | $out->selectors = $this->multiplySelectors($env, $block->selfParent); |
||
1576 | |||
1577 | // propagate selfParent to the children where they still can be useful |
||
1578 | $selfParentSelectors = null; |
||
1579 | |||
1580 | if (isset($block->selfParent->selectors)) { |
||
1581 | $selfParentSelectors = $block->selfParent->selectors; |
||
1582 | $block->selfParent->selectors = $out->selectors; |
||
1583 | } |
||
1584 | |||
1585 | $this->compileChildrenNoReturn($block->children, $out, $block->selfParent); |
||
1586 | |||
1587 | // and revert for the following children of the same block |
||
1588 | if ($selfParentSelectors) { |
||
1589 | $block->selfParent->selectors = $selfParentSelectors; |
||
1590 | } |
||
1591 | } |
||
1592 | |||
1593 | $this->popEnv(); |
||
1594 | } |
||
1595 | |||
1596 | |||
1597 | /** |
||
1598 | * Compile the value of a comment that can have interpolation |
||
1599 | * |
||
1600 | * @param array $value |
||
1601 | * @param boolean $pushEnv |
||
1602 | * |
||
1603 | * @return array|mixed|string |
||
1604 | */ |
||
1605 | protected function compileCommentValue($value, $pushEnv = false) |
||
1606 | { |
||
1607 | $c = $value[1]; |
||
1608 | |||
1609 | if (isset($value[2])) { |
||
1610 | if ($pushEnv) { |
||
1611 | $this->pushEnv(); |
||
1612 | } |
||
1613 | |||
1614 | $ignoreCallStackMessage = $this->ignoreCallStackMessage; |
||
1615 | $this->ignoreCallStackMessage = true; |
||
1616 | |||
1617 | try { |
||
1618 | $c = $this->compileValue($value[2]); |
||
1619 | } catch (\Exception $e) { |
||
1620 | // ignore error in comment compilation which are only interpolation |
||
1621 | } |
||
1622 | |||
1623 | $this->ignoreCallStackMessage = $ignoreCallStackMessage; |
||
1624 | |||
1625 | if ($pushEnv) { |
||
1626 | $this->popEnv(); |
||
1627 | } |
||
1628 | } |
||
1629 | |||
1630 | return $c; |
||
1631 | } |
||
1632 | |||
1633 | /** |
||
1634 | * Compile root level comment |
||
1635 | * |
||
1636 | * @param array $block |
||
1637 | */ |
||
1638 | protected function compileComment($block) |
||
1639 | { |
||
1640 | $out = $this->makeOutputBlock(Type::T_COMMENT); |
||
1641 | $out->lines[] = $this->compileCommentValue($block, true); |
||
1642 | |||
1643 | $this->scope->children[] = $out; |
||
1644 | } |
||
1645 | |||
1646 | /** |
||
1647 | * Evaluate selectors |
||
1648 | * |
||
1649 | * @param array $selectors |
||
1650 | * |
||
1651 | * @return array |
||
1652 | */ |
||
1653 | protected function evalSelectors($selectors) |
||
1654 | { |
||
1655 | $this->shouldEvaluate = false; |
||
1656 | |||
1657 | $selectors = array_map([$this, 'evalSelector'], $selectors); |
||
1658 | |||
1659 | // after evaluating interpolates, we might need a second pass |
||
1660 | if ($this->shouldEvaluate) { |
||
1661 | $selectors = $this->replaceSelfSelector($selectors, '&'); |
||
1662 | $buffer = $this->collapseSelectors($selectors); |
||
1663 | $parser = $this->parserFactory(__METHOD__); |
||
1664 | |||
1665 | if ($parser->parseSelector($buffer, $newSelectors)) { |
||
1666 | $selectors = array_map([$this, 'evalSelector'], $newSelectors); |
||
1667 | } |
||
1668 | } |
||
1669 | |||
1670 | return $selectors; |
||
1671 | } |
||
1672 | |||
1673 | /** |
||
1674 | * Evaluate selector |
||
1675 | * |
||
1676 | * @param array $selector |
||
1677 | * |
||
1678 | * @return array |
||
1679 | */ |
||
1680 | protected function evalSelector($selector) |
||
1681 | { |
||
1682 | return array_map([$this, 'evalSelectorPart'], $selector); |
||
1683 | } |
||
1684 | |||
1685 | /** |
||
1686 | * Evaluate selector part; replaces all the interpolates, stripping quotes |
||
1687 | * |
||
1688 | * @param array $part |
||
1689 | * |
||
1690 | * @return array |
||
1691 | */ |
||
1692 | protected function evalSelectorPart($part) |
||
1693 | { |
||
1694 | foreach ($part as &$p) { |
||
1695 | if (\is_array($p) && ($p[0] === Type::T_INTERPOLATE || $p[0] === Type::T_STRING)) { |
||
1696 | $p = $this->compileValue($p); |
||
1697 | |||
1698 | // force re-evaluation |
||
1699 | if (strpos($p, '&') !== false || strpos($p, ',') !== false) { |
||
1700 | $this->shouldEvaluate = true; |
||
1701 | } |
||
1702 | } elseif ( |
||
1703 | \is_string($p) && \strlen($p) >= 2 && |
||
1704 | ($first = $p[0]) && ($first === '"' || $first === "'") && |
||
1705 | substr($p, -1) === $first |
||
1706 | ) { |
||
1707 | $p = substr($p, 1, -1); |
||
1708 | } |
||
1709 | } |
||
1710 | |||
1711 | return $this->flattenSelectorSingle($part); |
||
1712 | } |
||
1713 | |||
1714 | /** |
||
1715 | * Collapse selectors |
||
1716 | * |
||
1717 | * @param array $selectors |
||
1718 | * @param boolean $selectorFormat |
||
1719 | * if false return a collapsed string |
||
1720 | * if true return an array description of a structured selector |
||
1721 | * |
||
1722 | * @return string |
||
1723 | */ |
||
1724 | protected function collapseSelectors($selectors, $selectorFormat = false) |
||
1725 | { |
||
1726 | $parts = []; |
||
1727 | |||
1728 | foreach ($selectors as $selector) { |
||
1729 | $output = []; |
||
1730 | $glueNext = false; |
||
1731 | |||
1732 | foreach ($selector as $node) { |
||
1733 | $compound = ''; |
||
1734 | |||
1735 | array_walk_recursive( |
||
1736 | $node, |
||
1737 | function ($value, $key) use (&$compound) { |
||
1738 | $compound .= $value; |
||
1739 | } |
||
1740 | ); |
||
1741 | |||
1742 | if ($selectorFormat && $this->isImmediateRelationshipCombinator($compound)) { |
||
1743 | if (\count($output)) { |
||
1744 | $output[\count($output) - 1] .= ' ' . $compound; |
||
1745 | } else { |
||
1746 | $output[] = $compound; |
||
1747 | } |
||
1748 | |||
1749 | $glueNext = true; |
||
1750 | } elseif ($glueNext) { |
||
1751 | $output[\count($output) - 1] .= ' ' . $compound; |
||
1752 | $glueNext = false; |
||
1753 | } else { |
||
1754 | $output[] = $compound; |
||
1755 | } |
||
1756 | } |
||
1757 | |||
1758 | if ($selectorFormat) { |
||
1759 | foreach ($output as &$o) { |
||
1760 | $o = [Type::T_STRING, '', [$o]]; |
||
1761 | } |
||
1762 | |||
1763 | $output = [Type::T_LIST, ' ', $output]; |
||
1764 | } else { |
||
1765 | $output = implode(' ', $output); |
||
1766 | } |
||
1767 | |||
1768 | $parts[] = $output; |
||
1769 | } |
||
1770 | |||
1771 | if ($selectorFormat) { |
||
1772 | $parts = [Type::T_LIST, ',', $parts]; |
||
1773 | } else { |
||
1774 | $parts = implode(', ', $parts); |
||
1775 | } |
||
1776 | |||
1777 | return $parts; |
||
1778 | } |
||
1779 | |||
1780 | /** |
||
1781 | * Parse down the selector and revert [self] to "&" before a reparsing |
||
1782 | * |
||
1783 | * @param array $selectors |
||
1784 | * |
||
1785 | * @return array |
||
1786 | */ |
||
1787 | protected function replaceSelfSelector($selectors, $replace = null) |
||
1788 | { |
||
1789 | foreach ($selectors as &$part) { |
||
1790 | if (\is_array($part)) { |
||
1791 | if ($part === [Type::T_SELF]) { |
||
1792 | if (\is_null($replace)) { |
||
1793 | $replace = $this->reduce([Type::T_SELF]); |
||
1794 | $replace = $this->compileValue($replace); |
||
1795 | } |
||
1796 | $part = $replace; |
||
1797 | } else { |
||
1798 | $part = $this->replaceSelfSelector($part, $replace); |
||
1799 | } |
||
1800 | } |
||
1801 | } |
||
1802 | |||
1803 | return $selectors; |
||
1804 | } |
||
1805 | |||
1806 | /** |
||
1807 | * Flatten selector single; joins together .classes and #ids |
||
1808 | * |
||
1809 | * @param array $single |
||
1810 | * |
||
1811 | * @return array |
||
1812 | */ |
||
1813 | protected function flattenSelectorSingle($single) |
||
1814 | { |
||
1815 | $joined = []; |
||
1816 | |||
1817 | foreach ($single as $part) { |
||
1818 | if ( |
||
1819 | empty($joined) || |
||
1820 | ! \is_string($part) || |
||
1821 | preg_match('/[\[.:#%]/', $part) |
||
1822 | ) { |
||
1823 | $joined[] = $part; |
||
1824 | continue; |
||
1825 | } |
||
1826 | |||
1827 | if (\is_array(end($joined))) { |
||
1828 | $joined[] = $part; |
||
1829 | } else { |
||
1830 | $joined[\count($joined) - 1] .= $part; |
||
1831 | } |
||
1832 | } |
||
1833 | |||
1834 | return $joined; |
||
1835 | } |
||
1836 | |||
1837 | /** |
||
1838 | * Compile selector to string; self(&) should have been replaced by now |
||
1839 | * |
||
1840 | * @param string|array $selector |
||
1841 | * |
||
1842 | * @return string |
||
1843 | */ |
||
1844 | protected function compileSelector($selector) |
||
1845 | { |
||
1846 | if (! \is_array($selector)) { |
||
1847 | return $selector; // media and the like |
||
1848 | } |
||
1849 | |||
1850 | return implode( |
||
1851 | ' ', |
||
1852 | array_map( |
||
1853 | [$this, 'compileSelectorPart'], |
||
1854 | $selector |
||
1855 | ) |
||
1856 | ); |
||
1857 | } |
||
1858 | |||
1859 | /** |
||
1860 | * Compile selector part |
||
1861 | * |
||
1862 | * @param array $piece |
||
1863 | * |
||
1864 | * @return string |
||
1865 | */ |
||
1866 | protected function compileSelectorPart($piece) |
||
1867 | { |
||
1868 | foreach ($piece as &$p) { |
||
1869 | if (! \is_array($p)) { |
||
1870 | continue; |
||
1871 | } |
||
1872 | |||
1873 | switch ($p[0]) { |
||
1874 | case Type::T_SELF: |
||
1875 | $p = '&'; |
||
1876 | break; |
||
1877 | |||
1878 | default: |
||
1879 | $p = $this->compileValue($p); |
||
1880 | break; |
||
1881 | } |
||
1882 | } |
||
1883 | |||
1884 | return implode($piece); |
||
1885 | } |
||
1886 | |||
1887 | /** |
||
1888 | * Has selector placeholder? |
||
1889 | * |
||
1890 | * @param array $selector |
||
1891 | * |
||
1892 | * @return boolean |
||
1893 | */ |
||
1894 | protected function hasSelectorPlaceholder($selector) |
||
1895 | { |
||
1896 | if (! \is_array($selector)) { |
||
1897 | return false; |
||
1898 | } |
||
1899 | |||
1900 | foreach ($selector as $parts) { |
||
1901 | foreach ($parts as $part) { |
||
1902 | if (\strlen($part) && '%' === $part[0]) { |
||
1903 | return true; |
||
1904 | } |
||
1905 | } |
||
1906 | } |
||
1907 | |||
1908 | return false; |
||
1909 | } |
||
1910 | |||
1911 | protected function pushCallStack($name = '') |
||
1912 | { |
||
1913 | $this->callStack[] = [ |
||
1914 | 'n' => $name, |
||
1915 | Parser::SOURCE_INDEX => $this->sourceIndex, |
||
1916 | Parser::SOURCE_LINE => $this->sourceLine, |
||
1917 | Parser::SOURCE_COLUMN => $this->sourceColumn |
||
1918 | ]; |
||
1919 | |||
1920 | // infinite calling loop |
||
1921 | if (\count($this->callStack) > 25000) { |
||
1922 | // not displayed but you can var_dump it to deep debug |
||
1923 | $msg = $this->callStackMessage(true, 100); |
||
1924 | $msg = 'Infinite calling loop'; |
||
1925 | |||
1926 | throw $this->error($msg); |
||
1927 | } |
||
1928 | } |
||
1929 | |||
1930 | protected function popCallStack() |
||
1931 | { |
||
1932 | array_pop($this->callStack); |
||
1933 | } |
||
1934 | |||
1935 | /** |
||
1936 | * Compile children and return result |
||
1937 | * |
||
1938 | * @param array $stms |
||
1939 | * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $out |
||
1940 | * @param string $traceName |
||
1941 | * |
||
1942 | * @return array|null |
||
1943 | */ |
||
1944 | protected function compileChildren($stms, OutputBlock $out, $traceName = '') |
||
1945 | { |
||
1946 | $this->pushCallStack($traceName); |
||
1947 | |||
1948 | foreach ($stms as $stm) { |
||
1949 | $ret = $this->compileChild($stm, $out); |
||
1950 | |||
1951 | if (isset($ret)) { |
||
1952 | $this->popCallStack(); |
||
1953 | |||
1954 | return $ret; |
||
1955 | } |
||
1956 | } |
||
1957 | |||
1958 | $this->popCallStack(); |
||
1959 | |||
1960 | return null; |
||
1961 | } |
||
1962 | |||
1963 | /** |
||
1964 | * Compile children and throw exception if unexpected @return |
||
1965 | * |
||
1966 | * @param array $stms |
||
1967 | * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $out |
||
1968 | * @param \ScssPhp\ScssPhp\Block $selfParent |
||
1969 | * @param string $traceName |
||
1970 | * |
||
1971 | * @throws \Exception |
||
1972 | */ |
||
1973 | protected function compileChildrenNoReturn($stms, OutputBlock $out, $selfParent = null, $traceName = '') |
||
1974 | { |
||
1975 | $this->pushCallStack($traceName); |
||
1976 | |||
1977 | foreach ($stms as $stm) { |
||
1978 | if ($selfParent && isset($stm[1]) && \is_object($stm[1]) && $stm[1] instanceof Block) { |
||
1979 | $stm[1]->selfParent = $selfParent; |
||
1980 | $ret = $this->compileChild($stm, $out); |
||
1981 | $stm[1]->selfParent = null; |
||
1982 | } elseif ($selfParent && \in_array($stm[0], [TYPE::T_INCLUDE, TYPE::T_EXTEND])) { |
||
1983 | $stm['selfParent'] = $selfParent; |
||
1984 | $ret = $this->compileChild($stm, $out); |
||
1985 | unset($stm['selfParent']); |
||
1986 | } else { |
||
1987 | $ret = $this->compileChild($stm, $out); |
||
1988 | } |
||
1989 | |||
1990 | if (isset($ret)) { |
||
1991 | throw $this->error('@return may only be used within a function'); |
||
1992 | } |
||
1993 | } |
||
1994 | |||
1995 | $this->popCallStack(); |
||
1996 | } |
||
1997 | |||
1998 | |||
1999 | /** |
||
2000 | * evaluate media query : compile internal value keeping the structure inchanged |
||
2001 | * |
||
2002 | * @param array $queryList |
||
2003 | * |
||
2004 | * @return array |
||
2005 | */ |
||
2006 | protected function evaluateMediaQuery($queryList) |
||
2007 | { |
||
2008 | static $parser = null; |
||
2009 | |||
2010 | $outQueryList = []; |
||
2011 | |||
2012 | foreach ($queryList as $kql => $query) { |
||
2013 | $shouldReparse = false; |
||
2014 | |||
2015 | foreach ($query as $kq => $q) { |
||
2016 | for ($i = 1; $i < \count($q); $i++) { |
||
2017 | $value = $this->compileValue($q[$i]); |
||
2018 | |||
2019 | // the parser had no mean to know if media type or expression if it was an interpolation |
||
2020 | // so you need to reparse if the T_MEDIA_TYPE looks like anything else a media type |
||
2021 | if ( |
||
2022 | $q[0] == Type::T_MEDIA_TYPE && |
||
2023 | (strpos($value, '(') !== false || |
||
2024 | strpos($value, ')') !== false || |
||
2025 | strpos($value, ':') !== false || |
||
2026 | strpos($value, ',') !== false) |
||
2027 | ) { |
||
2028 | $shouldReparse = true; |
||
2029 | } |
||
2030 | |||
2031 | $queryList[$kql][$kq][$i] = [Type::T_KEYWORD, $value]; |
||
2032 | } |
||
2033 | } |
||
2034 | |||
2035 | if ($shouldReparse) { |
||
2036 | if (\is_null($parser)) { |
||
2037 | $parser = $this->parserFactory(__METHOD__); |
||
2038 | } |
||
2039 | |||
2040 | $queryString = $this->compileMediaQuery([$queryList[$kql]]); |
||
2041 | $queryString = reset($queryString); |
||
2042 | |||
2043 | if (strpos($queryString, '@media ') === 0) { |
||
2044 | $queryString = substr($queryString, 7); |
||
2045 | $queries = []; |
||
2046 | |||
2047 | if ($parser->parseMediaQueryList($queryString, $queries)) { |
||
2048 | $queries = $this->evaluateMediaQuery($queries[2]); |
||
2049 | |||
2050 | while (\count($queries)) { |
||
2051 | $outQueryList[] = array_shift($queries); |
||
2052 | } |
||
2053 | |||
2054 | continue; |
||
2055 | } |
||
2056 | } |
||
2057 | } |
||
2058 | |||
2059 | $outQueryList[] = $queryList[$kql]; |
||
2060 | } |
||
2061 | |||
2062 | return $outQueryList; |
||
2063 | } |
||
2064 | |||
2065 | /** |
||
2066 | * Compile media query |
||
2067 | * |
||
2068 | * @param array $queryList |
||
2069 | * |
||
2070 | * @return array |
||
2071 | */ |
||
2072 | protected function compileMediaQuery($queryList) |
||
2073 | { |
||
2074 | $start = '@media '; |
||
2075 | $default = trim($start); |
||
2076 | $out = []; |
||
2077 | $current = ''; |
||
2078 | |||
2079 | foreach ($queryList as $query) { |
||
2080 | $type = null; |
||
2081 | $parts = []; |
||
2082 | |||
2083 | $mediaTypeOnly = true; |
||
2084 | |||
2085 | foreach ($query as $q) { |
||
2086 | if ($q[0] !== Type::T_MEDIA_TYPE) { |
||
2087 | $mediaTypeOnly = false; |
||
2088 | break; |
||
2089 | } |
||
2090 | } |
||
2091 | |||
2092 | foreach ($query as $q) { |
||
2093 | switch ($q[0]) { |
||
2094 | case Type::T_MEDIA_TYPE: |
||
2095 | $newType = array_map([$this, 'compileValue'], \array_slice($q, 1)); |
||
2096 | |||
2097 | // combining not and anything else than media type is too risky and should be avoided |
||
2098 | if (! $mediaTypeOnly) { |
||
2099 | if (\in_array(Type::T_NOT, $newType) || ($type && \in_array(Type::T_NOT, $type) )) { |
||
2100 | if ($type) { |
||
2101 | array_unshift($parts, implode(' ', array_filter($type))); |
||
2102 | } |
||
2103 | |||
2104 | if (! empty($parts)) { |
||
2105 | if (\strlen($current)) { |
||
2106 | $current .= $this->formatter->tagSeparator; |
||
2107 | } |
||
2108 | |||
2109 | $current .= implode(' and ', $parts); |
||
2110 | } |
||
2111 | |||
2112 | if ($current) { |
||
2113 | $out[] = $start . $current; |
||
2114 | } |
||
2115 | |||
2116 | $current = ''; |
||
2117 | $type = null; |
||
2118 | $parts = []; |
||
2119 | } |
||
2120 | } |
||
2121 | |||
2122 | if ($newType === ['all'] && $default) { |
||
2123 | $default = $start . 'all'; |
||
2124 | } |
||
2125 | |||
2126 | // all can be safely ignored and mixed with whatever else |
||
2127 | if ($newType !== ['all']) { |
||
2128 | if ($type) { |
||
2129 | $type = $this->mergeMediaTypes($type, $newType); |
||
2130 | |||
2131 | if (empty($type)) { |
||
2132 | // merge failed : ignore this query that is not valid, skip to the next one |
||
2133 | $parts = []; |
||
2134 | $default = ''; // if everything fail, no @media at all |
||
2135 | continue 3; |
||
2136 | } |
||
2137 | } else { |
||
2138 | $type = $newType; |
||
2139 | } |
||
2140 | } |
||
2141 | break; |
||
2142 | |||
2143 | case Type::T_MEDIA_EXPRESSION: |
||
2144 | if (isset($q[2])) { |
||
2145 | $parts[] = '(' |
||
2146 | . $this->compileValue($q[1]) |
||
2147 | . $this->formatter->assignSeparator |
||
2148 | . $this->compileValue($q[2]) |
||
2149 | . ')'; |
||
2150 | } else { |
||
2151 | $parts[] = '(' |
||
2152 | . $this->compileValue($q[1]) |
||
2153 | . ')'; |
||
2154 | } |
||
2155 | break; |
||
2156 | |||
2157 | case Type::T_MEDIA_VALUE: |
||
2158 | $parts[] = $this->compileValue($q[1]); |
||
2159 | break; |
||
2160 | } |
||
2161 | } |
||
2162 | |||
2163 | if ($type) { |
||
2164 | array_unshift($parts, implode(' ', array_filter($type))); |
||
2165 | } |
||
2166 | |||
2167 | if (! empty($parts)) { |
||
2168 | if (\strlen($current)) { |
||
2169 | $current .= $this->formatter->tagSeparator; |
||
2170 | } |
||
2171 | |||
2172 | $current .= implode(' and ', $parts); |
||
2173 | } |
||
2174 | } |
||
2175 | |||
2176 | if ($current) { |
||
2177 | $out[] = $start . $current; |
||
2178 | } |
||
2179 | |||
2180 | // no @media type except all, and no conflict? |
||
2181 | if (! $out && $default) { |
||
2182 | $out[] = $default; |
||
2183 | } |
||
2184 | |||
2185 | return $out; |
||
2186 | } |
||
2187 | |||
2188 | /** |
||
2189 | * Merge direct relationships between selectors |
||
2190 | * |
||
2191 | * @param array $selectors1 |
||
2192 | * @param array $selectors2 |
||
2193 | * |
||
2194 | * @return array |
||
2195 | */ |
||
2196 | protected function mergeDirectRelationships($selectors1, $selectors2) |
||
2197 | { |
||
2198 | if (empty($selectors1) || empty($selectors2)) { |
||
2199 | return array_merge($selectors1, $selectors2); |
||
2200 | } |
||
2201 | |||
2202 | $part1 = end($selectors1); |
||
2203 | $part2 = end($selectors2); |
||
2204 | |||
2205 | if (! $this->isImmediateRelationshipCombinator($part1[0]) && $part1 !== $part2) { |
||
2206 | return array_merge($selectors1, $selectors2); |
||
2207 | } |
||
2208 | |||
2209 | $merged = []; |
||
2210 | |||
2211 | do { |
||
2212 | $part1 = array_pop($selectors1); |
||
2213 | $part2 = array_pop($selectors2); |
||
2214 | |||
2215 | if (! $this->isImmediateRelationshipCombinator($part1[0]) && $part1 !== $part2) { |
||
2216 | if ($this->isImmediateRelationshipCombinator(reset($merged)[0])) { |
||
2217 | array_unshift($merged, [$part1[0] . $part2[0]]); |
||
2218 | $merged = array_merge($selectors1, $selectors2, $merged); |
||
2219 | } else { |
||
2220 | $merged = array_merge($selectors1, [$part1], $selectors2, [$part2], $merged); |
||
2221 | } |
||
2222 | |||
2223 | break; |
||
2224 | } |
||
2225 | |||
2226 | array_unshift($merged, $part1); |
||
2227 | } while (! empty($selectors1) && ! empty($selectors2)); |
||
2228 | |||
2229 | return $merged; |
||
2230 | } |
||
2231 | |||
2232 | /** |
||
2233 | * Merge media types |
||
2234 | * |
||
2235 | * @param array $type1 |
||
2236 | * @param array $type2 |
||
2237 | * |
||
2238 | * @return array|null |
||
2239 | */ |
||
2240 | protected function mergeMediaTypes($type1, $type2) |
||
2241 | { |
||
2242 | if (empty($type1)) { |
||
2243 | return $type2; |
||
2244 | } |
||
2245 | |||
2246 | if (empty($type2)) { |
||
2247 | return $type1; |
||
2248 | } |
||
2249 | |||
2250 | if (\count($type1) > 1) { |
||
2251 | $m1 = strtolower($type1[0]); |
||
2252 | $t1 = strtolower($type1[1]); |
||
2253 | } else { |
||
2254 | $m1 = ''; |
||
2255 | $t1 = strtolower($type1[0]); |
||
2256 | } |
||
2257 | |||
2258 | if (\count($type2) > 1) { |
||
2259 | $m2 = strtolower($type2[0]); |
||
2260 | $t2 = strtolower($type2[1]); |
||
2261 | } else { |
||
2262 | $m2 = ''; |
||
2263 | $t2 = strtolower($type2[0]); |
||
2264 | } |
||
2265 | |||
2266 | if (($m1 === Type::T_NOT) ^ ($m2 === Type::T_NOT)) { |
||
2267 | if ($t1 === $t2) { |
||
2268 | return null; |
||
2269 | } |
||
2270 | |||
2271 | return [ |
||
2272 | $m1 === Type::T_NOT ? $m2 : $m1, |
||
2273 | $m1 === Type::T_NOT ? $t2 : $t1, |
||
2274 | ]; |
||
2275 | } |
||
2276 | |||
2277 | if ($m1 === Type::T_NOT && $m2 === Type::T_NOT) { |
||
2278 | // CSS has no way of representing "neither screen nor print" |
||
2279 | if ($t1 !== $t2) { |
||
2280 | return null; |
||
2281 | } |
||
2282 | |||
2283 | return [Type::T_NOT, $t1]; |
||
2284 | } |
||
2285 | |||
2286 | if ($t1 !== $t2) { |
||
2287 | return null; |
||
2288 | } |
||
2289 | |||
2290 | // t1 == t2, neither m1 nor m2 are "not" |
||
2291 | return [empty($m1) ? $m2 : $m1, $t1]; |
||
2292 | } |
||
2293 | |||
2294 | /** |
||
2295 | * Compile import; returns true if the value was something that could be imported |
||
2296 | * |
||
2297 | * @param array $rawPath |
||
2298 | * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $out |
||
2299 | * @param boolean $once |
||
2300 | * |
||
2301 | * @return boolean |
||
2302 | */ |
||
2303 | protected function compileImport($rawPath, OutputBlock $out, $once = false) |
||
2304 | { |
||
2305 | if ($rawPath[0] === Type::T_STRING) { |
||
2306 | $path = $this->compileStringContent($rawPath); |
||
2307 | |||
2308 | if (strpos($path, 'url(') !== 0 && $path = $this->findImport($path)) { |
||
2309 | if (! $once || ! \in_array($path, $this->importedFiles)) { |
||
2310 | $this->importFile($path, $out); |
||
2311 | $this->importedFiles[] = $path; |
||
2312 | } |
||
2313 | |||
2314 | return true; |
||
2315 | } |
||
2316 | |||
2317 | $this->appendRootDirective('@import ' . $this->compileImportPath($rawPath) . ';', $out); |
||
2318 | |||
2319 | return false; |
||
2320 | } |
||
2321 | |||
2322 | if ($rawPath[0] === Type::T_LIST) { |
||
2323 | // handle a list of strings |
||
2324 | if (\count($rawPath[2]) === 0) { |
||
2325 | return false; |
||
2326 | } |
||
2327 | |||
2328 | foreach ($rawPath[2] as $path) { |
||
2329 | if ($path[0] !== Type::T_STRING) { |
||
2330 | $this->appendRootDirective('@import ' . $this->compileImportPath($rawPath) . ';', $out); |
||
2331 | |||
2332 | return false; |
||
2333 | } |
||
2334 | } |
||
2335 | |||
2336 | foreach ($rawPath[2] as $path) { |
||
2337 | $this->compileImport($path, $out, $once); |
||
2338 | } |
||
2339 | |||
2340 | return true; |
||
2341 | } |
||
2342 | |||
2343 | $this->appendRootDirective('@import ' . $this->compileImportPath($rawPath) . ';', $out); |
||
2344 | |||
2345 | return false; |
||
2346 | } |
||
2347 | |||
2348 | /** |
||
2349 | * @param $rawPath |
||
2350 | * @return string |
||
2351 | * @throws CompilerException |
||
2352 | */ |
||
2353 | protected function compileImportPath($rawPath) |
||
2354 | { |
||
2355 | $path = $this->compileValue($rawPath); |
||
2356 | |||
2357 | // case url() without quotes : supress \r \n remaining in the path |
||
2358 | // if this is a real string there can not be CR or LF char |
||
2359 | if (strpos($path, 'url(') === 0) { |
||
2360 | $path = str_replace(array("\r", "\n"), array('', ' '), $path); |
||
2361 | } else { |
||
2362 | // if this is a file name in a string, spaces shoudl be escaped |
||
2363 | $path = $this->reduce($rawPath); |
||
2364 | $path = $this->escapeImportPathString($path); |
||
2365 | $path = $this->compileValue($path); |
||
2366 | } |
||
2367 | |||
2368 | return $path; |
||
2369 | } |
||
2370 | |||
2371 | /** |
||
2372 | * @param array $path |
||
2373 | * @return array |
||
2374 | * @throws CompilerException |
||
2375 | */ |
||
2376 | protected function escapeImportPathString($path) |
||
2377 | { |
||
2378 | switch ($path[0]) { |
||
2379 | case Type::T_LIST: |
||
2380 | foreach ($path[2] as $k => $v) { |
||
2381 | $path[2][$k] = $this->escapeImportPathString($v); |
||
2382 | } |
||
2383 | break; |
||
2384 | case Type::T_STRING: |
||
2385 | if ($path[1]) { |
||
2386 | $path = $this->compileValue($path); |
||
2387 | $path = str_replace(' ', '\\ ', $path); |
||
2388 | $path = [Type::T_KEYWORD, $path]; |
||
2389 | } |
||
2390 | break; |
||
2391 | } |
||
2392 | |||
2393 | return $path; |
||
2394 | } |
||
2395 | |||
2396 | /** |
||
2397 | * Append a root directive like @import or @charset as near as the possible from the source code |
||
2398 | * (keeping before comments, @import and @charset coming before in the source code) |
||
2399 | * |
||
2400 | * @param string $line |
||
2401 | * @param @param \ScssPhp\ScssPhp\Formatter\OutputBlock $out |
||
2402 | * @param array $allowed |
||
2403 | */ |
||
2404 | protected function appendRootDirective($line, $out, $allowed = [Type::T_COMMENT]) |
||
2405 | { |
||
2406 | $root = $out; |
||
2407 | |||
2408 | while ($root->parent) { |
||
2409 | $root = $root->parent; |
||
2410 | } |
||
2411 | |||
2412 | $i = 0; |
||
2413 | |||
2414 | while ($i < \count($root->children)) { |
||
2415 | if (! isset($root->children[$i]->type) || ! \in_array($root->children[$i]->type, $allowed)) { |
||
2416 | break; |
||
2417 | } |
||
2418 | |||
2419 | $i++; |
||
2420 | } |
||
2421 | |||
2422 | // remove incompatible children from the bottom of the list |
||
2423 | $saveChildren = []; |
||
2424 | |||
2425 | while ($i < \count($root->children)) { |
||
2426 | $saveChildren[] = array_pop($root->children); |
||
2427 | } |
||
2428 | |||
2429 | // insert the directive as a comment |
||
2430 | $child = $this->makeOutputBlock(Type::T_COMMENT); |
||
2431 | $child->lines[] = $line; |
||
2432 | $child->sourceName = $this->sourceNames[$this->sourceIndex]; |
||
2433 | $child->sourceLine = $this->sourceLine; |
||
2434 | $child->sourceColumn = $this->sourceColumn; |
||
2435 | |||
2436 | $root->children[] = $child; |
||
2437 | |||
2438 | // repush children |
||
2439 | while (\count($saveChildren)) { |
||
2440 | $root->children[] = array_pop($saveChildren); |
||
2441 | } |
||
2442 | } |
||
2443 | |||
2444 | /** |
||
2445 | * Append lines to the current output block: |
||
2446 | * directly to the block or through a child if necessary |
||
2447 | * |
||
2448 | * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $out |
||
2449 | * @param string $type |
||
2450 | * @param string|mixed $line |
||
2451 | */ |
||
2452 | protected function appendOutputLine(OutputBlock $out, $type, $line) |
||
2453 | { |
||
2454 | $outWrite = &$out; |
||
2455 | |||
2456 | // check if it's a flat output or not |
||
2457 | if (\count($out->children)) { |
||
2458 | $lastChild = &$out->children[\count($out->children) - 1]; |
||
2459 | |||
2460 | if ( |
||
2461 | $lastChild->depth === $out->depth && |
||
2462 | \is_null($lastChild->selectors) && |
||
2463 | ! \count($lastChild->children) |
||
2464 | ) { |
||
2465 | $outWrite = $lastChild; |
||
2466 | } else { |
||
2467 | $nextLines = $this->makeOutputBlock($type); |
||
2468 | $nextLines->parent = $out; |
||
2469 | $nextLines->depth = $out->depth; |
||
2470 | |||
2471 | $out->children[] = $nextLines; |
||
2472 | $outWrite = &$nextLines; |
||
2473 | } |
||
2474 | } |
||
2475 | |||
2476 | $outWrite->lines[] = $line; |
||
2477 | } |
||
2478 | |||
2479 | /** |
||
2480 | * Compile child; returns a value to halt execution |
||
2481 | * |
||
2482 | * @param array $child |
||
2483 | * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $out |
||
2484 | * |
||
2485 | * @return array |
||
2486 | */ |
||
2487 | protected function compileChild($child, OutputBlock $out) |
||
2488 | { |
||
2489 | if (isset($child[Parser::SOURCE_LINE])) { |
||
2490 | $this->sourceIndex = isset($child[Parser::SOURCE_INDEX]) ? $child[Parser::SOURCE_INDEX] : null; |
||
2491 | $this->sourceLine = isset($child[Parser::SOURCE_LINE]) ? $child[Parser::SOURCE_LINE] : -1; |
||
2492 | $this->sourceColumn = isset($child[Parser::SOURCE_COLUMN]) ? $child[Parser::SOURCE_COLUMN] : -1; |
||
2493 | } elseif (\is_array($child) && isset($child[1]->sourceLine)) { |
||
2494 | $this->sourceIndex = $child[1]->sourceIndex; |
||
2495 | $this->sourceLine = $child[1]->sourceLine; |
||
2496 | $this->sourceColumn = $child[1]->sourceColumn; |
||
2497 | } elseif (! empty($out->sourceLine) && ! empty($out->sourceName)) { |
||
2498 | $this->sourceLine = $out->sourceLine; |
||
2499 | $this->sourceIndex = array_search($out->sourceName, $this->sourceNames); |
||
2500 | $this->sourceColumn = $out->sourceColumn; |
||
2501 | |||
2502 | if ($this->sourceIndex === false) { |
||
2503 | $this->sourceIndex = null; |
||
2504 | } |
||
2505 | } |
||
2506 | |||
2507 | switch ($child[0]) { |
||
2508 | case Type::T_SCSSPHP_IMPORT_ONCE: |
||
2509 | $rawPath = $this->reduce($child[1]); |
||
2510 | |||
2511 | $this->compileImport($rawPath, $out, true); |
||
2512 | break; |
||
2513 | |||
2514 | case Type::T_IMPORT: |
||
2515 | $rawPath = $this->reduce($child[1]); |
||
2516 | |||
2517 | $this->compileImport($rawPath, $out); |
||
2518 | break; |
||
2519 | |||
2520 | case Type::T_DIRECTIVE: |
||
2521 | $this->compileDirective($child[1], $out); |
||
2522 | break; |
||
2523 | |||
2524 | case Type::T_AT_ROOT: |
||
2525 | $this->compileAtRoot($child[1]); |
||
2526 | break; |
||
2527 | |||
2528 | case Type::T_MEDIA: |
||
2529 | $this->compileMedia($child[1]); |
||
2530 | break; |
||
2531 | |||
2532 | case Type::T_BLOCK: |
||
2533 | $this->compileBlock($child[1]); |
||
2534 | break; |
||
2535 | |||
2536 | case Type::T_CHARSET: |
||
2537 | if (! $this->charsetSeen) { |
||
2538 | $this->charsetSeen = true; |
||
2539 | $this->appendRootDirective('@charset ' . $this->compileValue($child[1]) . ';', $out); |
||
2540 | } |
||
2541 | break; |
||
2542 | |||
2543 | case Type::T_CUSTOM_PROPERTY: |
||
2544 | list(, $name, $value) = $child; |
||
2545 | $compiledName = $this->compileValue($name); |
||
2546 | |||
2547 | // if the value reduces to null from something else then |
||
2548 | // the property should be discarded |
||
2549 | if ($value[0] !== Type::T_NULL) { |
||
2550 | $value = $this->reduce($value); |
||
2551 | |||
2552 | if ($value[0] === Type::T_NULL || $value === static::$nullString) { |
||
2553 | break; |
||
2554 | } |
||
2555 | } |
||
2556 | |||
2557 | $compiledValue = $this->compileValue($value); |
||
2558 | |||
2559 | $line = $this->formatter->customProperty( |
||
2560 | $compiledName, |
||
2561 | $compiledValue |
||
2562 | ); |
||
2563 | |||
2564 | $this->appendOutputLine($out, Type::T_ASSIGN, $line); |
||
2565 | break; |
||
2566 | |||
2567 | case Type::T_ASSIGN: |
||
2568 | list(, $name, $value) = $child; |
||
2569 | |||
2570 | if ($name[0] === Type::T_VARIABLE) { |
||
2571 | $flags = isset($child[3]) ? $child[3] : []; |
||
2572 | $isDefault = \in_array('!default', $flags); |
||
2573 | $isGlobal = \in_array('!global', $flags); |
||
2574 | |||
2575 | if ($isGlobal) { |
||
2576 | $this->set($name[1], $this->reduce($value), false, $this->rootEnv, $value); |
||
2577 | break; |
||
2578 | } |
||
2579 | |||
2580 | $shouldSet = $isDefault && |
||
2581 | (\is_null($result = $this->get($name[1], false)) || |
||
2582 | $result === static::$null); |
||
2583 | |||
2584 | if (! $isDefault || $shouldSet) { |
||
2585 | $this->set($name[1], $this->reduce($value), true, null, $value); |
||
2586 | } |
||
2587 | break; |
||
2588 | } |
||
2589 | |||
2590 | $compiledName = $this->compileValue($name); |
||
2591 | |||
2592 | // handle shorthand syntaxes : size / line-height... |
||
2593 | if (\in_array($compiledName, ['font', 'grid-row', 'grid-column', 'border-radius'])) { |
||
2594 | if ($value[0] === Type::T_VARIABLE) { |
||
2595 | // if the font value comes from variable, the content is already reduced |
||
2596 | // (i.e., formulas were already calculated), so we need the original unreduced value |
||
2597 | $value = $this->get($value[1], true, null, true); |
||
2598 | } |
||
2599 | |||
2600 | $shorthandValue=&$value; |
||
2601 | |||
2602 | $shorthandDividerNeedsUnit = false; |
||
2603 | $maxListElements = null; |
||
2604 | $maxShorthandDividers = 1; |
||
2605 | |||
2606 | switch ($compiledName) { |
||
2607 | case 'border-radius': |
||
2608 | $maxListElements = 4; |
||
2609 | $shorthandDividerNeedsUnit = true; |
||
2610 | break; |
||
2611 | } |
||
2612 | |||
2613 | if ($compiledName === 'font' && $value[0] === Type::T_LIST && $value[1] === ',') { |
||
2614 | // this is the case if more than one font is given: example: "font: 400 1em/1.3 arial,helvetica" |
||
2615 | // we need to handle the first list element |
||
2616 | $shorthandValue=&$value[2][0]; |
||
2617 | } |
||
2618 | |||
2619 | if ($shorthandValue[0] === Type::T_EXPRESSION && $shorthandValue[1] === '/') { |
||
2620 | $revert = true; |
||
2621 | |||
2622 | if ($shorthandDividerNeedsUnit) { |
||
2623 | $divider = $shorthandValue[3]; |
||
2624 | |||
2625 | if (\is_array($divider)) { |
||
2626 | $divider = $this->reduce($divider, true); |
||
2627 | } |
||
2628 | |||
2629 | if (\intval($divider->dimension) && ! \count($divider->units)) { |
||
2630 | $revert = false; |
||
2631 | } |
||
2632 | } |
||
2633 | |||
2634 | if ($revert) { |
||
2635 | $shorthandValue = $this->expToString($shorthandValue); |
||
2636 | } |
||
2637 | } elseif ($shorthandValue[0] === Type::T_LIST) { |
||
2638 | foreach ($shorthandValue[2] as &$item) { |
||
2639 | if ($item[0] === Type::T_EXPRESSION && $item[1] === '/') { |
||
2640 | if ($maxShorthandDividers > 0) { |
||
2641 | $revert = true; |
||
2642 | |||
2643 | // if the list of values is too long, this has to be a shorthand, |
||
2644 | // otherwise it could be a real division |
||
2645 | if (\is_null($maxListElements) || \count($shorthandValue[2]) <= $maxListElements) { |
||
2646 | if ($shorthandDividerNeedsUnit) { |
||
2647 | $divider = $item[3]; |
||
2648 | |||
2649 | if (\is_array($divider)) { |
||
2650 | $divider = $this->reduce($divider, true); |
||
2651 | } |
||
2652 | |||
2653 | if (\intval($divider->dimension) && ! \count($divider->units)) { |
||
2654 | $revert = false; |
||
2655 | } |
||
2656 | } |
||
2657 | } |
||
2658 | |||
2659 | if ($revert) { |
||
2660 | $item = $this->expToString($item); |
||
2661 | $maxShorthandDividers--; |
||
2662 | } |
||
2663 | } |
||
2664 | } |
||
2665 | } |
||
2666 | } |
||
2667 | } |
||
2668 | |||
2669 | // if the value reduces to null from something else then |
||
2670 | // the property should be discarded |
||
2671 | if ($value[0] !== Type::T_NULL) { |
||
2672 | $value = $this->reduce($value); |
||
2673 | |||
2674 | if ($value[0] === Type::T_NULL || $value === static::$nullString) { |
||
2675 | break; |
||
2676 | } |
||
2677 | } |
||
2678 | |||
2679 | $compiledValue = $this->compileValue($value); |
||
2680 | |||
2681 | // ignore empty value |
||
2682 | if (\strlen($compiledValue)) { |
||
2683 | $line = $this->formatter->property( |
||
2684 | $compiledName, |
||
2685 | $compiledValue |
||
2686 | ); |
||
2687 | $this->appendOutputLine($out, Type::T_ASSIGN, $line); |
||
2688 | } |
||
2689 | break; |
||
2690 | |||
2691 | case Type::T_COMMENT: |
||
2692 | if ($out->type === Type::T_ROOT) { |
||
2693 | $this->compileComment($child); |
||
2694 | break; |
||
2695 | } |
||
2696 | |||
2697 | $line = $this->compileCommentValue($child, true); |
||
2698 | $this->appendOutputLine($out, Type::T_COMMENT, $line); |
||
2699 | break; |
||
2700 | |||
2701 | case Type::T_MIXIN: |
||
2702 | case Type::T_FUNCTION: |
||
2703 | list(, $block) = $child; |
||
2704 | // the block need to be able to go up to it's parent env to resolve vars |
||
2705 | $block->parentEnv = $this->getStoreEnv(); |
||
2706 | $this->set(static::$namespaces[$block->type] . $block->name, $block, true); |
||
2707 | break; |
||
2708 | |||
2709 | case Type::T_EXTEND: |
||
2710 | foreach ($child[1] as $sel) { |
||
2711 | $sel = $this->replaceSelfSelector($sel); |
||
2712 | $results = $this->evalSelectors([$sel]); |
||
2713 | |||
2714 | foreach ($results as $result) { |
||
2715 | // only use the first one |
||
2716 | $result = current($result); |
||
2717 | $selectors = $out->selectors; |
||
2718 | |||
2719 | if (! $selectors && isset($child['selfParent'])) { |
||
2720 | $selectors = $this->multiplySelectors($this->env, $child['selfParent']); |
||
2721 | } |
||
2722 | |||
2723 | $this->pushExtends($result, $selectors, $child); |
||
2724 | } |
||
2725 | } |
||
2726 | break; |
||
2727 | |||
2728 | case Type::T_IF: |
||
2729 | list(, $if) = $child; |
||
2730 | |||
2731 | if ($this->isTruthy($this->reduce($if->cond, true))) { |
||
2732 | return $this->compileChildren($if->children, $out); |
||
2733 | } |
||
2734 | |||
2735 | foreach ($if->cases as $case) { |
||
2736 | if ( |
||
2737 | $case->type === Type::T_ELSE || |
||
2738 | $case->type === Type::T_ELSEIF && $this->isTruthy($this->reduce($case->cond)) |
||
2739 | ) { |
||
2740 | return $this->compileChildren($case->children, $out); |
||
2741 | } |
||
2742 | } |
||
2743 | break; |
||
2744 | |||
2745 | case Type::T_EACH: |
||
2746 | list(, $each) = $child; |
||
2747 | |||
2748 | $list = $this->coerceList($this->reduce($each->list), ',', true); |
||
2749 | |||
2750 | $this->pushEnv(); |
||
2751 | |||
2752 | foreach ($list[2] as $item) { |
||
2753 | if (\count($each->vars) === 1) { |
||
2754 | $this->set($each->vars[0], $item, true); |
||
2755 | } else { |
||
2756 | list(,, $values) = $this->coerceList($item); |
||
2757 | |||
2758 | foreach ($each->vars as $i => $var) { |
||
2759 | $this->set($var, isset($values[$i]) ? $values[$i] : static::$null, true); |
||
2760 | } |
||
2761 | } |
||
2762 | |||
2763 | $ret = $this->compileChildren($each->children, $out); |
||
2764 | |||
2765 | if ($ret) { |
||
2766 | if ($ret[0] !== Type::T_CONTROL) { |
||
2767 | $store = $this->env->store; |
||
2768 | $this->popEnv(); |
||
2769 | $this->backPropagateEnv($store, $each->vars); |
||
2770 | |||
2771 | return $ret; |
||
2772 | } |
||
2773 | |||
2774 | if ($ret[1]) { |
||
2775 | break; |
||
2776 | } |
||
2777 | } |
||
2778 | } |
||
2779 | $store = $this->env->store; |
||
2780 | $this->popEnv(); |
||
2781 | $this->backPropagateEnv($store, $each->vars); |
||
2782 | |||
2783 | break; |
||
2784 | |||
2785 | case Type::T_WHILE: |
||
2786 | list(, $while) = $child; |
||
2787 | |||
2788 | while ($this->isTruthy($this->reduce($while->cond, true))) { |
||
2789 | $ret = $this->compileChildren($while->children, $out); |
||
2790 | |||
2791 | if ($ret) { |
||
2792 | if ($ret[0] !== Type::T_CONTROL) { |
||
2793 | return $ret; |
||
2794 | } |
||
2795 | |||
2796 | if ($ret[1]) { |
||
2797 | break; |
||
2798 | } |
||
2799 | } |
||
2800 | } |
||
2801 | break; |
||
2802 | |||
2803 | case Type::T_FOR: |
||
2804 | list(, $for) = $child; |
||
2805 | |||
2806 | $start = $this->reduce($for->start, true); |
||
2807 | $end = $this->reduce($for->end, true); |
||
2808 | |||
2809 | if (! $start instanceof Node\Number) { |
||
2810 | throw $this->error('%s is not a number', $start[0]); |
||
2811 | } |
||
2812 | |||
2813 | if (! $end instanceof Node\Number) { |
||
2814 | throw $this->error('%s is not a number', $end[0]); |
||
2815 | } |
||
2816 | |||
2817 | if (! ($start[2] == $end[2] || $end->unitless())) { |
||
2818 | throw $this->error('Incompatible units: "%s" && "%s".', $start->unitStr(), $end->unitStr()); |
||
2819 | } |
||
2820 | |||
2821 | $unit = $start[2]; |
||
2822 | $start = $start[1]; |
||
2823 | $end = $end[1]; |
||
2824 | |||
2825 | $d = $start < $end ? 1 : -1; |
||
2826 | |||
2827 | $this->pushEnv(); |
||
2828 | |||
2829 | for (;;) { |
||
2830 | if ( |
||
2831 | (! $for->until && $start - $d == $end) || |
||
2832 | ($for->until && $start == $end) |
||
2833 | ) { |
||
2834 | break; |
||
2835 | } |
||
2836 | |||
2837 | $this->set($for->var, new Node\Number($start, $unit)); |
||
2838 | $start += $d; |
||
2839 | |||
2840 | $ret = $this->compileChildren($for->children, $out); |
||
2841 | |||
2842 | if ($ret) { |
||
2843 | if ($ret[0] !== Type::T_CONTROL) { |
||
2844 | $store = $this->env->store; |
||
2845 | $this->popEnv(); |
||
2846 | $this->backPropagateEnv($store, [$for->var]); |
||
2847 | |||
2848 | return $ret; |
||
2849 | } |
||
2850 | |||
2851 | if ($ret[1]) { |
||
2852 | break; |
||
2853 | } |
||
2854 | } |
||
2855 | } |
||
2856 | |||
2857 | $store = $this->env->store; |
||
2858 | $this->popEnv(); |
||
2859 | $this->backPropagateEnv($store, [$for->var]); |
||
2860 | |||
2861 | break; |
||
2862 | |||
2863 | case Type::T_BREAK: |
||
2864 | return [Type::T_CONTROL, true]; |
||
2865 | |||
2866 | case Type::T_CONTINUE: |
||
2867 | return [Type::T_CONTROL, false]; |
||
2868 | |||
2869 | case Type::T_RETURN: |
||
2870 | return $this->reduce($child[1], true); |
||
2871 | |||
2872 | case Type::T_NESTED_PROPERTY: |
||
2873 | $this->compileNestedPropertiesBlock($child[1], $out); |
||
2874 | break; |
||
2875 | |||
2876 | case Type::T_INCLUDE: |
||
2877 | // including a mixin |
||
2878 | list(, $name, $argValues, $content, $argUsing) = $child; |
||
2879 | |||
2880 | $mixin = $this->get(static::$namespaces['mixin'] . $name, false); |
||
2881 | |||
2882 | if (! $mixin) { |
||
2883 | throw $this->error("Undefined mixin $name"); |
||
2884 | } |
||
2885 | |||
2886 | $callingScope = $this->getStoreEnv(); |
||
2887 | |||
2888 | // push scope, apply args |
||
2889 | $this->pushEnv(); |
||
2890 | $this->env->depth--; |
||
2891 | |||
2892 | // Find the parent selectors in the env to be able to know what '&' refers to in the mixin |
||
2893 | // and assign this fake parent to childs |
||
2894 | $selfParent = null; |
||
2895 | |||
2896 | if (isset($child['selfParent']) && isset($child['selfParent']->selectors)) { |
||
2897 | $selfParent = $child['selfParent']; |
||
2898 | } else { |
||
2899 | $parentSelectors = $this->multiplySelectors($this->env); |
||
2900 | |||
2901 | if ($parentSelectors) { |
||
2902 | $parent = new Block(); |
||
2903 | $parent->selectors = $parentSelectors; |
||
2904 | |||
2905 | foreach ($mixin->children as $k => $child) { |
||
2906 | if (isset($child[1]) && \is_object($child[1]) && $child[1] instanceof Block) { |
||
2907 | $mixin->children[$k][1]->parent = $parent; |
||
2908 | } |
||
2909 | } |
||
2910 | } |
||
2911 | } |
||
2912 | |||
2913 | // clone the stored content to not have its scope spoiled by a further call to the same mixin |
||
2914 | // i.e., recursive @include of the same mixin |
||
2915 | if (isset($content)) { |
||
2916 | $copyContent = clone $content; |
||
2917 | $copyContent->scope = clone $callingScope; |
||
2918 | |||
2919 | $this->setRaw(static::$namespaces['special'] . 'content', $copyContent, $this->env); |
||
2920 | } else { |
||
2921 | $this->setRaw(static::$namespaces['special'] . 'content', null, $this->env); |
||
2922 | } |
||
2923 | |||
2924 | // save the "using" argument list for applying it to when "@content" is invoked |
||
2925 | if (isset($argUsing)) { |
||
2926 | $this->setRaw(static::$namespaces['special'] . 'using', $argUsing, $this->env); |
||
2927 | } else { |
||
2928 | $this->setRaw(static::$namespaces['special'] . 'using', null, $this->env); |
||
2929 | } |
||
2930 | |||
2931 | if (isset($mixin->args)) { |
||
2932 | $this->applyArguments($mixin->args, $argValues); |
||
2933 | } |
||
2934 | |||
2935 | $this->env->marker = 'mixin'; |
||
2936 | |||
2937 | if (! empty($mixin->parentEnv)) { |
||
2938 | $this->env->declarationScopeParent = $mixin->parentEnv; |
||
2939 | } else { |
||
2940 | throw $this->error("@mixin $name() without parentEnv"); |
||
2941 | } |
||
2942 | |||
2943 | $this->compileChildrenNoReturn($mixin->children, $out, $selfParent, $this->env->marker . ' ' . $name); |
||
2944 | |||
2945 | $this->popEnv(); |
||
2946 | break; |
||
2947 | |||
2948 | case Type::T_MIXIN_CONTENT: |
||
2949 | $env = isset($this->storeEnv) ? $this->storeEnv : $this->env; |
||
2950 | $content = $this->get(static::$namespaces['special'] . 'content', false, $env); |
||
2951 | $argUsing = $this->get(static::$namespaces['special'] . 'using', false, $env); |
||
2952 | $argContent = $child[1]; |
||
2953 | |||
2954 | if (! $content) { |
||
2955 | break; |
||
2956 | } |
||
2957 | |||
2958 | $storeEnv = $this->storeEnv; |
||
2959 | $varsUsing = []; |
||
2960 | |||
2961 | if (isset($argUsing) && isset($argContent)) { |
||
2962 | // Get the arguments provided for the content with the names provided in the "using" argument list |
||
2963 | $this->storeEnv = null; |
||
2964 | $varsUsing = $this->applyArguments($argUsing, $argContent, false); |
||
2965 | } |
||
2966 | |||
2967 | // restore the scope from the @content |
||
2968 | $this->storeEnv = $content->scope; |
||
2969 | |||
2970 | // append the vars from using if any |
||
2971 | foreach ($varsUsing as $name => $val) { |
||
2972 | $this->set($name, $val, true, $this->storeEnv); |
||
2973 | } |
||
2974 | |||
2975 | $this->compileChildrenNoReturn($content->children, $out); |
||
2976 | |||
2977 | $this->storeEnv = $storeEnv; |
||
2978 | break; |
||
2979 | |||
2980 | case Type::T_DEBUG: |
||
2981 | list(, $value) = $child; |
||
2982 | |||
2983 | $fname = $this->sourceNames[$this->sourceIndex]; |
||
2984 | $line = $this->sourceLine; |
||
2985 | $value = $this->compileDebugValue($value); |
||
2986 | |||
2987 | fwrite($this->stderr, "$fname:$line DEBUG: $value\n"); |
||
2988 | break; |
||
2989 | |||
2990 | case Type::T_WARN: |
||
2991 | list(, $value) = $child; |
||
2992 | |||
2993 | $fname = $this->sourceNames[$this->sourceIndex]; |
||
2994 | $line = $this->sourceLine; |
||
2995 | $value = $this->compileDebugValue($value); |
||
2996 | |||
2997 | fwrite($this->stderr, "WARNING: $value\n on line $line of $fname\n\n"); |
||
2998 | break; |
||
2999 | |||
3000 | case Type::T_ERROR: |
||
3001 | list(, $value) = $child; |
||
3002 | |||
3003 | $fname = $this->sourceNames[$this->sourceIndex]; |
||
3004 | $line = $this->sourceLine; |
||
3005 | $value = $this->compileValue($this->reduce($value, true)); |
||
3006 | |||
3007 | throw $this->error("File $fname on line $line ERROR: $value\n"); |
||
3008 | |||
3009 | case Type::T_CONTROL: |
||
3010 | throw $this->error('@break/@continue not permitted in this scope'); |
||
3011 | |||
3012 | default: |
||
3013 | throw $this->error("unknown child type: $child[0]"); |
||
3014 | } |
||
3015 | } |
||
3016 | |||
3017 | /** |
||
3018 | * Reduce expression to string |
||
3019 | * |
||
3020 | * @param array $exp |
||
3021 | * |
||
3022 | * @return array |
||
3023 | */ |
||
3024 | protected function expToString($exp) |
||
3025 | { |
||
3026 | list(, $op, $left, $right, /* $inParens */, $whiteLeft, $whiteRight) = $exp; |
||
3027 | |||
3028 | $content = [$this->reduce($left)]; |
||
3029 | |||
3030 | if ($whiteLeft) { |
||
3031 | $content[] = ' '; |
||
3032 | } |
||
3033 | |||
3034 | $content[] = $op; |
||
3035 | |||
3036 | if ($whiteRight) { |
||
3037 | $content[] = ' '; |
||
3038 | } |
||
3039 | |||
3040 | $content[] = $this->reduce($right); |
||
3041 | |||
3042 | return [Type::T_STRING, '', $content]; |
||
3043 | } |
||
3044 | |||
3045 | /** |
||
3046 | * Is truthy? |
||
3047 | * |
||
3048 | * @param array $value |
||
3049 | * |
||
3050 | * @return boolean |
||
3051 | */ |
||
3052 | protected function isTruthy($value) |
||
3053 | { |
||
3054 | return $value !== static::$false && $value !== static::$null; |
||
3055 | } |
||
3056 | |||
3057 | /** |
||
3058 | * Is the value a direct relationship combinator? |
||
3059 | * |
||
3060 | * @param string $value |
||
3061 | * |
||
3062 | * @return boolean |
||
3063 | */ |
||
3064 | protected function isImmediateRelationshipCombinator($value) |
||
3065 | { |
||
3066 | return $value === '>' || $value === '+' || $value === '~'; |
||
3067 | } |
||
3068 | |||
3069 | /** |
||
3070 | * Should $value cause its operand to eval |
||
3071 | * |
||
3072 | * @param array $value |
||
3073 | * |
||
3074 | * @return boolean |
||
3075 | */ |
||
3076 | protected function shouldEval($value) |
||
3077 | { |
||
3078 | switch ($value[0]) { |
||
3079 | case Type::T_EXPRESSION: |
||
3080 | if ($value[1] === '/') { |
||
3081 | return $this->shouldEval($value[2]) || $this->shouldEval($value[3]); |
||
3082 | } |
||
3083 | |||
3084 | // fall-thru |
||
3085 | case Type::T_VARIABLE: |
||
3086 | case Type::T_FUNCTION_CALL: |
||
3087 | return true; |
||
3088 | } |
||
3089 | |||
3090 | return false; |
||
3091 | } |
||
3092 | |||
3093 | /** |
||
3094 | * Reduce value |
||
3095 | * |
||
3096 | * @param array $value |
||
3097 | * @param boolean $inExp |
||
3098 | * |
||
3099 | * @return null|string|array|\ScssPhp\ScssPhp\Node\Number |
||
3100 | */ |
||
3101 | protected function reduce($value, $inExp = false) |
||
3102 | { |
||
3103 | if (\is_null($value)) { |
||
3104 | return null; |
||
3105 | } |
||
3106 | |||
3107 | switch ($value[0]) { |
||
3108 | case Type::T_EXPRESSION: |
||
3109 | list(, $op, $left, $right, $inParens) = $value; |
||
3110 | |||
3111 | $opName = isset(static::$operatorNames[$op]) ? static::$operatorNames[$op] : $op; |
||
3112 | $inExp = $inExp || $this->shouldEval($left) || $this->shouldEval($right); |
||
3113 | |||
3114 | $left = $this->reduce($left, true); |
||
3115 | |||
3116 | if ($op !== 'and' && $op !== 'or') { |
||
3117 | $right = $this->reduce($right, true); |
||
3118 | } |
||
3119 | |||
3120 | // special case: looks like css shorthand |
||
3121 | if ( |
||
3122 | $opName == 'div' && ! $inParens && ! $inExp && isset($right[2]) && |
||
3123 | (($right[0] !== Type::T_NUMBER && $right[2] != '') || |
||
3124 | ($right[0] === Type::T_NUMBER && ! $right->unitless())) |
||
3125 | ) { |
||
3126 | return $this->expToString($value); |
||
3127 | } |
||
3128 | |||
3129 | $left = $this->coerceForExpression($left); |
||
3130 | $right = $this->coerceForExpression($right); |
||
3131 | $ltype = $left[0]; |
||
3132 | $rtype = $right[0]; |
||
3133 | |||
3134 | $ucOpName = ucfirst($opName); |
||
3135 | $ucLType = ucfirst($ltype); |
||
3136 | $ucRType = ucfirst($rtype); |
||
3137 | |||
3138 | // this tries: |
||
3139 | // 1. op[op name][left type][right type] |
||
3140 | // 2. op[left type][right type] (passing the op as first arg |
||
3141 | // 3. op[op name] |
||
3142 | $fn = "op${ucOpName}${ucLType}${ucRType}"; |
||
3143 | |||
3144 | if ( |
||
3145 | \is_callable([$this, $fn]) || |
||
3146 | (($fn = "op${ucLType}${ucRType}") && |
||
3147 | \is_callable([$this, $fn]) && |
||
3148 | $passOp = true) || |
||
3149 | (($fn = "op${ucOpName}") && |
||
3150 | \is_callable([$this, $fn]) && |
||
3151 | $genOp = true) |
||
3152 | ) { |
||
3153 | $coerceUnit = false; |
||
3154 | |||
3155 | if ( |
||
3156 | ! isset($genOp) && |
||
3157 | $left[0] === Type::T_NUMBER && $right[0] === Type::T_NUMBER |
||
3158 | ) { |
||
3159 | $coerceUnit = true; |
||
3160 | |||
3161 | switch ($opName) { |
||
3162 | case 'mul': |
||
3163 | $targetUnit = $left[2]; |
||
3164 | |||
3165 | foreach ($right[2] as $unit => $exp) { |
||
3166 | $targetUnit[$unit] = (isset($targetUnit[$unit]) ? $targetUnit[$unit] : 0) + $exp; |
||
3167 | } |
||
3168 | break; |
||
3169 | |||
3170 | case 'div': |
||
3171 | $targetUnit = $left[2]; |
||
3172 | |||
3173 | foreach ($right[2] as $unit => $exp) { |
||
3174 | $targetUnit[$unit] = (isset($targetUnit[$unit]) ? $targetUnit[$unit] : 0) - $exp; |
||
3175 | } |
||
3176 | break; |
||
3177 | |||
3178 | case 'mod': |
||
3179 | $targetUnit = $left[2]; |
||
3180 | break; |
||
3181 | |||
3182 | default: |
||
3183 | $targetUnit = $left->unitless() ? $right[2] : $left[2]; |
||
3184 | } |
||
3185 | |||
3186 | $baseUnitLeft = $left->isNormalizable(); |
||
3187 | $baseUnitRight = $right->isNormalizable(); |
||
3188 | |||
3189 | if ($baseUnitLeft && $baseUnitRight && $baseUnitLeft === $baseUnitRight) { |
||
3190 | $left = $left->normalize(); |
||
3191 | $right = $right->normalize(); |
||
3192 | } elseif ($coerceUnit) { |
||
3193 | $left = new Node\Number($left[1], []); |
||
3194 | } |
||
3195 | } |
||
3196 | |||
3197 | $shouldEval = $inParens || $inExp; |
||
3198 | |||
3199 | if (isset($passOp)) { |
||
3200 | $out = $this->$fn($op, $left, $right, $shouldEval); |
||
3201 | } else { |
||
3202 | $out = $this->$fn($left, $right, $shouldEval); |
||
3203 | } |
||
3204 | |||
3205 | if (isset($out)) { |
||
3206 | if ($coerceUnit && $out[0] === Type::T_NUMBER) { |
||
3207 | $out = $out->coerce($targetUnit); |
||
3208 | } |
||
3209 | |||
3210 | return $out; |
||
3211 | } |
||
3212 | } |
||
3213 | |||
3214 | return $this->expToString($value); |
||
3215 | |||
3216 | case Type::T_UNARY: |
||
3217 | list(, $op, $exp, $inParens) = $value; |
||
3218 | |||
3219 | $inExp = $inExp || $this->shouldEval($exp); |
||
3220 | $exp = $this->reduce($exp); |
||
3221 | |||
3222 | if ($exp[0] === Type::T_NUMBER) { |
||
3223 | switch ($op) { |
||
3224 | case '+': |
||
3225 | return new Node\Number($exp[1], $exp[2]); |
||
3226 | |||
3227 | case '-': |
||
3228 | return new Node\Number(-$exp[1], $exp[2]); |
||
3229 | } |
||
3230 | } |
||
3231 | |||
3232 | if ($op === 'not') { |
||
3233 | if ($inExp || $inParens) { |
||
3234 | if ($exp === static::$false || $exp === static::$null) { |
||
3235 | return static::$true; |
||
3236 | } |
||
3237 | |||
3238 | return static::$false; |
||
3239 | } |
||
3240 | |||
3241 | $op = $op . ' '; |
||
3242 | } |
||
3243 | |||
3244 | return [Type::T_STRING, '', [$op, $exp]]; |
||
3245 | |||
3246 | case Type::T_VARIABLE: |
||
3247 | return $this->reduce($this->get($value[1])); |
||
3248 | |||
3249 | case Type::T_LIST: |
||
3250 | foreach ($value[2] as &$item) { |
||
3251 | $item = $this->reduce($item); |
||
3252 | } |
||
3253 | |||
3254 | return $value; |
||
3255 | |||
3256 | case Type::T_MAP: |
||
3257 | foreach ($value[1] as &$item) { |
||
3258 | $item = $this->reduce($item); |
||
3259 | } |
||
3260 | |||
3261 | foreach ($value[2] as &$item) { |
||
3262 | $item = $this->reduce($item); |
||
3263 | } |
||
3264 | |||
3265 | return $value; |
||
3266 | |||
3267 | case Type::T_STRING: |
||
3268 | foreach ($value[2] as &$item) { |
||
3269 | if (\is_array($item) || $item instanceof \ArrayAccess) { |
||
3270 | $item = $this->reduce($item); |
||
3271 | } |
||
3272 | } |
||
3273 | |||
3274 | return $value; |
||
3275 | |||
3276 | case Type::T_INTERPOLATE: |
||
3277 | $value[1] = $this->reduce($value[1]); |
||
3278 | |||
3279 | if ($inExp) { |
||
3280 | return $value[1]; |
||
3281 | } |
||
3282 | |||
3283 | return $value; |
||
3284 | |||
3285 | case Type::T_FUNCTION_CALL: |
||
3286 | return $this->fncall($value[1], $value[2]); |
||
3287 | |||
3288 | case Type::T_SELF: |
||
3289 | $selfParent = ! empty($this->env->block->selfParent) ? $this->env->block->selfParent : null; |
||
3290 | $selfSelector = $this->multiplySelectors($this->env, $selfParent); |
||
3291 | $selfSelector = $this->collapseSelectors($selfSelector, true); |
||
3292 | |||
3293 | return $selfSelector; |
||
3294 | |||
3295 | default: |
||
3296 | return $value; |
||
3297 | } |
||
3298 | } |
||
3299 | |||
3300 | /** |
||
3301 | * Function caller |
||
3302 | * |
||
3303 | * @param string $name |
||
3304 | * @param array $argValues |
||
3305 | * |
||
3306 | * @return array|null |
||
3307 | */ |
||
3308 | protected function fncall($functionReference, $argValues) |
||
3309 | { |
||
3310 | // a string means this is a static hard reference coming from the parsing |
||
3311 | if (is_string($functionReference)) { |
||
3312 | $name = $functionReference; |
||
3313 | |||
3314 | $functionReference = $this->getFunctionReference($name); |
||
3315 | if ($functionReference === static::$null || $functionReference[0] !== Type::T_FUNCTION_REFERENCE) { |
||
3316 | $functionReference = [Type::T_FUNCTION, $name, [Type::T_LIST, ',', []]]; |
||
3317 | } |
||
3318 | } |
||
3319 | |||
3320 | // a function type means we just want a plain css function call |
||
3321 | if ($functionReference[0] === Type::T_FUNCTION) { |
||
3322 | // for CSS functions, simply flatten the arguments into a list |
||
3323 | $listArgs = []; |
||
3324 | |||
3325 | foreach ((array) $argValues as $arg) { |
||
3326 | if (empty($arg[0]) || count($argValues) === 1) { |
||
3327 | $listArgs[] = $this->reduce($this->stringifyFncallArgs($arg[1])); |
||
3328 | } |
||
3329 | } |
||
3330 | |||
3331 | return [Type::T_FUNCTION, $functionReference[1], [Type::T_LIST, ',', $listArgs]]; |
||
3332 | } |
||
3333 | |||
3334 | if ($functionReference === static::$null || $functionReference[0] !== Type::T_FUNCTION_REFERENCE) { |
||
3335 | return static::$defaultValue; |
||
3336 | } |
||
3337 | |||
3338 | |||
3339 | switch ($functionReference[1]) { |
||
3340 | // SCSS @function |
||
3341 | case 'scss': |
||
3342 | return $this->callScssFunction($functionReference[3], $argValues); |
||
3343 | |||
3344 | // native PHP functions |
||
3345 | case 'user': |
||
3346 | case 'native': |
||
3347 | list(,,$name, $fn, $prototype) = $functionReference; |
||
3348 | $returnValue = $this->callNativeFunction($name, $fn, $prototype, $argValues); |
||
3349 | |||
3350 | if (! isset($returnValue)) { |
||
3351 | return $this->fncall([Type::T_FUNCTION, $name, [Type::T_LIST, ',', []]], $argValues); |
||
3352 | } |
||
3353 | |||
3354 | return $returnValue; |
||
3355 | |||
3356 | default: |
||
3357 | return static::$defaultValue; |
||
3358 | } |
||
3359 | } |
||
3360 | |||
3361 | /** |
||
3362 | * Reformat fncall arguments to proper css function output |
||
3363 | * @param $arg |
||
3364 | * @return array|\ArrayAccess|Node\Number|string|null |
||
3365 | */ |
||
3366 | protected function stringifyFncallArgs($arg) |
||
3367 | { |
||
3368 | |||
3369 | switch ($arg[0]) { |
||
3370 | case Type::T_LIST: |
||
3371 | foreach ($arg[2] as $k => $v) { |
||
3372 | $arg[2][$k] = $this->stringifyFncallArgs($v); |
||
3373 | } |
||
3374 | break; |
||
3375 | |||
3376 | case Type::T_EXPRESSION: |
||
3377 | if ($arg[1] === '/') { |
||
3378 | $arg[2] = $this->stringifyFncallArgs($arg[2]); |
||
3379 | $arg[3] = $this->stringifyFncallArgs($arg[3]); |
||
3380 | $arg[5] = $arg[6] = false; // no space around / |
||
3381 | $arg = $this->expToString($arg); |
||
3382 | } |
||
3383 | break; |
||
3384 | |||
3385 | case Type::T_FUNCTION_CALL: |
||
3386 | $name = $arg[1]; |
||
3387 | |||
3388 | if (in_array($name, ['max', 'min', 'calc'])) { |
||
3389 | $args = $arg[2]; |
||
3390 | $arg = $this->fncall([Type::T_FUNCTION, $name, [Type::T_LIST, ',', []]], $args); |
||
3391 | } |
||
3392 | break; |
||
3393 | } |
||
3394 | |||
3395 | return $arg; |
||
3396 | } |
||
3397 | |||
3398 | /** |
||
3399 | * Find a function reference |
||
3400 | * @param string $name |
||
3401 | * @param bool $safeCopy |
||
3402 | * @return array |
||
3403 | */ |
||
3404 | protected function getFunctionReference($name, $safeCopy = false) |
||
3405 | { |
||
3406 | // SCSS @function |
||
3407 | if ($func = $this->get(static::$namespaces['function'] . $name, false)) { |
||
3408 | if ($safeCopy) { |
||
3409 | $func = clone $func; |
||
3410 | } |
||
3411 | |||
3412 | return [Type::T_FUNCTION_REFERENCE, 'scss', $name, $func]; |
||
3413 | } |
||
3414 | |||
3415 | // native PHP functions |
||
3416 | |||
3417 | // try to find a native lib function |
||
3418 | $normalizedName = $this->normalizeName($name); |
||
3419 | $libName = null; |
||
3420 | |||
3421 | if (isset($this->userFunctions[$normalizedName])) { |
||
3422 | // see if we can find a user function |
||
3423 | list($f, $prototype) = $this->userFunctions[$normalizedName]; |
||
3424 | |||
3425 | return [Type::T_FUNCTION_REFERENCE, 'user', $name, $f, $prototype]; |
||
3426 | } |
||
3427 | |||
3428 | if (($f = $this->getBuiltinFunction($normalizedName)) && \is_callable($f)) { |
||
3429 | $libName = $f[1]; |
||
3430 | $prototype = isset(static::$$libName) ? static::$$libName : null; |
||
3431 | |||
3432 | return [Type::T_FUNCTION_REFERENCE, 'native', $name, $f, $prototype]; |
||
3433 | } |
||
3434 | |||
3435 | return static::$null; |
||
3436 | } |
||
3437 | |||
3438 | |||
3439 | /** |
||
3440 | * Normalize name |
||
3441 | * |
||
3442 | * @param string $name |
||
3443 | * |
||
3444 | * @return string |
||
3445 | */ |
||
3446 | protected function normalizeName($name) |
||
3447 | { |
||
3448 | return str_replace('-', '_', $name); |
||
3449 | } |
||
3450 | |||
3451 | /** |
||
3452 | * Normalize value |
||
3453 | * |
||
3454 | * @param array $value |
||
3455 | * |
||
3456 | * @return array |
||
3457 | */ |
||
3458 | public function normalizeValue($value) |
||
3459 | { |
||
3460 | $value = $this->coerceForExpression($this->reduce($value)); |
||
3461 | |||
3462 | switch ($value[0]) { |
||
3463 | case Type::T_LIST: |
||
3464 | $value = $this->extractInterpolation($value); |
||
3465 | |||
3466 | if ($value[0] !== Type::T_LIST) { |
||
3467 | return [Type::T_KEYWORD, $this->compileValue($value)]; |
||
3468 | } |
||
3469 | |||
3470 | foreach ($value[2] as $key => $item) { |
||
3471 | $value[2][$key] = $this->normalizeValue($item); |
||
3472 | } |
||
3473 | |||
3474 | if (! empty($value['enclosing'])) { |
||
3475 | unset($value['enclosing']); |
||
3476 | } |
||
3477 | |||
3478 | return $value; |
||
3479 | |||
3480 | case Type::T_STRING: |
||
3481 | return [$value[0], '"', [$this->compileStringContent($value)]]; |
||
3482 | |||
3483 | case Type::T_NUMBER: |
||
3484 | return $value->normalize(); |
||
3485 | |||
3486 | case Type::T_INTERPOLATE: |
||
3487 | return [Type::T_KEYWORD, $this->compileValue($value)]; |
||
3488 | |||
3489 | default: |
||
3490 | return $value; |
||
3491 | } |
||
3492 | } |
||
3493 | |||
3494 | /** |
||
3495 | * Add numbers |
||
3496 | * |
||
3497 | * @param array $left |
||
3498 | * @param array $right |
||
3499 | * |
||
3500 | * @return \ScssPhp\ScssPhp\Node\Number |
||
3501 | */ |
||
3502 | protected function opAddNumberNumber($left, $right) |
||
3503 | { |
||
3504 | return new Node\Number($left[1] + $right[1], $left[2]); |
||
3505 | } |
||
3506 | |||
3507 | /** |
||
3508 | * Multiply numbers |
||
3509 | * |
||
3510 | * @param array $left |
||
3511 | * @param array $right |
||
3512 | * |
||
3513 | * @return \ScssPhp\ScssPhp\Node\Number |
||
3514 | */ |
||
3515 | protected function opMulNumberNumber($left, $right) |
||
3516 | { |
||
3517 | return new Node\Number($left[1] * $right[1], $left[2]); |
||
3518 | } |
||
3519 | |||
3520 | /** |
||
3521 | * Subtract numbers |
||
3522 | * |
||
3523 | * @param array $left |
||
3524 | * @param array $right |
||
3525 | * |
||
3526 | * @return \ScssPhp\ScssPhp\Node\Number |
||
3527 | */ |
||
3528 | protected function opSubNumberNumber($left, $right) |
||
3529 | { |
||
3530 | return new Node\Number($left[1] - $right[1], $left[2]); |
||
3531 | } |
||
3532 | |||
3533 | /** |
||
3534 | * Divide numbers |
||
3535 | * |
||
3536 | * @param array $left |
||
3537 | * @param array $right |
||
3538 | * |
||
3539 | * @return array|\ScssPhp\ScssPhp\Node\Number |
||
3540 | */ |
||
3541 | protected function opDivNumberNumber($left, $right) |
||
3542 | { |
||
3543 | if ($right[1] == 0) { |
||
3544 | return ($left[1] == 0) ? static::$NaN : static::$Infinity; |
||
3545 | } |
||
3546 | |||
3547 | return new Node\Number($left[1] / $right[1], $left[2]); |
||
3548 | } |
||
3549 | |||
3550 | /** |
||
3551 | * Mod numbers |
||
3552 | * |
||
3553 | * @param array $left |
||
3554 | * @param array $right |
||
3555 | * |
||
3556 | * @return \ScssPhp\ScssPhp\Node\Number |
||
3557 | */ |
||
3558 | protected function opModNumberNumber($left, $right) |
||
3559 | { |
||
3560 | if ($right[1] == 0) { |
||
3561 | return static::$NaN; |
||
3562 | } |
||
3563 | |||
3564 | return new Node\Number($left[1] % $right[1], $left[2]); |
||
3565 | } |
||
3566 | |||
3567 | /** |
||
3568 | * Add strings |
||
3569 | * |
||
3570 | * @param array $left |
||
3571 | * @param array $right |
||
3572 | * |
||
3573 | * @return array|null |
||
3574 | */ |
||
3575 | protected function opAdd($left, $right) |
||
3576 | { |
||
3577 | if ($strLeft = $this->coerceString($left)) { |
||
3578 | if ($right[0] === Type::T_STRING) { |
||
3579 | $right[1] = ''; |
||
3580 | } |
||
3581 | |||
3582 | $strLeft[2][] = $right; |
||
3583 | |||
3584 | return $strLeft; |
||
3585 | } |
||
3586 | |||
3587 | if ($strRight = $this->coerceString($right)) { |
||
3588 | if ($left[0] === Type::T_STRING) { |
||
3589 | $left[1] = ''; |
||
3590 | } |
||
3591 | |||
3592 | array_unshift($strRight[2], $left); |
||
3593 | |||
3594 | return $strRight; |
||
3595 | } |
||
3596 | |||
3597 | return null; |
||
3598 | } |
||
3599 | |||
3600 | /** |
||
3601 | * Boolean and |
||
3602 | * |
||
3603 | * @param array $left |
||
3604 | * @param array $right |
||
3605 | * @param boolean $shouldEval |
||
3606 | * |
||
3607 | * @return array|null |
||
3608 | */ |
||
3609 | protected function opAnd($left, $right, $shouldEval) |
||
3610 | { |
||
3611 | $truthy = ($left === static::$null || $right === static::$null) || |
||
3612 | ($left === static::$false || $left === static::$true) && |
||
3613 | ($right === static::$false || $right === static::$true); |
||
3614 | |||
3615 | if (! $shouldEval) { |
||
3616 | if (! $truthy) { |
||
3617 | return null; |
||
3618 | } |
||
3619 | } |
||
3620 | |||
3621 | if ($left !== static::$false && $left !== static::$null) { |
||
3622 | return $this->reduce($right, true); |
||
3623 | } |
||
3624 | |||
3625 | return $left; |
||
3626 | } |
||
3627 | |||
3628 | /** |
||
3629 | * Boolean or |
||
3630 | * |
||
3631 | * @param array $left |
||
3632 | * @param array $right |
||
3633 | * @param boolean $shouldEval |
||
3634 | * |
||
3635 | * @return array|null |
||
3636 | */ |
||
3637 | protected function opOr($left, $right, $shouldEval) |
||
3638 | { |
||
3639 | $truthy = ($left === static::$null || $right === static::$null) || |
||
3640 | ($left === static::$false || $left === static::$true) && |
||
3641 | ($right === static::$false || $right === static::$true); |
||
3642 | |||
3643 | if (! $shouldEval) { |
||
3644 | if (! $truthy) { |
||
3645 | return null; |
||
3646 | } |
||
3647 | } |
||
3648 | |||
3649 | if ($left !== static::$false && $left !== static::$null) { |
||
3650 | return $left; |
||
3651 | } |
||
3652 | |||
3653 | return $this->reduce($right, true); |
||
3654 | } |
||
3655 | |||
3656 | /** |
||
3657 | * Compare colors |
||
3658 | * |
||
3659 | * @param string $op |
||
3660 | * @param array $left |
||
3661 | * @param array $right |
||
3662 | * |
||
3663 | * @return array |
||
3664 | */ |
||
3665 | protected function opColorColor($op, $left, $right) |
||
3666 | { |
||
3667 | $out = [Type::T_COLOR]; |
||
3668 | |||
3669 | foreach ([1, 2, 3] as $i) { |
||
3670 | $lval = isset($left[$i]) ? $left[$i] : 0; |
||
3671 | $rval = isset($right[$i]) ? $right[$i] : 0; |
||
3672 | |||
3673 | switch ($op) { |
||
3674 | case '+': |
||
3675 | $out[] = $lval + $rval; |
||
3676 | break; |
||
3677 | |||
3678 | case '-': |
||
3679 | $out[] = $lval - $rval; |
||
3680 | break; |
||
3681 | |||
3682 | case '*': |
||
3683 | $out[] = $lval * $rval; |
||
3684 | break; |
||
3685 | |||
3686 | case '%': |
||
3687 | if ($rval == 0) { |
||
3688 | throw $this->error("color: Can't take modulo by zero"); |
||
3689 | } |
||
3690 | |||
3691 | $out[] = $lval % $rval; |
||
3692 | break; |
||
3693 | |||
3694 | case '/': |
||
3695 | if ($rval == 0) { |
||
3696 | throw $this->error("color: Can't divide by zero"); |
||
3697 | } |
||
3698 | |||
3699 | $out[] = (int) ($lval / $rval); |
||
3700 | break; |
||
3701 | |||
3702 | case '==': |
||
3703 | return $this->opEq($left, $right); |
||
3704 | |||
3705 | case '!=': |
||
3706 | return $this->opNeq($left, $right); |
||
3707 | |||
3708 | default: |
||
3709 | throw $this->error("color: unknown op $op"); |
||
3710 | } |
||
3711 | } |
||
3712 | |||
3713 | if (isset($left[4])) { |
||
3714 | $out[4] = $left[4]; |
||
3715 | } elseif (isset($right[4])) { |
||
3716 | $out[4] = $right[4]; |
||
3717 | } |
||
3718 | |||
3719 | return $this->fixColor($out); |
||
3720 | } |
||
3721 | |||
3722 | /** |
||
3723 | * Compare color and number |
||
3724 | * |
||
3725 | * @param string $op |
||
3726 | * @param array $left |
||
3727 | * @param array $right |
||
3728 | * |
||
3729 | * @return array |
||
3730 | */ |
||
3731 | protected function opColorNumber($op, $left, $right) |
||
3732 | { |
||
3733 | $value = $right[1]; |
||
3734 | |||
3735 | return $this->opColorColor( |
||
3736 | $op, |
||
3737 | $left, |
||
3738 | [Type::T_COLOR, $value, $value, $value] |
||
3739 | ); |
||
3740 | } |
||
3741 | |||
3742 | /** |
||
3743 | * Compare number and color |
||
3744 | * |
||
3745 | * @param string $op |
||
3746 | * @param array $left |
||
3747 | * @param array $right |
||
3748 | * |
||
3749 | * @return array |
||
3750 | */ |
||
3751 | protected function opNumberColor($op, $left, $right) |
||
3752 | { |
||
3753 | $value = $left[1]; |
||
3754 | |||
3755 | return $this->opColorColor( |
||
3756 | $op, |
||
3757 | [Type::T_COLOR, $value, $value, $value], |
||
3758 | $right |
||
3759 | ); |
||
3760 | } |
||
3761 | |||
3762 | /** |
||
3763 | * Compare number1 == number2 |
||
3764 | * |
||
3765 | * @param array $left |
||
3766 | * @param array $right |
||
3767 | * |
||
3768 | * @return array |
||
3769 | */ |
||
3770 | protected function opEq($left, $right) |
||
3771 | { |
||
3772 | if (($lStr = $this->coerceString($left)) && ($rStr = $this->coerceString($right))) { |
||
3773 | $lStr[1] = ''; |
||
3774 | $rStr[1] = ''; |
||
3775 | |||
3776 | $left = $this->compileValue($lStr); |
||
3777 | $right = $this->compileValue($rStr); |
||
3778 | } |
||
3779 | |||
3780 | return $this->toBool($left === $right); |
||
3781 | } |
||
3782 | |||
3783 | /** |
||
3784 | * Compare number1 != number2 |
||
3785 | * |
||
3786 | * @param array $left |
||
3787 | * @param array $right |
||
3788 | * |
||
3789 | * @return array |
||
3790 | */ |
||
3791 | protected function opNeq($left, $right) |
||
3792 | { |
||
3793 | if (($lStr = $this->coerceString($left)) && ($rStr = $this->coerceString($right))) { |
||
3794 | $lStr[1] = ''; |
||
3795 | $rStr[1] = ''; |
||
3796 | |||
3797 | $left = $this->compileValue($lStr); |
||
3798 | $right = $this->compileValue($rStr); |
||
3799 | } |
||
3800 | |||
3801 | return $this->toBool($left !== $right); |
||
3802 | } |
||
3803 | |||
3804 | /** |
||
3805 | * Compare number1 >= number2 |
||
3806 | * |
||
3807 | * @param array $left |
||
3808 | * @param array $right |
||
3809 | * |
||
3810 | * @return array |
||
3811 | */ |
||
3812 | protected function opGteNumberNumber($left, $right) |
||
3813 | { |
||
3814 | return $this->toBool($left[1] >= $right[1]); |
||
3815 | } |
||
3816 | |||
3817 | /** |
||
3818 | * Compare number1 > number2 |
||
3819 | * |
||
3820 | * @param array $left |
||
3821 | * @param array $right |
||
3822 | * |
||
3823 | * @return array |
||
3824 | */ |
||
3825 | protected function opGtNumberNumber($left, $right) |
||
3826 | { |
||
3827 | return $this->toBool($left[1] > $right[1]); |
||
3828 | } |
||
3829 | |||
3830 | /** |
||
3831 | * Compare number1 <= number2 |
||
3832 | * |
||
3833 | * @param array $left |
||
3834 | * @param array $right |
||
3835 | * |
||
3836 | * @return array |
||
3837 | */ |
||
3838 | protected function opLteNumberNumber($left, $right) |
||
3839 | { |
||
3840 | return $this->toBool($left[1] <= $right[1]); |
||
3841 | } |
||
3842 | |||
3843 | /** |
||
3844 | * Compare number1 < number2 |
||
3845 | * |
||
3846 | * @param array $left |
||
3847 | * @param array $right |
||
3848 | * |
||
3849 | * @return array |
||
3850 | */ |
||
3851 | protected function opLtNumberNumber($left, $right) |
||
3852 | { |
||
3853 | return $this->toBool($left[1] < $right[1]); |
||
3854 | } |
||
3855 | |||
3856 | /** |
||
3857 | * Three-way comparison, aka spaceship operator |
||
3858 | * |
||
3859 | * @param array $left |
||
3860 | * @param array $right |
||
3861 | * |
||
3862 | * @return \ScssPhp\ScssPhp\Node\Number |
||
3863 | */ |
||
3864 | protected function opCmpNumberNumber($left, $right) |
||
3865 | { |
||
3866 | $n = $left[1] - $right[1]; |
||
3867 | |||
3868 | return new Node\Number($n ? $n / abs($n) : 0, ''); |
||
3869 | } |
||
3870 | |||
3871 | /** |
||
3872 | * Cast to boolean |
||
3873 | * |
||
3874 | * @api |
||
3875 | * |
||
3876 | * @param mixed $thing |
||
3877 | * |
||
3878 | * @return array |
||
3879 | */ |
||
3880 | public function toBool($thing) |
||
3881 | { |
||
3882 | return $thing ? static::$true : static::$false; |
||
3883 | } |
||
3884 | |||
3885 | /** |
||
3886 | * Compiles a primitive value into a CSS property value. |
||
3887 | * |
||
3888 | * Values in scssphp are typed by being wrapped in arrays, their format is |
||
3889 | * typically: |
||
3890 | * |
||
3891 | * array(type, contents [, additional_contents]*) |
||
3892 | * |
||
3893 | * The input is expected to be reduced. This function will not work on |
||
3894 | * things like expressions and variables. |
||
3895 | * |
||
3896 | * @api |
||
3897 | * |
||
3898 | * @param array $value |
||
3899 | * |
||
3900 | * @return string|array |
||
3901 | */ |
||
3902 | public function compileValue($value) |
||
3903 | { |
||
3904 | $value = $this->reduce($value); |
||
3905 | |||
3906 | switch ($value[0]) { |
||
3907 | case Type::T_KEYWORD: |
||
3908 | return $value[1]; |
||
3909 | |||
3910 | case Type::T_COLOR: |
||
3911 | // [1] - red component (either number for a %) |
||
3912 | // [2] - green component |
||
3913 | // [3] - blue component |
||
3914 | // [4] - optional alpha component |
||
3915 | list(, $r, $g, $b) = $value; |
||
3916 | |||
3917 | $r = $this->compileRGBAValue($r); |
||
3918 | $g = $this->compileRGBAValue($g); |
||
3919 | $b = $this->compileRGBAValue($b); |
||
3920 | |||
3921 | if (\count($value) === 5) { |
||
3922 | $alpha = $this->compileRGBAValue($value[4], true); |
||
3923 | |||
3924 | if (! is_numeric($alpha) || $alpha < 1) { |
||
3925 | $colorName = Colors::RGBaToColorName($r, $g, $b, $alpha); |
||
3926 | |||
3927 | if (! \is_null($colorName)) { |
||
3928 | return $colorName; |
||
3929 | } |
||
3930 | |||
3931 | if (is_numeric($alpha)) { |
||
3932 | $a = new Node\Number($alpha, ''); |
||
3933 | } else { |
||
3934 | $a = $alpha; |
||
3935 | } |
||
3936 | |||
3937 | return 'rgba(' . $r . ', ' . $g . ', ' . $b . ', ' . $a . ')'; |
||
3938 | } |
||
3939 | } |
||
3940 | |||
3941 | if (! is_numeric($r) || ! is_numeric($g) || ! is_numeric($b)) { |
||
3942 | return 'rgb(' . $r . ', ' . $g . ', ' . $b . ')'; |
||
3943 | } |
||
3944 | |||
3945 | $colorName = Colors::RGBaToColorName($r, $g, $b); |
||
3946 | |||
3947 | if (! \is_null($colorName)) { |
||
3948 | return $colorName; |
||
3949 | } |
||
3950 | |||
3951 | $h = sprintf('#%02x%02x%02x', $r, $g, $b); |
||
3952 | |||
3953 | // Converting hex color to short notation (e.g. #003399 to #039) |
||
3954 | if ($h[1] === $h[2] && $h[3] === $h[4] && $h[5] === $h[6]) { |
||
3955 | $h = '#' . $h[1] . $h[3] . $h[5]; |
||
3956 | } |
||
3957 | |||
3958 | return $h; |
||
3959 | |||
3960 | case Type::T_NUMBER: |
||
3961 | return $value->output($this); |
||
3962 | |||
3963 | case Type::T_STRING: |
||
3964 | $content = $this->compileStringContent($value); |
||
3965 | |||
3966 | if ($value[1]) { |
||
3967 | // force double quote as string quote for the output in certain cases |
||
3968 | if ( |
||
3969 | $value[1] === "'" && |
||
3970 | strpos($content, '"') === false && |
||
3971 | strpbrk($content, '{}') !== false |
||
3972 | ) { |
||
3973 | $value[1] = '"'; |
||
3974 | } |
||
3975 | $content = str_replace( |
||
3976 | array('\\a', "\n", "\f" , '\\' , "\r" , $value[1]), |
||
3977 | array("\r" , ' ' , '\\f', '\\\\', '\\a', '\\' . $value[1]), |
||
3978 | $content |
||
3979 | ); |
||
3980 | } |
||
3981 | |||
3982 | return $value[1] . $content . $value[1]; |
||
3983 | |||
3984 | case Type::T_FUNCTION: |
||
3985 | $args = ! empty($value[2]) ? $this->compileValue($value[2]) : ''; |
||
3986 | |||
3987 | return "$value[1]($args)"; |
||
3988 | |||
3989 | case Type::T_FUNCTION_REFERENCE: |
||
3990 | $name = ! empty($value[2]) ? $value[2] : ''; |
||
3991 | |||
3992 | return "get-function(\"$name\")"; |
||
3993 | |||
3994 | case Type::T_LIST: |
||
3995 | $value = $this->extractInterpolation($value); |
||
3996 | |||
3997 | if ($value[0] !== Type::T_LIST) { |
||
3998 | return $this->compileValue($value); |
||
3999 | } |
||
4000 | |||
4001 | list(, $delim, $items) = $value; |
||
4002 | $pre = $post = ''; |
||
4003 | |||
4004 | if (! empty($value['enclosing'])) { |
||
4005 | switch ($value['enclosing']) { |
||
4006 | case 'parent': |
||
4007 | //$pre = '('; |
||
4008 | //$post = ')'; |
||
4009 | break; |
||
4010 | case 'forced_parent': |
||
4011 | $pre = '('; |
||
4012 | $post = ')'; |
||
4013 | break; |
||
4014 | case 'bracket': |
||
4015 | case 'forced_bracket': |
||
4016 | $pre = '['; |
||
4017 | $post = ']'; |
||
4018 | break; |
||
4019 | } |
||
4020 | } |
||
4021 | |||
4022 | $prefix_value = ''; |
||
4023 | |||
4024 | if ($delim !== ' ') { |
||
4025 | $prefix_value = ' '; |
||
4026 | } |
||
4027 | |||
4028 | $filtered = []; |
||
4029 | |||
4030 | foreach ($items as $item) { |
||
4031 | if ($item[0] === Type::T_NULL) { |
||
4032 | continue; |
||
4033 | } |
||
4034 | |||
4035 | $compiled = $this->compileValue($item); |
||
4036 | |||
4037 | if ($prefix_value && \strlen($compiled)) { |
||
4038 | $compiled = $prefix_value . $compiled; |
||
4039 | } |
||
4040 | |||
4041 | $filtered[] = $compiled; |
||
4042 | } |
||
4043 | |||
4044 | return $pre . substr(implode("$delim", $filtered), \strlen($prefix_value)) . $post; |
||
4045 | |||
4046 | case Type::T_MAP: |
||
4047 | $keys = $value[1]; |
||
4048 | $values = $value[2]; |
||
4049 | $filtered = []; |
||
4050 | |||
4051 | for ($i = 0, $s = \count($keys); $i < $s; $i++) { |
||
4052 | $filtered[$this->compileValue($keys[$i])] = $this->compileValue($values[$i]); |
||
4053 | } |
||
4054 | |||
4055 | array_walk($filtered, function (&$value, $key) { |
||
4056 | $value = $key . ': ' . $value; |
||
4057 | }); |
||
4058 | |||
4059 | return '(' . implode(', ', $filtered) . ')'; |
||
4060 | |||
4061 | case Type::T_INTERPOLATED: |
||
4062 | // node created by extractInterpolation |
||
4063 | list(, $interpolate, $left, $right) = $value; |
||
4064 | list(,, $whiteLeft, $whiteRight) = $interpolate; |
||
4065 | |||
4066 | $delim = $left[1]; |
||
4067 | |||
4068 | if ($delim && $delim !== ' ' && ! $whiteLeft) { |
||
4069 | $delim .= ' '; |
||
4070 | } |
||
4071 | |||
4072 | $left = \count($left[2]) > 0 |
||
4073 | ? $this->compileValue($left) . $delim . $whiteLeft |
||
4074 | : ''; |
||
4075 | |||
4076 | $delim = $right[1]; |
||
4077 | |||
4078 | if ($delim && $delim !== ' ') { |
||
4079 | $delim .= ' '; |
||
4080 | } |
||
4081 | |||
4082 | $right = \count($right[2]) > 0 ? |
||
4083 | $whiteRight . $delim . $this->compileValue($right) : ''; |
||
4084 | |||
4085 | return $left . $this->compileValue($interpolate) . $right; |
||
4086 | |||
4087 | case Type::T_INTERPOLATE: |
||
4088 | // strip quotes if it's a string |
||
4089 | $reduced = $this->reduce($value[1]); |
||
4090 | |||
4091 | switch ($reduced[0]) { |
||
4092 | case Type::T_LIST: |
||
4093 | $reduced = $this->extractInterpolation($reduced); |
||
4094 | |||
4095 | if ($reduced[0] !== Type::T_LIST) { |
||
4096 | break; |
||
4097 | } |
||
4098 | |||
4099 | list(, $delim, $items) = $reduced; |
||
4100 | |||
4101 | if ($delim !== ' ') { |
||
4102 | $delim .= ' '; |
||
4103 | } |
||
4104 | |||
4105 | $filtered = []; |
||
4106 | |||
4107 | foreach ($items as $item) { |
||
4108 | if ($item[0] === Type::T_NULL) { |
||
4109 | continue; |
||
4110 | } |
||
4111 | |||
4112 | $temp = $this->compileValue([Type::T_KEYWORD, $item]); |
||
4113 | |||
4114 | if ($temp[0] === Type::T_STRING) { |
||
4115 | $filtered[] = $this->compileStringContent($temp); |
||
4116 | } elseif ($temp[0] === Type::T_KEYWORD) { |
||
4117 | $filtered[] = $temp[1]; |
||
4118 | } else { |
||
4119 | $filtered[] = $this->compileValue($temp); |
||
4120 | } |
||
4121 | } |
||
4122 | |||
4123 | $reduced = [Type::T_KEYWORD, implode("$delim", $filtered)]; |
||
4124 | break; |
||
4125 | |||
4126 | case Type::T_STRING: |
||
4127 | $reduced = [Type::T_KEYWORD, $this->compileStringContent($reduced)]; |
||
4128 | break; |
||
4129 | |||
4130 | case Type::T_NULL: |
||
4131 | $reduced = [Type::T_KEYWORD, '']; |
||
4132 | } |
||
4133 | |||
4134 | return $this->compileValue($reduced); |
||
4135 | |||
4136 | case Type::T_NULL: |
||
4137 | return 'null'; |
||
4138 | |||
4139 | case Type::T_COMMENT: |
||
4140 | return $this->compileCommentValue($value); |
||
4141 | |||
4142 | default: |
||
4143 | throw $this->error('unknown value type: ' . json_encode($value)); |
||
4144 | } |
||
4145 | } |
||
4146 | |||
4147 | /** |
||
4148 | * @param array $value |
||
4149 | * |
||
4150 | * @return array|string |
||
4151 | */ |
||
4152 | protected function compileDebugValue($value) |
||
4153 | { |
||
4154 | $value = $this->reduce($value, true); |
||
4155 | |||
4156 | switch ($value[0]) { |
||
4157 | case Type::T_STRING: |
||
4158 | return $this->compileStringContent($value); |
||
4159 | |||
4160 | default: |
||
4161 | return $this->compileValue($value); |
||
4162 | } |
||
4163 | } |
||
4164 | |||
4165 | /** |
||
4166 | * Flatten list |
||
4167 | * |
||
4168 | * @param array $list |
||
4169 | * |
||
4170 | * @return string |
||
4171 | */ |
||
4172 | protected function flattenList($list) |
||
4173 | { |
||
4174 | return $this->compileValue($list); |
||
4175 | } |
||
4176 | |||
4177 | /** |
||
4178 | * Compile string content |
||
4179 | * |
||
4180 | * @param array $string |
||
4181 | * |
||
4182 | * @return string |
||
4183 | */ |
||
4184 | protected function compileStringContent($string) |
||
4185 | { |
||
4186 | $parts = []; |
||
4187 | |||
4188 | foreach ($string[2] as $part) { |
||
4189 | if (\is_array($part) || $part instanceof \ArrayAccess) { |
||
4190 | $parts[] = $this->compileValue($part); |
||
4191 | } else { |
||
4192 | $parts[] = $part; |
||
4193 | } |
||
4194 | } |
||
4195 | |||
4196 | return implode($parts); |
||
4197 | } |
||
4198 | |||
4199 | /** |
||
4200 | * Extract interpolation; it doesn't need to be recursive, compileValue will handle that |
||
4201 | * |
||
4202 | * @param array $list |
||
4203 | * |
||
4204 | * @return array |
||
4205 | */ |
||
4206 | protected function extractInterpolation($list) |
||
4207 | { |
||
4208 | $items = $list[2]; |
||
4209 | |||
4210 | foreach ($items as $i => $item) { |
||
4211 | if ($item[0] === Type::T_INTERPOLATE) { |
||
4212 | $before = [Type::T_LIST, $list[1], \array_slice($items, 0, $i)]; |
||
4213 | $after = [Type::T_LIST, $list[1], \array_slice($items, $i + 1)]; |
||
4214 | |||
4215 | return [Type::T_INTERPOLATED, $item, $before, $after]; |
||
4216 | } |
||
4217 | } |
||
4218 | |||
4219 | return $list; |
||
4220 | } |
||
4221 | |||
4222 | /** |
||
4223 | * Find the final set of selectors |
||
4224 | * |
||
4225 | * @param \ScssPhp\ScssPhp\Compiler\Environment $env |
||
4226 | * @param \ScssPhp\ScssPhp\Block $selfParent |
||
4227 | * |
||
4228 | * @return array |
||
4229 | */ |
||
4230 | protected function multiplySelectors(Environment $env, $selfParent = null) |
||
4231 | { |
||
4232 | $envs = $this->compactEnv($env); |
||
4233 | $selectors = []; |
||
4234 | $parentSelectors = [[]]; |
||
4235 | |||
4236 | $selfParentSelectors = null; |
||
4237 | |||
4238 | if (! \is_null($selfParent) && $selfParent->selectors) { |
||
4239 | $selfParentSelectors = $this->evalSelectors($selfParent->selectors); |
||
4240 | } |
||
4241 | |||
4242 | while ($env = array_pop($envs)) { |
||
4243 | if (empty($env->selectors)) { |
||
4244 | continue; |
||
4245 | } |
||
4246 | |||
4247 | $selectors = $env->selectors; |
||
4248 | |||
4249 | do { |
||
4250 | $stillHasSelf = false; |
||
4251 | $prevSelectors = $selectors; |
||
4252 | $selectors = []; |
||
4253 | |||
4254 | foreach ($parentSelectors as $parent) { |
||
4255 | foreach ($prevSelectors as $selector) { |
||
4256 | if ($selfParentSelectors) { |
||
4257 | foreach ($selfParentSelectors as $selfParent) { |
||
4258 | // if no '&' in the selector, each call will give same result, only add once |
||
4259 | $s = $this->joinSelectors($parent, $selector, $stillHasSelf, $selfParent); |
||
4260 | $selectors[serialize($s)] = $s; |
||
4261 | } |
||
4262 | } else { |
||
4263 | $s = $this->joinSelectors($parent, $selector, $stillHasSelf); |
||
4264 | $selectors[serialize($s)] = $s; |
||
4265 | } |
||
4266 | } |
||
4267 | } |
||
4268 | } while ($stillHasSelf); |
||
4269 | |||
4270 | $parentSelectors = $selectors; |
||
4271 | } |
||
4272 | |||
4273 | $selectors = array_values($selectors); |
||
4274 | |||
4275 | // case we are just starting a at-root : nothing to multiply but parentSelectors |
||
4276 | if (! $selectors && $selfParentSelectors) { |
||
4277 | $selectors = $selfParentSelectors; |
||
4278 | } |
||
4279 | |||
4280 | return $selectors; |
||
4281 | } |
||
4282 | |||
4283 | /** |
||
4284 | * Join selectors; looks for & to replace, or append parent before child |
||
4285 | * |
||
4286 | * @param array $parent |
||
4287 | * @param array $child |
||
4288 | * @param boolean $stillHasSelf |
||
4289 | * @param array $selfParentSelectors |
||
4290 | |||
4291 | * @return array |
||
4292 | */ |
||
4293 | protected function joinSelectors($parent, $child, &$stillHasSelf, $selfParentSelectors = null) |
||
4294 | { |
||
4295 | $setSelf = false; |
||
4296 | $out = []; |
||
4297 | |||
4298 | foreach ($child as $part) { |
||
4299 | $newPart = []; |
||
4300 | |||
4301 | foreach ($part as $p) { |
||
4302 | // only replace & once and should be recalled to be able to make combinations |
||
4303 | if ($p === static::$selfSelector && $setSelf) { |
||
4304 | $stillHasSelf = true; |
||
4305 | } |
||
4306 | |||
4307 | if ($p === static::$selfSelector && ! $setSelf) { |
||
4308 | $setSelf = true; |
||
4309 | |||
4310 | if (\is_null($selfParentSelectors)) { |
||
4311 | $selfParentSelectors = $parent; |
||
4312 | } |
||
4313 | |||
4314 | foreach ($selfParentSelectors as $i => $parentPart) { |
||
4315 | if ($i > 0) { |
||
4316 | $out[] = $newPart; |
||
4317 | $newPart = []; |
||
4318 | } |
||
4319 | |||
4320 | foreach ($parentPart as $pp) { |
||
4321 | if (\is_array($pp)) { |
||
4322 | $flatten = []; |
||
4323 | |||
4324 | array_walk_recursive($pp, function ($a) use (&$flatten) { |
||
4325 | $flatten[] = $a; |
||
4326 | }); |
||
4327 | |||
4328 | $pp = implode($flatten); |
||
4329 | } |
||
4330 | |||
4331 | $newPart[] = $pp; |
||
4332 | } |
||
4333 | } |
||
4334 | } else { |
||
4335 | $newPart[] = $p; |
||
4336 | } |
||
4337 | } |
||
4338 | |||
4339 | $out[] = $newPart; |
||
4340 | } |
||
4341 | |||
4342 | return $setSelf ? $out : array_merge($parent, $child); |
||
4343 | } |
||
4344 | |||
4345 | /** |
||
4346 | * Multiply media |
||
4347 | * |
||
4348 | * @param \ScssPhp\ScssPhp\Compiler\Environment $env |
||
4349 | * @param array $childQueries |
||
4350 | * |
||
4351 | * @return array |
||
4352 | */ |
||
4353 | protected function multiplyMedia(Environment $env = null, $childQueries = null) |
||
4354 | { |
||
4355 | if ( |
||
4356 | ! isset($env) || |
||
4357 | ! empty($env->block->type) && $env->block->type !== Type::T_MEDIA |
||
4358 | ) { |
||
4359 | return $childQueries; |
||
4360 | } |
||
4361 | |||
4362 | // plain old block, skip |
||
4363 | if (empty($env->block->type)) { |
||
4364 | return $this->multiplyMedia($env->parent, $childQueries); |
||
4365 | } |
||
4366 | |||
4367 | $parentQueries = isset($env->block->queryList) |
||
4368 | ? $env->block->queryList |
||
4369 | : [[[Type::T_MEDIA_VALUE, $env->block->value]]]; |
||
4370 | |||
4371 | $store = [$this->env, $this->storeEnv]; |
||
4372 | |||
4373 | $this->env = $env; |
||
4374 | $this->storeEnv = null; |
||
4375 | $parentQueries = $this->evaluateMediaQuery($parentQueries); |
||
4376 | |||
4377 | list($this->env, $this->storeEnv) = $store; |
||
4378 | |||
4379 | if (\is_null($childQueries)) { |
||
4380 | $childQueries = $parentQueries; |
||
4381 | } else { |
||
4382 | $originalQueries = $childQueries; |
||
4383 | $childQueries = []; |
||
4384 | |||
4385 | foreach ($parentQueries as $parentQuery) { |
||
4386 | foreach ($originalQueries as $childQuery) { |
||
4387 | $childQueries[] = array_merge( |
||
4388 | $parentQuery, |
||
4389 | [[Type::T_MEDIA_TYPE, [Type::T_KEYWORD, 'all']]], |
||
4390 | $childQuery |
||
4391 | ); |
||
4392 | } |
||
4393 | } |
||
4394 | } |
||
4395 | |||
4396 | return $this->multiplyMedia($env->parent, $childQueries); |
||
4397 | } |
||
4398 | |||
4399 | /** |
||
4400 | * Convert env linked list to stack |
||
4401 | * |
||
4402 | * @param \ScssPhp\ScssPhp\Compiler\Environment $env |
||
4403 | * |
||
4404 | * @return array |
||
4405 | */ |
||
4406 | protected function compactEnv(Environment $env) |
||
4407 | { |
||
4408 | for ($envs = []; $env; $env = $env->parent) { |
||
4409 | $envs[] = $env; |
||
4410 | } |
||
4411 | |||
4412 | return $envs; |
||
4413 | } |
||
4414 | |||
4415 | /** |
||
4416 | * Convert env stack to singly linked list |
||
4417 | * |
||
4418 | * @param array $envs |
||
4419 | * |
||
4420 | * @return \ScssPhp\ScssPhp\Compiler\Environment |
||
4421 | */ |
||
4422 | protected function extractEnv($envs) |
||
4423 | { |
||
4424 | for ($env = null; $e = array_pop($envs);) { |
||
4425 | $e->parent = $env; |
||
4426 | $env = $e; |
||
4427 | } |
||
4428 | |||
4429 | return $env; |
||
4430 | } |
||
4431 | |||
4432 | /** |
||
4433 | * Push environment |
||
4434 | * |
||
4435 | * @param \ScssPhp\ScssPhp\Block $block |
||
4436 | * |
||
4437 | * @return \ScssPhp\ScssPhp\Compiler\Environment |
||
4438 | */ |
||
4439 | protected function pushEnv(Block $block = null) |
||
4440 | { |
||
4441 | $env = new Environment(); |
||
4442 | $env->parent = $this->env; |
||
4443 | $env->parentStore = $this->storeEnv; |
||
4444 | $env->store = []; |
||
4445 | $env->block = $block; |
||
4446 | $env->depth = isset($this->env->depth) ? $this->env->depth + 1 : 0; |
||
4447 | |||
4448 | $this->env = $env; |
||
4449 | $this->storeEnv = null; |
||
4450 | |||
4451 | return $env; |
||
4452 | } |
||
4453 | |||
4454 | /** |
||
4455 | * Pop environment |
||
4456 | */ |
||
4457 | protected function popEnv() |
||
4458 | { |
||
4459 | $this->storeEnv = $this->env->parentStore; |
||
4460 | $this->env = $this->env->parent; |
||
4461 | } |
||
4462 | |||
4463 | /** |
||
4464 | * Propagate vars from a just poped Env (used in @each and @for) |
||
4465 | * |
||
4466 | * @param array $store |
||
4467 | * @param null|array $excludedVars |
||
4468 | */ |
||
4469 | protected function backPropagateEnv($store, $excludedVars = null) |
||
4470 | { |
||
4471 | foreach ($store as $key => $value) { |
||
4472 | if (empty($excludedVars) || ! \in_array($key, $excludedVars)) { |
||
4473 | $this->set($key, $value, true); |
||
4474 | } |
||
4475 | } |
||
4476 | } |
||
4477 | |||
4478 | /** |
||
4479 | * Get store environment |
||
4480 | * |
||
4481 | * @return \ScssPhp\ScssPhp\Compiler\Environment |
||
4482 | */ |
||
4483 | protected function getStoreEnv() |
||
4484 | { |
||
4485 | return isset($this->storeEnv) ? $this->storeEnv : $this->env; |
||
4486 | } |
||
4487 | |||
4488 | /** |
||
4489 | * Set variable |
||
4490 | * |
||
4491 | * @param string $name |
||
4492 | * @param mixed $value |
||
4493 | * @param boolean $shadow |
||
4494 | * @param \ScssPhp\ScssPhp\Compiler\Environment $env |
||
4495 | * @param mixed $valueUnreduced |
||
4496 | */ |
||
4497 | protected function set($name, $value, $shadow = false, Environment $env = null, $valueUnreduced = null) |
||
4498 | { |
||
4499 | $name = $this->normalizeName($name); |
||
4500 | |||
4501 | if (! isset($env)) { |
||
4502 | $env = $this->getStoreEnv(); |
||
4503 | } |
||
4504 | |||
4505 | if ($shadow) { |
||
4506 | $this->setRaw($name, $value, $env, $valueUnreduced); |
||
4507 | } else { |
||
4508 | $this->setExisting($name, $value, $env, $valueUnreduced); |
||
4509 | } |
||
4510 | } |
||
4511 | |||
4512 | /** |
||
4513 | * Set existing variable |
||
4514 | * |
||
4515 | * @param string $name |
||
4516 | * @param mixed $value |
||
4517 | * @param \ScssPhp\ScssPhp\Compiler\Environment $env |
||
4518 | * @param mixed $valueUnreduced |
||
4519 | */ |
||
4520 | protected function setExisting($name, $value, Environment $env, $valueUnreduced = null) |
||
4521 | { |
||
4522 | $storeEnv = $env; |
||
4523 | $specialContentKey = static::$namespaces['special'] . 'content'; |
||
4524 | |||
4525 | $hasNamespace = $name[0] === '^' || $name[0] === '@' || $name[0] === '%'; |
||
4526 | |||
4527 | $maxDepth = 10000; |
||
4528 | |||
4529 | for (;;) { |
||
4530 | if ($maxDepth-- <= 0) { |
||
4531 | break; |
||
4532 | } |
||
4533 | |||
4534 | if (\array_key_exists($name, $env->store)) { |
||
4535 | break; |
||
4536 | } |
||
4537 | |||
4538 | if (! $hasNamespace && isset($env->marker)) { |
||
4539 | if (! empty($env->store[$specialContentKey])) { |
||
4540 | $env = $env->store[$specialContentKey]->scope; |
||
4541 | continue; |
||
4542 | } |
||
4543 | |||
4544 | if (! empty($env->declarationScopeParent)) { |
||
4545 | $env = $env->declarationScopeParent; |
||
4546 | continue; |
||
4547 | } else { |
||
4548 | $env = $storeEnv; |
||
4549 | break; |
||
4550 | } |
||
4551 | } |
||
4552 | |||
4553 | if (isset($env->parentStore)) { |
||
4554 | $env = $env->parentStore; |
||
4555 | } elseif (isset($env->parent)) { |
||
4556 | $env = $env->parent; |
||
4557 | } else { |
||
4558 | $env = $storeEnv; |
||
4559 | break; |
||
4560 | } |
||
4561 | } |
||
4562 | |||
4563 | $env->store[$name] = $value; |
||
4564 | |||
4565 | if ($valueUnreduced) { |
||
4566 | $env->storeUnreduced[$name] = $valueUnreduced; |
||
4567 | } |
||
4568 | } |
||
4569 | |||
4570 | /** |
||
4571 | * Set raw variable |
||
4572 | * |
||
4573 | * @param string $name |
||
4574 | * @param mixed $value |
||
4575 | * @param \ScssPhp\ScssPhp\Compiler\Environment $env |
||
4576 | * @param mixed $valueUnreduced |
||
4577 | */ |
||
4578 | protected function setRaw($name, $value, Environment $env, $valueUnreduced = null) |
||
4579 | { |
||
4580 | $env->store[$name] = $value; |
||
4581 | |||
4582 | if ($valueUnreduced) { |
||
4583 | $env->storeUnreduced[$name] = $valueUnreduced; |
||
4584 | } |
||
4585 | } |
||
4586 | |||
4587 | /** |
||
4588 | * Get variable |
||
4589 | * |
||
4590 | * @api |
||
4591 | * |
||
4592 | * @param string $name |
||
4593 | * @param boolean $shouldThrow |
||
4594 | * @param \ScssPhp\ScssPhp\Compiler\Environment $env |
||
4595 | * @param boolean $unreduced |
||
4596 | * |
||
4597 | * @return mixed|null |
||
4598 | */ |
||
4599 | public function get($name, $shouldThrow = true, Environment $env = null, $unreduced = false) |
||
4600 | { |
||
4601 | $normalizedName = $this->normalizeName($name); |
||
4602 | $specialContentKey = static::$namespaces['special'] . 'content'; |
||
4603 | |||
4604 | if (! isset($env)) { |
||
4605 | $env = $this->getStoreEnv(); |
||
4606 | } |
||
4607 | |||
4608 | $hasNamespace = $normalizedName[0] === '^' || $normalizedName[0] === '@' || $normalizedName[0] === '%'; |
||
4609 | |||
4610 | $maxDepth = 10000; |
||
4611 | |||
4612 | for (;;) { |
||
4613 | if ($maxDepth-- <= 0) { |
||
4614 | break; |
||
4615 | } |
||
4616 | |||
4617 | if (\array_key_exists($normalizedName, $env->store)) { |
||
4618 | if ($unreduced && isset($env->storeUnreduced[$normalizedName])) { |
||
4619 | return $env->storeUnreduced[$normalizedName]; |
||
4620 | } |
||
4621 | |||
4622 | return $env->store[$normalizedName]; |
||
4623 | } |
||
4624 | |||
4625 | if (! $hasNamespace && isset($env->marker)) { |
||
4626 | if (! empty($env->store[$specialContentKey])) { |
||
4627 | $env = $env->store[$specialContentKey]->scope; |
||
4628 | continue; |
||
4629 | } |
||
4630 | |||
4631 | if (! empty($env->declarationScopeParent)) { |
||
4632 | $env = $env->declarationScopeParent; |
||
4633 | } else { |
||
4634 | $env = $this->rootEnv; |
||
4635 | } |
||
4636 | continue; |
||
4637 | } |
||
4638 | |||
4639 | if (isset($env->parentStore)) { |
||
4640 | $env = $env->parentStore; |
||
4641 | } elseif (isset($env->parent)) { |
||
4642 | $env = $env->parent; |
||
4643 | } else { |
||
4644 | break; |
||
4645 | } |
||
4646 | } |
||
4647 | |||
4648 | if ($shouldThrow) { |
||
4649 | throw $this->error("Undefined variable \$$name" . ($maxDepth <= 0 ? ' (infinite recursion)' : '')); |
||
4650 | } |
||
4651 | |||
4652 | // found nothing |
||
4653 | return null; |
||
4654 | } |
||
4655 | |||
4656 | /** |
||
4657 | * Has variable? |
||
4658 | * |
||
4659 | * @param string $name |
||
4660 | * @param \ScssPhp\ScssPhp\Compiler\Environment $env |
||
4661 | * |
||
4662 | * @return boolean |
||
4663 | */ |
||
4664 | protected function has($name, Environment $env = null) |
||
4665 | { |
||
4666 | return ! \is_null($this->get($name, false, $env)); |
||
4667 | } |
||
4668 | |||
4669 | /** |
||
4670 | * Inject variables |
||
4671 | * |
||
4672 | * @param array $args |
||
4673 | */ |
||
4674 | protected function injectVariables(array $args) |
||
4675 | { |
||
4676 | if (empty($args)) { |
||
4677 | return; |
||
4678 | } |
||
4679 | |||
4680 | $parser = $this->parserFactory(__METHOD__); |
||
4681 | |||
4682 | foreach ($args as $name => $strValue) { |
||
4683 | if ($name[0] === '$') { |
||
4684 | $name = substr($name, 1); |
||
4685 | } |
||
4686 | |||
4687 | if (! $parser->parseValue($strValue, $value)) { |
||
4688 | $value = $this->coerceValue($strValue); |
||
4689 | } |
||
4690 | |||
4691 | $this->set($name, $value); |
||
4692 | } |
||
4693 | } |
||
4694 | |||
4695 | /** |
||
4696 | * Set variables |
||
4697 | * |
||
4698 | * @api |
||
4699 | * |
||
4700 | * @param array $variables |
||
4701 | */ |
||
4702 | public function setVariables(array $variables) |
||
4703 | { |
||
4704 | $this->registeredVars = array_merge($this->registeredVars, $variables); |
||
4705 | } |
||
4706 | |||
4707 | /** |
||
4708 | * Unset variable |
||
4709 | * |
||
4710 | * @api |
||
4711 | * |
||
4712 | * @param string $name |
||
4713 | */ |
||
4714 | public function unsetVariable($name) |
||
4715 | { |
||
4716 | unset($this->registeredVars[$name]); |
||
4717 | } |
||
4718 | |||
4719 | /** |
||
4720 | * Returns list of variables |
||
4721 | * |
||
4722 | * @api |
||
4723 | * |
||
4724 | * @return array |
||
4725 | */ |
||
4726 | public function getVariables() |
||
4727 | { |
||
4728 | return $this->registeredVars; |
||
4729 | } |
||
4730 | |||
4731 | /** |
||
4732 | * Adds to list of parsed files |
||
4733 | * |
||
4734 | * @api |
||
4735 | * |
||
4736 | * @param string $path |
||
4737 | */ |
||
4738 | public function addParsedFile($path) |
||
4739 | { |
||
4740 | if (isset($path) && is_file($path)) { |
||
4741 | $this->parsedFiles[realpath($path)] = filemtime($path); |
||
4742 | } |
||
4743 | } |
||
4744 | |||
4745 | /** |
||
4746 | * Returns list of parsed files |
||
4747 | * |
||
4748 | * @api |
||
4749 | * |
||
4750 | * @return array |
||
4751 | */ |
||
4752 | public function getParsedFiles() |
||
4753 | { |
||
4754 | return $this->parsedFiles; |
||
4755 | } |
||
4756 | |||
4757 | /** |
||
4758 | * Add import path |
||
4759 | * |
||
4760 | * @api |
||
4761 | * |
||
4762 | * @param string|callable $path |
||
4763 | */ |
||
4764 | public function addImportPath($path) |
||
4765 | { |
||
4766 | if (! \in_array($path, $this->importPaths)) { |
||
4767 | $this->importPaths[] = $path; |
||
4768 | } |
||
4769 | } |
||
4770 | |||
4771 | /** |
||
4772 | * Set import paths |
||
4773 | * |
||
4774 | * @api |
||
4775 | * |
||
4776 | * @param string|array $path |
||
4777 | */ |
||
4778 | public function setImportPaths($path) |
||
4779 | { |
||
4780 | $this->importPaths = (array) $path; |
||
4781 | } |
||
4782 | |||
4783 | /** |
||
4784 | * Set number precision |
||
4785 | * |
||
4786 | * @api |
||
4787 | * |
||
4788 | * @param integer $numberPrecision |
||
4789 | * |
||
4790 | * @deprecated The number precision is not configurable anymore. The default is enough for all browsers. |
||
4791 | */ |
||
4792 | public function setNumberPrecision($numberPrecision) |
||
4793 | { |
||
4794 | @trigger_error('The number precision is not configurable anymore. ' |
||
4795 | . 'The default is enough for all browsers.', E_USER_DEPRECATED); |
||
4796 | } |
||
4797 | |||
4798 | /** |
||
4799 | * Set formatter |
||
4800 | * |
||
4801 | * @api |
||
4802 | * |
||
4803 | * @param string $formatterName |
||
4804 | */ |
||
4805 | public function setFormatter($formatterName) |
||
4806 | { |
||
4807 | $this->formatter = $formatterName; |
||
4808 | } |
||
4809 | |||
4810 | /** |
||
4811 | * Set line number style |
||
4812 | * |
||
4813 | * @api |
||
4814 | * |
||
4815 | * @param string $lineNumberStyle |
||
4816 | */ |
||
4817 | public function setLineNumberStyle($lineNumberStyle) |
||
4818 | { |
||
4819 | $this->lineNumberStyle = $lineNumberStyle; |
||
4820 | } |
||
4821 | |||
4822 | /** |
||
4823 | * Enable/disable source maps |
||
4824 | * |
||
4825 | * @api |
||
4826 | * |
||
4827 | * @param integer $sourceMap |
||
4828 | */ |
||
4829 | public function setSourceMap($sourceMap) |
||
4830 | { |
||
4831 | $this->sourceMap = $sourceMap; |
||
4832 | } |
||
4833 | |||
4834 | /** |
||
4835 | * Set source map options |
||
4836 | * |
||
4837 | * @api |
||
4838 | * |
||
4839 | * @param array $sourceMapOptions |
||
4840 | */ |
||
4841 | public function setSourceMapOptions($sourceMapOptions) |
||
4842 | { |
||
4843 | $this->sourceMapOptions = $sourceMapOptions; |
||
4844 | } |
||
4845 | |||
4846 | /** |
||
4847 | * Register function |
||
4848 | * |
||
4849 | * @api |
||
4850 | * |
||
4851 | * @param string $name |
||
4852 | * @param callable $func |
||
4853 | * @param array $prototype |
||
4854 | */ |
||
4855 | public function registerFunction($name, $func, $prototype = null) |
||
4856 | { |
||
4857 | $this->userFunctions[$this->normalizeName($name)] = [$func, $prototype]; |
||
4858 | } |
||
4859 | |||
4860 | /** |
||
4861 | * Unregister function |
||
4862 | * |
||
4863 | * @api |
||
4864 | * |
||
4865 | * @param string $name |
||
4866 | */ |
||
4867 | public function unregisterFunction($name) |
||
4868 | { |
||
4869 | unset($this->userFunctions[$this->normalizeName($name)]); |
||
4870 | } |
||
4871 | |||
4872 | /** |
||
4873 | * Add feature |
||
4874 | * |
||
4875 | * @api |
||
4876 | * |
||
4877 | * @param string $name |
||
4878 | */ |
||
4879 | public function addFeature($name) |
||
4880 | { |
||
4881 | $this->registeredFeatures[$name] = true; |
||
4882 | } |
||
4883 | |||
4884 | /** |
||
4885 | * Import file |
||
4886 | * |
||
4887 | * @param string $path |
||
4888 | * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $out |
||
4889 | */ |
||
4890 | protected function importFile($path, OutputBlock $out) |
||
4891 | { |
||
4892 | $this->pushCallStack('import ' . $path); |
||
4893 | // see if tree is cached |
||
4894 | $realPath = realpath($path); |
||
4895 | |||
4896 | if (isset($this->importCache[$realPath])) { |
||
4897 | $this->handleImportLoop($realPath); |
||
4898 | |||
4899 | $tree = $this->importCache[$realPath]; |
||
4900 | } else { |
||
4901 | $code = file_get_contents($path); |
||
4902 | $parser = $this->parserFactory($path); |
||
4903 | $tree = $parser->parse($code); |
||
4904 | |||
4905 | $this->importCache[$realPath] = $tree; |
||
4906 | } |
||
4907 | |||
4908 | $pi = pathinfo($path); |
||
4909 | |||
4910 | array_unshift($this->importPaths, $pi['dirname']); |
||
4911 | $this->compileChildrenNoReturn($tree->children, $out); |
||
4912 | array_shift($this->importPaths); |
||
4913 | $this->popCallStack(); |
||
4914 | } |
||
4915 | |||
4916 | /** |
||
4917 | * Return the file path for an import url if it exists |
||
4918 | * |
||
4919 | * @api |
||
4920 | * |
||
4921 | * @param string $url |
||
4922 | * |
||
4923 | * @return string|null |
||
4924 | */ |
||
4925 | public function findImport($url) |
||
4926 | { |
||
4927 | $urls = []; |
||
4928 | |||
4929 | $hasExtension = preg_match('/[.]s?css$/', $url); |
||
4930 | |||
4931 | // for "normal" scss imports (ignore vanilla css and external requests) |
||
4932 | if (! preg_match('~\.css$|^https?://|^//~', $url)) { |
||
4933 | $isPartial = (strpos(basename($url), '_') === 0); |
||
4934 | |||
4935 | // try both normal and the _partial filename |
||
4936 | $urls = [$url . ($hasExtension ? '' : '.scss')]; |
||
4937 | |||
4938 | if (! $isPartial) { |
||
4939 | $urls[] = preg_replace('~[^/]+$~', '_\0', $url) . ($hasExtension ? '' : '.scss'); |
||
4940 | } |
||
4941 | |||
4942 | if (! $hasExtension) { |
||
4943 | $urls[] = "$url/index.scss"; |
||
4944 | $urls[] = "$url/_index.scss"; |
||
4945 | // allow to find a plain css file, *if* no scss or partial scss is found |
||
4946 | $urls[] .= $url . '.css'; |
||
4947 | } |
||
4948 | } |
||
4949 | |||
4950 | foreach ($this->importPaths as $dir) { |
||
4951 | if (\is_string($dir)) { |
||
4952 | // check urls for normal import paths |
||
4953 | foreach ($urls as $full) { |
||
4954 | $separator = ( |
||
4955 | ! empty($dir) && |
||
4956 | substr($dir, -1) !== '/' && |
||
4957 | substr($full, 0, 1) !== '/' |
||
4958 | ) ? '/' : ''; |
||
4959 | $full = $dir . $separator . $full; |
||
4960 | |||
4961 | if (is_file($file = $full)) { |
||
4962 | return $file; |
||
4963 | } |
||
4964 | } |
||
4965 | } elseif (\is_callable($dir)) { |
||
4966 | // check custom callback for import path |
||
4967 | $file = \call_user_func($dir, $url); |
||
4968 | |||
4969 | if (! \is_null($file)) { |
||
4970 | return $file; |
||
4971 | } |
||
4972 | } |
||
4973 | } |
||
4974 | |||
4975 | if ($urls) { |
||
4976 | if (! $hasExtension || preg_match('/[.]scss$/', $url)) { |
||
4977 | throw $this->error("`$url` file not found for @import"); |
||
4978 | } |
||
4979 | } |
||
4980 | |||
4981 | return null; |
||
4982 | } |
||
4983 | |||
4984 | /** |
||
4985 | * Set encoding |
||
4986 | * |
||
4987 | * @api |
||
4988 | * |
||
4989 | * @param string $encoding |
||
4990 | */ |
||
4991 | public function setEncoding($encoding) |
||
4992 | { |
||
4993 | $this->encoding = $encoding; |
||
4994 | } |
||
4995 | |||
4996 | /** |
||
4997 | * Ignore errors? |
||
4998 | * |
||
4999 | * @api |
||
5000 | * |
||
5001 | * @param boolean $ignoreErrors |
||
5002 | * |
||
5003 | * @return \ScssPhp\ScssPhp\Compiler |
||
5004 | * |
||
5005 | * @deprecated Ignoring Sass errors is not longer supported. |
||
5006 | */ |
||
5007 | public function setIgnoreErrors($ignoreErrors) |
||
5008 | { |
||
5009 | @trigger_error('Ignoring Sass errors is not longer supported.', E_USER_DEPRECATED); |
||
5010 | |||
5011 | return $this; |
||
5012 | } |
||
5013 | |||
5014 | /** |
||
5015 | * Get source position |
||
5016 | * |
||
5017 | * @api |
||
5018 | * |
||
5019 | * @return array |
||
5020 | */ |
||
5021 | public function getSourcePosition() |
||
5022 | { |
||
5023 | $sourceFile = isset($this->sourceNames[$this->sourceIndex]) ? $this->sourceNames[$this->sourceIndex] : ''; |
||
5024 | |||
5025 | return [$sourceFile, $this->sourceLine, $this->sourceColumn]; |
||
5026 | } |
||
5027 | |||
5028 | /** |
||
5029 | * Throw error (exception) |
||
5030 | * |
||
5031 | * @api |
||
5032 | * |
||
5033 | * @param string $msg Message with optional sprintf()-style vararg parameters |
||
5034 | * |
||
5035 | * @throws \ScssPhp\ScssPhp\Exception\CompilerException |
||
5036 | * |
||
5037 | * @deprecated use "error" and throw the exception in the caller instead. |
||
5038 | */ |
||
5039 | public function throwError($msg) |
||
5040 | { |
||
5041 | @trigger_error( |
||
5042 | 'The method "throwError" is deprecated. Use "error" and throw the exception in the caller instead', |
||
5043 | E_USER_DEPRECATED |
||
5044 | ); |
||
5045 | |||
5046 | throw $this->error(...func_get_args()); |
||
5047 | } |
||
5048 | |||
5049 | /** |
||
5050 | * Build an error (exception) |
||
5051 | * |
||
5052 | * @api |
||
5053 | * |
||
5054 | * @param string $msg Message with optional sprintf()-style vararg parameters |
||
5055 | * |
||
5056 | * @return CompilerException |
||
5057 | */ |
||
5058 | public function error($msg, ...$args) |
||
5059 | { |
||
5060 | if ($args) { |
||
5061 | $msg = sprintf($msg, ...$args); |
||
5062 | } |
||
5063 | |||
5064 | if (! $this->ignoreCallStackMessage) { |
||
5065 | $line = $this->sourceLine; |
||
5066 | $column = $this->sourceColumn; |
||
5067 | |||
5068 | $loc = isset($this->sourceNames[$this->sourceIndex]) |
||
5069 | ? $this->sourceNames[$this->sourceIndex] . " on line $line, at column $column" |
||
5070 | : "line: $line, column: $column"; |
||
5071 | |||
5072 | $msg = "$msg: $loc"; |
||
5073 | |||
5074 | $callStackMsg = $this->callStackMessage(); |
||
5075 | |||
5076 | if ($callStackMsg) { |
||
5077 | $msg .= "\nCall Stack:\n" . $callStackMsg; |
||
5078 | } |
||
5079 | } |
||
5080 | |||
5081 | return new CompilerException($msg); |
||
5082 | } |
||
5083 | |||
5084 | /** |
||
5085 | * @param string $functionName |
||
5086 | * @param array $ExpectedArgs |
||
5087 | * @param int $nbActual |
||
5088 | * @return CompilerException |
||
5089 | */ |
||
5090 | public function errorArgsNumber($functionName, $ExpectedArgs, $nbActual) |
||
5091 | { |
||
5092 | $nbExpected = \count($ExpectedArgs); |
||
5093 | |||
5094 | if ($nbActual > $nbExpected) { |
||
5095 | return $this->error( |
||
5096 | 'Error: Only %d arguments allowed in %s(), but %d were passed.', |
||
5097 | $nbExpected, |
||
5098 | $functionName, |
||
5099 | $nbActual |
||
5100 | ); |
||
5101 | } else { |
||
5102 | $missing = []; |
||
5103 | |||
5104 | while (count($ExpectedArgs) && count($ExpectedArgs) > $nbActual) { |
||
5105 | array_unshift($missing, array_pop($ExpectedArgs)); |
||
5106 | } |
||
5107 | |||
5108 | return $this->error( |
||
5109 | 'Error: %s() argument%s %s missing.', |
||
5110 | $functionName, |
||
5111 | count($missing) > 1 ? 's' : '', |
||
5112 | implode(', ', $missing) |
||
5113 | ); |
||
5114 | } |
||
5115 | } |
||
5116 | |||
5117 | /** |
||
5118 | * Beautify call stack for output |
||
5119 | * |
||
5120 | * @param boolean $all |
||
5121 | * @param null $limit |
||
5122 | * |
||
5123 | * @return string |
||
5124 | */ |
||
5125 | protected function callStackMessage($all = false, $limit = null) |
||
5126 | { |
||
5127 | $callStackMsg = []; |
||
5128 | $ncall = 0; |
||
5129 | |||
5130 | if ($this->callStack) { |
||
5131 | foreach (array_reverse($this->callStack) as $call) { |
||
5132 | if ($all || (isset($call['n']) && $call['n'])) { |
||
5133 | $msg = '#' . $ncall++ . ' ' . $call['n'] . ' '; |
||
5134 | $msg .= (isset($this->sourceNames[$call[Parser::SOURCE_INDEX]]) |
||
5135 | ? $this->sourceNames[$call[Parser::SOURCE_INDEX]] |
||
5136 | : '(unknown file)'); |
||
5137 | $msg .= ' on line ' . $call[Parser::SOURCE_LINE]; |
||
5138 | |||
5139 | $callStackMsg[] = $msg; |
||
5140 | |||
5141 | if (! \is_null($limit) && $ncall > $limit) { |
||
5142 | break; |
||
5143 | } |
||
5144 | } |
||
5145 | } |
||
5146 | } |
||
5147 | |||
5148 | return implode("\n", $callStackMsg); |
||
5149 | } |
||
5150 | |||
5151 | /** |
||
5152 | * Handle import loop |
||
5153 | * |
||
5154 | * @param string $name |
||
5155 | * |
||
5156 | * @throws \Exception |
||
5157 | */ |
||
5158 | protected function handleImportLoop($name) |
||
5159 | { |
||
5160 | for ($env = $this->env; $env; $env = $env->parent) { |
||
5161 | if (! $env->block) { |
||
5162 | continue; |
||
5163 | } |
||
5164 | |||
5165 | $file = $this->sourceNames[$env->block->sourceIndex]; |
||
5166 | |||
5167 | if (realpath($file) === $name) { |
||
5168 | throw $this->error('An @import loop has been found: %s imports %s', $file, basename($file)); |
||
5169 | } |
||
5170 | } |
||
5171 | } |
||
5172 | |||
5173 | /** |
||
5174 | * Call SCSS @function |
||
5175 | * |
||
5176 | * @param Object $func |
||
5177 | * @param array $argValues |
||
5178 | * |
||
5179 | * @return array $returnValue |
||
5180 | */ |
||
5181 | protected function callScssFunction($func, $argValues) |
||
5182 | { |
||
5183 | if (! $func) { |
||
5184 | return static::$defaultValue; |
||
5185 | } |
||
5186 | $name = $func->name; |
||
5187 | |||
5188 | $this->pushEnv(); |
||
5189 | |||
5190 | // set the args |
||
5191 | if (isset($func->args)) { |
||
5192 | $this->applyArguments($func->args, $argValues); |
||
5193 | } |
||
5194 | |||
5195 | // throw away lines and children |
||
5196 | $tmp = new OutputBlock(); |
||
5197 | $tmp->lines = []; |
||
5198 | $tmp->children = []; |
||
5199 | |||
5200 | $this->env->marker = 'function'; |
||
5201 | |||
5202 | if (! empty($func->parentEnv)) { |
||
5203 | $this->env->declarationScopeParent = $func->parentEnv; |
||
5204 | } else { |
||
5205 | throw $this->error("@function $name() without parentEnv"); |
||
5206 | } |
||
5207 | |||
5208 | $ret = $this->compileChildren($func->children, $tmp, $this->env->marker . ' ' . $name); |
||
5209 | |||
5210 | $this->popEnv(); |
||
5211 | |||
5212 | return ! isset($ret) ? static::$defaultValue : $ret; |
||
5213 | } |
||
5214 | |||
5215 | /** |
||
5216 | * Call built-in and registered (PHP) functions |
||
5217 | * |
||
5218 | * @param string $name |
||
5219 | * @param string|array $function |
||
5220 | * @param array $prototype |
||
5221 | * @param array $args |
||
5222 | * |
||
5223 | * @return array |
||
5224 | */ |
||
5225 | protected function callNativeFunction($name, $function, $prototype, $args) |
||
5226 | { |
||
5227 | $libName = (is_array($function) ? end($function) : null); |
||
5228 | $sorted_kwargs = $this->sortNativeFunctionArgs($libName, $prototype, $args); |
||
5229 | |||
5230 | if (\is_null($sorted_kwargs)) { |
||
5231 | return null; |
||
5232 | } |
||
5233 | @list($sorted, $kwargs) = $sorted_kwargs; |
||
5234 | |||
5235 | if ($name !== 'if' && $name !== 'call') { |
||
5236 | $inExp = true; |
||
5237 | |||
5238 | if ($name === 'join') { |
||
5239 | $inExp = false; |
||
5240 | } |
||
5241 | |||
5242 | foreach ($sorted as &$val) { |
||
5243 | $val = $this->reduce($val, $inExp); |
||
5244 | } |
||
5245 | } |
||
5246 | |||
5247 | $returnValue = \call_user_func($function, $sorted, $kwargs); |
||
5248 | |||
5249 | if (! isset($returnValue)) { |
||
5250 | return null; |
||
5251 | } |
||
5252 | |||
5253 | return $this->coerceValue($returnValue); |
||
5254 | } |
||
5255 | |||
5256 | /** |
||
5257 | * Get built-in function |
||
5258 | * |
||
5259 | * @param string $name Normalized name |
||
5260 | * |
||
5261 | * @return array |
||
5262 | */ |
||
5263 | protected function getBuiltinFunction($name) |
||
5264 | { |
||
5265 | $libName = 'lib' . preg_replace_callback( |
||
5266 | '/_(.)/', |
||
5267 | function ($m) { |
||
5268 | return ucfirst($m[1]); |
||
5269 | }, |
||
5270 | ucfirst($name) |
||
5271 | ); |
||
5272 | |||
5273 | return [$this, $libName]; |
||
5274 | } |
||
5275 | |||
5276 | /** |
||
5277 | * Sorts keyword arguments |
||
5278 | * |
||
5279 | * @param string $functionName |
||
5280 | * @param array $prototypes |
||
5281 | * @param array $args |
||
5282 | * |
||
5283 | * @return array|null |
||
5284 | */ |
||
5285 | protected function sortNativeFunctionArgs($functionName, $prototypes, $args) |
||
5286 | { |
||
5287 | static $parser = null; |
||
5288 | |||
5289 | if (! isset($prototypes)) { |
||
5290 | $keyArgs = []; |
||
5291 | $posArgs = []; |
||
5292 | |||
5293 | if (\is_array($args) && \count($args) && \end($args) === static::$null) { |
||
5294 | array_pop($args); |
||
5295 | } |
||
5296 | |||
5297 | // separate positional and keyword arguments |
||
5298 | foreach ($args as $arg) { |
||
5299 | list($key, $value) = $arg; |
||
5300 | |||
5301 | if (empty($key) or empty($key[1])) { |
||
5302 | $posArgs[] = empty($arg[2]) ? $value : $arg; |
||
5303 | } else { |
||
5304 | $keyArgs[$key[1]] = $value; |
||
5305 | } |
||
5306 | } |
||
5307 | |||
5308 | return [$posArgs, $keyArgs]; |
||
5309 | } |
||
5310 | |||
5311 | // specific cases ? |
||
5312 | if (\in_array($functionName, ['libRgb', 'libRgba', 'libHsl', 'libHsla'])) { |
||
5313 | // notation 100 127 255 / 0 is in fact a simple list of 4 values |
||
5314 | foreach ($args as $k => $arg) { |
||
5315 | if ($arg[1][0] === Type::T_LIST && \count($arg[1][2]) === 3) { |
||
5316 | $last = end($arg[1][2]); |
||
5317 | |||
5318 | if ($last[0] === Type::T_EXPRESSION && $last[1] === '/') { |
||
5319 | array_pop($arg[1][2]); |
||
5320 | $arg[1][2][] = $last[2]; |
||
5321 | $arg[1][2][] = $last[3]; |
||
5322 | $args[$k] = $arg; |
||
5323 | } |
||
5324 | } |
||
5325 | } |
||
5326 | } |
||
5327 | |||
5328 | $finalArgs = []; |
||
5329 | |||
5330 | if (! \is_array(reset($prototypes))) { |
||
5331 | $prototypes = [$prototypes]; |
||
5332 | } |
||
5333 | |||
5334 | $keyArgs = []; |
||
5335 | |||
5336 | // trying each prototypes |
||
5337 | $prototypeHasMatch = false; |
||
5338 | $exceptionMessage = ''; |
||
5339 | |||
5340 | foreach ($prototypes as $prototype) { |
||
5341 | $argDef = []; |
||
5342 | |||
5343 | foreach ($prototype as $i => $p) { |
||
5344 | $default = null; |
||
5345 | $p = explode(':', $p, 2); |
||
5346 | $name = array_shift($p); |
||
5347 | |||
5348 | if (\count($p)) { |
||
5349 | $p = trim(reset($p)); |
||
5350 | |||
5351 | if ($p === 'null') { |
||
5352 | // differentiate this null from the static::$null |
||
5353 | $default = [Type::T_KEYWORD, 'null']; |
||
5354 | } else { |
||
5355 | if (\is_null($parser)) { |
||
5356 | $parser = $this->parserFactory(__METHOD__); |
||
5357 | } |
||
5358 | |||
5359 | $parser->parseValue($p, $default); |
||
5360 | } |
||
5361 | } |
||
5362 | |||
5363 | $isVariable = false; |
||
5364 | |||
5365 | if (substr($name, -3) === '...') { |
||
5366 | $isVariable = true; |
||
5367 | $name = substr($name, 0, -3); |
||
5368 | } |
||
5369 | |||
5370 | $argDef[] = [$name, $default, $isVariable]; |
||
5371 | } |
||
5372 | |||
5373 | $ignoreCallStackMessage = $this->ignoreCallStackMessage; |
||
5374 | $this->ignoreCallStackMessage = true; |
||
5375 | |||
5376 | try { |
||
5377 | if (\count($args) > \count($argDef)) { |
||
5378 | $lastDef = end($argDef); |
||
5379 | |||
5380 | // check that last arg is not a ... |
||
5381 | if (empty($lastDef[2])) { |
||
5382 | throw $this->errorArgsNumber($functionName, $argDef, \count($args)); |
||
5383 | } |
||
5384 | } |
||
5385 | $vars = $this->applyArguments($argDef, $args, false, false); |
||
5386 | |||
5387 | // ensure all args are populated |
||
5388 | foreach ($prototype as $i => $p) { |
||
5389 | $name = explode(':', $p)[0]; |
||
5390 | |||
5391 | if (! isset($finalArgs[$i])) { |
||
5392 | $finalArgs[$i] = null; |
||
5393 | } |
||
5394 | } |
||
5395 | |||
5396 | // apply positional args |
||
5397 | foreach (array_values($vars) as $i => $val) { |
||
5398 | $finalArgs[$i] = $val; |
||
5399 | } |
||
5400 | |||
5401 | $keyArgs = array_merge($keyArgs, $vars); |
||
5402 | $prototypeHasMatch = true; |
||
5403 | |||
5404 | // overwrite positional args with keyword args |
||
5405 | foreach ($prototype as $i => $p) { |
||
5406 | $name = explode(':', $p)[0]; |
||
5407 | |||
5408 | if (isset($keyArgs[$name])) { |
||
5409 | $finalArgs[$i] = $keyArgs[$name]; |
||
5410 | } |
||
5411 | |||
5412 | // special null value as default: translate to real null here |
||
5413 | if ($finalArgs[$i] === [Type::T_KEYWORD, 'null']) { |
||
5414 | $finalArgs[$i] = null; |
||
5415 | } |
||
5416 | } |
||
5417 | // should we break if this prototype seems fulfilled? |
||
5418 | } catch (CompilerException $e) { |
||
5419 | $exceptionMessage = $e->getMessage(); |
||
5420 | } |
||
5421 | $this->ignoreCallStackMessage = $ignoreCallStackMessage; |
||
5422 | } |
||
5423 | |||
5424 | if ($exceptionMessage && ! $prototypeHasMatch) { |
||
5425 | if (\in_array($functionName, ['libRgb', 'libRgba', 'libHsl', 'libHsla'])) { |
||
5426 | // if var() or calc() is used as an argument, return as a css function |
||
5427 | foreach ($args as $arg) { |
||
5428 | if ($arg[1][0] === Type::T_FUNCTION_CALL && in_array($arg[1][1], ['var'])) { |
||
5429 | return null; |
||
5430 | } |
||
5431 | } |
||
5432 | } |
||
5433 | |||
5434 | throw $this->error($exceptionMessage); |
||
5435 | } |
||
5436 | |||
5437 | return [$finalArgs, $keyArgs]; |
||
5438 | } |
||
5439 | |||
5440 | /** |
||
5441 | * Apply argument values per definition |
||
5442 | * |
||
5443 | * @param array $argDef |
||
5444 | * @param array $argValues |
||
5445 | * @param boolean $storeInEnv |
||
5446 | * @param boolean $reduce |
||
5447 | * only used if $storeInEnv = false |
||
5448 | * |
||
5449 | * @return array |
||
5450 | * |
||
5451 | * @throws \Exception |
||
5452 | */ |
||
5453 | protected function applyArguments($argDef, $argValues, $storeInEnv = true, $reduce = true) |
||
5454 | { |
||
5455 | $output = []; |
||
5456 | |||
5457 | if (\is_array($argValues) && \count($argValues) && end($argValues) === static::$null) { |
||
5458 | array_pop($argValues); |
||
5459 | } |
||
5460 | |||
5461 | if ($storeInEnv) { |
||
5462 | $storeEnv = $this->getStoreEnv(); |
||
5463 | |||
5464 | $env = new Environment(); |
||
5465 | $env->store = $storeEnv->store; |
||
5466 | } |
||
5467 | |||
5468 | $hasVariable = false; |
||
5469 | $args = []; |
||
5470 | |||
5471 | foreach ($argDef as $i => $arg) { |
||
5472 | list($name, $default, $isVariable) = $argDef[$i]; |
||
5473 | |||
5474 | $args[$name] = [$i, $name, $default, $isVariable]; |
||
5475 | $hasVariable |= $isVariable; |
||
5476 | } |
||
5477 | |||
5478 | $splatSeparator = null; |
||
5479 | $keywordArgs = []; |
||
5480 | $deferredKeywordArgs = []; |
||
5481 | $deferredNamedKeywordArgs = []; |
||
5482 | $remaining = []; |
||
5483 | $hasKeywordArgument = false; |
||
5484 | |||
5485 | // assign the keyword args |
||
5486 | foreach ((array) $argValues as $arg) { |
||
5487 | if (! empty($arg[0])) { |
||
5488 | $hasKeywordArgument = true; |
||
5489 | |||
5490 | $name = $arg[0][1]; |
||
5491 | |||
5492 | if (! isset($args[$name])) { |
||
5493 | foreach (array_keys($args) as $an) { |
||
5494 | if (str_replace('_', '-', $an) === str_replace('_', '-', $name)) { |
||
5495 | $name = $an; |
||
5496 | break; |
||
5497 | } |
||
5498 | } |
||
5499 | } |
||
5500 | |||
5501 | if (! isset($args[$name]) || $args[$name][3]) { |
||
5502 | if ($hasVariable) { |
||
5503 | $deferredNamedKeywordArgs[$name] = $arg[1]; |
||
5504 | } else { |
||
5505 | throw $this->error("Mixin or function doesn't have an argument named $%s.", $arg[0][1]); |
||
5506 | } |
||
5507 | } elseif ($args[$name][0] < \count($remaining)) { |
||
5508 | throw $this->error("The argument $%s was passed both by position and by name.", $arg[0][1]); |
||
5509 | } else { |
||
5510 | $keywordArgs[$name] = $arg[1]; |
||
5511 | } |
||
5512 | } elseif (! empty($arg[2])) { |
||
5513 | // $arg[2] means a var followed by ... in the arg ($list... ) |
||
5514 | $val = $this->reduce($arg[1], true); |
||
5515 | |||
5516 | if ($val[0] === Type::T_LIST) { |
||
5517 | foreach ($val[2] as $name => $item) { |
||
5518 | if (! is_numeric($name)) { |
||
5519 | if (! isset($args[$name])) { |
||
5520 | foreach (array_keys($args) as $an) { |
||
5521 | if (str_replace('_', '-', $an) === str_replace('_', '-', $name)) { |
||
5522 | $name = $an; |
||
5523 | break; |
||
5524 | } |
||
5525 | } |
||
5526 | } |
||
5527 | |||
5528 | if ($hasVariable) { |
||
5529 | $deferredKeywordArgs[$name] = $item; |
||
5530 | } else { |
||
5531 | $keywordArgs[$name] = $item; |
||
5532 | } |
||
5533 | } else { |
||
5534 | if (\is_null($splatSeparator)) { |
||
5535 | $splatSeparator = $val[1]; |
||
5536 | } |
||
5537 | |||
5538 | $remaining[] = $item; |
||
5539 | } |
||
5540 | } |
||
5541 | } elseif ($val[0] === Type::T_MAP) { |
||
5542 | foreach ($val[1] as $i => $name) { |
||
5543 | $name = $this->compileStringContent($this->coerceString($name)); |
||
5544 | $item = $val[2][$i]; |
||
5545 | |||
5546 | if (! is_numeric($name)) { |
||
5547 | if (! isset($args[$name])) { |
||
5548 | foreach (array_keys($args) as $an) { |
||
5549 | if (str_replace('_', '-', $an) === str_replace('_', '-', $name)) { |
||
5550 | $name = $an; |
||
5551 | break; |
||
5552 | } |
||
5553 | } |
||
5554 | } |
||
5555 | |||
5556 | if ($hasVariable) { |
||
5557 | $deferredKeywordArgs[$name] = $item; |
||
5558 | } else { |
||
5559 | $keywordArgs[$name] = $item; |
||
5560 | } |
||
5561 | } else { |
||
5562 | if (\is_null($splatSeparator)) { |
||
5563 | $splatSeparator = $val[1]; |
||
5564 | } |
||
5565 | |||
5566 | $remaining[] = $item; |
||
5567 | } |
||
5568 | } |
||
5569 | } else { |
||
5570 | $remaining[] = $val; |
||
5571 | } |
||
5572 | } elseif ($hasKeywordArgument) { |
||
5573 | throw $this->error('Positional arguments must come before keyword arguments.'); |
||
5574 | } else { |
||
5575 | $remaining[] = $arg[1]; |
||
5576 | } |
||
5577 | } |
||
5578 | |||
5579 | foreach ($args as $arg) { |
||
5580 | list($i, $name, $default, $isVariable) = $arg; |
||
5581 | |||
5582 | if ($isVariable) { |
||
5583 | // only if more than one arg : can not be passed as position and value |
||
5584 | // see https://github.com/sass/libsass/issues/2927 |
||
5585 | if (count($args) > 1) { |
||
5586 | if (isset($remaining[$i]) && isset($deferredNamedKeywordArgs[$name])) { |
||
5587 | throw $this->error("The argument $%s was passed both by position and by name.", $name); |
||
5588 | } |
||
5589 | } |
||
5590 | |||
5591 | $val = [Type::T_LIST, \is_null($splatSeparator) ? ',' : $splatSeparator , [], $isVariable]; |
||
5592 | |||
5593 | for ($count = \count($remaining); $i < $count; $i++) { |
||
5594 | $val[2][] = $remaining[$i]; |
||
5595 | } |
||
5596 | |||
5597 | foreach ($deferredKeywordArgs as $itemName => $item) { |
||
5598 | $val[2][$itemName] = $item; |
||
5599 | } |
||
5600 | |||
5601 | foreach ($deferredNamedKeywordArgs as $itemName => $item) { |
||
5602 | $val[2][$itemName] = $item; |
||
5603 | } |
||
5604 | } elseif (isset($remaining[$i])) { |
||
5605 | $val = $remaining[$i]; |
||
5606 | } elseif (isset($keywordArgs[$name])) { |
||
5607 | $val = $keywordArgs[$name]; |
||
5608 | } elseif (! empty($default)) { |
||
5609 | continue; |
||
5610 | } else { |
||
5611 | throw $this->error("Missing argument $name"); |
||
5612 | } |
||
5613 | |||
5614 | if ($storeInEnv) { |
||
5615 | $this->set($name, $this->reduce($val, true), true, $env); |
||
5616 | } else { |
||
5617 | $output[$name] = ($reduce ? $this->reduce($val, true) : $val); |
||
5618 | } |
||
5619 | } |
||
5620 | |||
5621 | if ($storeInEnv) { |
||
5622 | $storeEnv->store = $env->store; |
||
5623 | } |
||
5624 | |||
5625 | foreach ($args as $arg) { |
||
5626 | list($i, $name, $default, $isVariable) = $arg; |
||
5627 | |||
5628 | if ($isVariable || isset($remaining[$i]) || isset($keywordArgs[$name]) || empty($default)) { |
||
5629 | continue; |
||
5630 | } |
||
5631 | |||
5632 | if ($storeInEnv) { |
||
5633 | $this->set($name, $this->reduce($default, true), true); |
||
5634 | } else { |
||
5635 | $output[$name] = ($reduce ? $this->reduce($default, true) : $default); |
||
5636 | } |
||
5637 | } |
||
5638 | |||
5639 | return $output; |
||
5640 | } |
||
5641 | |||
5642 | /** |
||
5643 | * Coerce a php value into a scss one |
||
5644 | * |
||
5645 | * @param mixed $value |
||
5646 | * |
||
5647 | * @return array|\ScssPhp\ScssPhp\Node\Number |
||
5648 | */ |
||
5649 | protected function coerceValue($value) |
||
5650 | { |
||
5651 | if (\is_array($value) || $value instanceof \ArrayAccess) { |
||
5652 | return $value; |
||
5653 | } |
||
5654 | |||
5655 | if (\is_bool($value)) { |
||
5656 | return $this->toBool($value); |
||
5657 | } |
||
5658 | |||
5659 | if (\is_null($value)) { |
||
5660 | return static::$null; |
||
5661 | } |
||
5662 | |||
5663 | if (is_numeric($value)) { |
||
5664 | return new Node\Number($value, ''); |
||
5665 | } |
||
5666 | |||
5667 | if ($value === '') { |
||
5668 | return static::$emptyString; |
||
5669 | } |
||
5670 | |||
5671 | $value = [Type::T_KEYWORD, $value]; |
||
5672 | $color = $this->coerceColor($value); |
||
5673 | |||
5674 | if ($color) { |
||
5675 | return $color; |
||
5676 | } |
||
5677 | |||
5678 | return $value; |
||
5679 | } |
||
5680 | |||
5681 | /** |
||
5682 | * Coerce something to map |
||
5683 | * |
||
5684 | * @param array $item |
||
5685 | * |
||
5686 | * @return array |
||
5687 | */ |
||
5688 | protected function coerceMap($item) |
||
5689 | { |
||
5690 | if ($item[0] === Type::T_MAP) { |
||
5691 | return $item; |
||
5692 | } |
||
5693 | |||
5694 | if ( |
||
5695 | $item[0] === static::$emptyList[0] && |
||
5696 | $item[1] === static::$emptyList[1] && |
||
5697 | $item[2] === static::$emptyList[2] |
||
5698 | ) { |
||
5699 | return static::$emptyMap; |
||
5700 | } |
||
5701 | |||
5702 | return $item; |
||
5703 | } |
||
5704 | |||
5705 | /** |
||
5706 | * Coerce something to list |
||
5707 | * |
||
5708 | * @param array $item |
||
5709 | * @param string $delim |
||
5710 | * @param boolean $removeTrailingNull |
||
5711 | * |
||
5712 | * @return array |
||
5713 | */ |
||
5714 | protected function coerceList($item, $delim = ',', $removeTrailingNull = false) |
||
5715 | { |
||
5716 | if (isset($item) && $item[0] === Type::T_LIST) { |
||
5717 | // remove trailing null from the list |
||
5718 | if ($removeTrailingNull && end($item[2]) === static::$null) { |
||
5719 | array_pop($item[2]); |
||
5720 | } |
||
5721 | |||
5722 | return $item; |
||
5723 | } |
||
5724 | |||
5725 | if (isset($item) && $item[0] === Type::T_MAP) { |
||
5726 | $keys = $item[1]; |
||
5727 | $values = $item[2]; |
||
5728 | $list = []; |
||
5729 | |||
5730 | for ($i = 0, $s = \count($keys); $i < $s; $i++) { |
||
5731 | $key = $keys[$i]; |
||
5732 | $value = $values[$i]; |
||
5733 | |||
5734 | switch ($key[0]) { |
||
5735 | case Type::T_LIST: |
||
5736 | case Type::T_MAP: |
||
5737 | case Type::T_STRING: |
||
5738 | case Type::T_NULL: |
||
5739 | break; |
||
5740 | |||
5741 | default: |
||
5742 | $key = [Type::T_KEYWORD, $this->compileStringContent($this->coerceString($key))]; |
||
5743 | break; |
||
5744 | } |
||
5745 | |||
5746 | $list[] = [ |
||
5747 | Type::T_LIST, |
||
5748 | '', |
||
5749 | [$key, $value] |
||
5750 | ]; |
||
5751 | } |
||
5752 | |||
5753 | return [Type::T_LIST, ',', $list]; |
||
5754 | } |
||
5755 | |||
5756 | return [Type::T_LIST, $delim, ! isset($item) ? [] : [$item]]; |
||
5757 | } |
||
5758 | |||
5759 | /** |
||
5760 | * Coerce color for expression |
||
5761 | * |
||
5762 | * @param array $value |
||
5763 | * |
||
5764 | * @return array|null |
||
5765 | */ |
||
5766 | protected function coerceForExpression($value) |
||
5767 | { |
||
5768 | if ($color = $this->coerceColor($value)) { |
||
5769 | return $color; |
||
5770 | } |
||
5771 | |||
5772 | return $value; |
||
5773 | } |
||
5774 | |||
5775 | /** |
||
5776 | * Coerce value to color |
||
5777 | * |
||
5778 | * @param array $value |
||
5779 | * |
||
5780 | * @return array|null |
||
5781 | */ |
||
5782 | protected function coerceColor($value, $inRGBFunction = false) |
||
5783 | { |
||
5784 | switch ($value[0]) { |
||
5785 | case Type::T_COLOR: |
||
5786 | for ($i = 1; $i <= 3; $i++) { |
||
5787 | if (! is_numeric($value[$i])) { |
||
5788 | $cv = $this->compileRGBAValue($value[$i]); |
||
5789 | |||
5790 | if (! is_numeric($cv)) { |
||
5791 | return null; |
||
5792 | } |
||
5793 | |||
5794 | $value[$i] = $cv; |
||
5795 | } |
||
5796 | |||
5797 | if (isset($value[4])) { |
||
5798 | if (! is_numeric($value[4])) { |
||
5799 | $cv = $this->compileRGBAValue($value[4], true); |
||
5800 | |||
5801 | if (! is_numeric($cv)) { |
||
5802 | return null; |
||
5803 | } |
||
5804 | |||
5805 | $value[4] = $cv; |
||
5806 | } |
||
5807 | } |
||
5808 | } |
||
5809 | |||
5810 | return $value; |
||
5811 | |||
5812 | case Type::T_LIST: |
||
5813 | if ($inRGBFunction) { |
||
5814 | if (\count($value[2]) == 3 || \count($value[2]) == 4) { |
||
5815 | $color = $value[2]; |
||
5816 | array_unshift($color, Type::T_COLOR); |
||
5817 | |||
5818 | return $this->coerceColor($color); |
||
5819 | } |
||
5820 | } |
||
5821 | |||
5822 | return null; |
||
5823 | |||
5824 | case Type::T_KEYWORD: |
||
5825 | if (! \is_string($value[1])) { |
||
5826 | return null; |
||
5827 | } |
||
5828 | |||
5829 | $name = strtolower($value[1]); |
||
5830 | |||
5831 | // hexa color? |
||
5832 | if (preg_match('/^#([0-9a-f]+)$/i', $name, $m)) { |
||
5833 | $nofValues = \strlen($m[1]); |
||
5834 | |||
5835 | if (\in_array($nofValues, [3, 4, 6, 8])) { |
||
5836 | $nbChannels = 3; |
||
5837 | $color = []; |
||
5838 | $num = hexdec($m[1]); |
||
5839 | |||
5840 | switch ($nofValues) { |
||
5841 | case 4: |
||
5842 | $nbChannels = 4; |
||
5843 | // then continuing with the case 3: |
||
5844 | case 3: |
||
5845 | for ($i = 0; $i < $nbChannels; $i++) { |
||
5846 | $t = $num & 0xf; |
||
5847 | array_unshift($color, $t << 4 | $t); |
||
5848 | $num >>= 4; |
||
5849 | } |
||
5850 | |||
5851 | break; |
||
5852 | |||
5853 | case 8: |
||
5854 | $nbChannels = 4; |
||
5855 | // then continuing with the case 6: |
||
5856 | case 6: |
||
5857 | for ($i = 0; $i < $nbChannels; $i++) { |
||
5858 | array_unshift($color, $num & 0xff); |
||
5859 | $num >>= 8; |
||
5860 | } |
||
5861 | |||
5862 | break; |
||
5863 | } |
||
5864 | |||
5865 | if ($nbChannels === 4) { |
||
5866 | if ($color[3] === 255) { |
||
5867 | $color[3] = 1; // fully opaque |
||
5868 | } else { |
||
5869 | $color[3] = round($color[3] / 255, Node\Number::PRECISION); |
||
5870 | } |
||
5871 | } |
||
5872 | |||
5873 | array_unshift($color, Type::T_COLOR); |
||
5874 | |||
5875 | return $color; |
||
5876 | } |
||
5877 | } |
||
5878 | |||
5879 | if ($rgba = Colors::colorNameToRGBa($name)) { |
||
5880 | return isset($rgba[3]) |
||
5881 | ? [Type::T_COLOR, $rgba[0], $rgba[1], $rgba[2], $rgba[3]] |
||
5882 | : [Type::T_COLOR, $rgba[0], $rgba[1], $rgba[2]]; |
||
5883 | } |
||
5884 | |||
5885 | return null; |
||
5886 | } |
||
5887 | |||
5888 | return null; |
||
5889 | } |
||
5890 | |||
5891 | /** |
||
5892 | * @param integer|\ScssPhp\ScssPhp\Node\Number $value |
||
5893 | * @param boolean $isAlpha |
||
5894 | * |
||
5895 | * @return integer|mixed |
||
5896 | */ |
||
5897 | protected function compileRGBAValue($value, $isAlpha = false) |
||
5904 | } |
||
5905 | |||
5906 | /** |
||
5907 | * @param mixed $value |
||
5908 | * @param integer|float $min |
||
5909 | * @param integer|float $max |
||
5910 | * @param boolean $isInt |
||
5911 | * @param boolean $clamp |
||
5912 | * @param boolean $modulo |
||
5913 | * |
||
5914 | * @return integer|mixed |
||
5915 | */ |
||
5916 | protected function compileColorPartValue($value, $min, $max, $isInt = true, $clamp = true, $modulo = false) |
||
5917 | { |
||
5918 | if (! is_numeric($value)) { |
||
5919 | if (\is_array($value)) { |
||
5920 | $reduced = $this->reduce($value); |
||
5921 | |||
5922 | if (\is_object($reduced) && $value->type === Type::T_NUMBER) { |
||
5923 | $value = $reduced; |
||
5924 | } |
||
5925 | } |
||
5926 | |||
5927 | if (\is_object($value) && $value->type === Type::T_NUMBER) { |
||
5928 | $num = $value->dimension; |
||
5929 | |||
5930 | if (\count($value->units)) { |
||
5931 | $unit = array_keys($value->units); |
||
5932 | $unit = reset($unit); |
||
5933 | |||
5934 | switch ($unit) { |
||
5935 | case '%': |
||
5936 | $num *= $max / 100; |
||
5937 | break; |
||
5938 | default: |
||
5939 | break; |
||
5940 | } |
||
5941 | } |
||
5942 | |||
5943 | $value = $num; |
||
5944 | } elseif (\is_array($value)) { |
||
5945 | $value = $this->compileValue($value); |
||
5946 | } |
||
5947 | } |
||
5948 | |||
5949 | if (is_numeric($value)) { |
||
5950 | if ($isInt) { |
||
5951 | $value = round($value); |
||
5952 | } |
||
5953 | |||
5954 | if ($clamp) { |
||
5955 | $value = min($max, max($min, $value)); |
||
5956 | } |
||
5957 | |||
5958 | if ($modulo) { |
||
5959 | $value = $value % $max; |
||
5960 | |||
5961 | // still negative? |
||
5962 | while ($value < $min) { |
||
5963 | $value += $max; |
||
5964 | } |
||
5965 | } |
||
5966 | |||
5967 | return $value; |
||
5968 | } |
||
5969 | |||
5970 | return $value; |
||
5971 | } |
||
5972 | |||
5973 | /** |
||
5974 | * Coerce value to string |
||
5975 | * |
||
5976 | * @param array $value |
||
5977 | * |
||
5978 | * @return array|null |
||
5979 | */ |
||
5980 | protected function coerceString($value) |
||
5981 | { |
||
5982 | if ($value[0] === Type::T_STRING) { |
||
5983 | return $value; |
||
5984 | } |
||
5985 | |||
5986 | return [Type::T_STRING, '', [$this->compileValue($value)]]; |
||
5987 | } |
||
5988 | |||
5989 | /** |
||
5990 | * Coerce value to a percentage |
||
5991 | * |
||
5992 | * @param array $value |
||
5993 | * |
||
5994 | * @return integer|float |
||
5995 | */ |
||
5996 | protected function coercePercent($value) |
||
5997 | { |
||
5998 | if ($value[0] === Type::T_NUMBER) { |
||
5999 | if (! empty($value[2]['%'])) { |
||
6000 | return $value[1] / 100; |
||
6001 | } |
||
6002 | |||
6003 | return $value[1]; |
||
6004 | } |
||
6005 | |||
6006 | return 0; |
||
6007 | } |
||
6008 | |||
6009 | /** |
||
6010 | * Assert value is a map |
||
6011 | * |
||
6012 | * @api |
||
6013 | * |
||
6014 | * @param array $value |
||
6015 | * |
||
6016 | * @return array |
||
6017 | * |
||
6018 | * @throws \Exception |
||
6019 | */ |
||
6020 | public function assertMap($value) |
||
6021 | { |
||
6022 | $value = $this->coerceMap($value); |
||
6023 | |||
6024 | if ($value[0] !== Type::T_MAP) { |
||
6025 | throw $this->error('expecting map, %s received', $value[0]); |
||
6026 | } |
||
6027 | |||
6028 | return $value; |
||
6029 | } |
||
6030 | |||
6031 | /** |
||
6032 | * Assert value is a list |
||
6033 | * |
||
6034 | * @api |
||
6035 | * |
||
6036 | * @param array $value |
||
6037 | * |
||
6038 | * @return array |
||
6039 | * |
||
6040 | * @throws \Exception |
||
6041 | */ |
||
6042 | public function assertList($value) |
||
6043 | { |
||
6044 | if ($value[0] !== Type::T_LIST) { |
||
6045 | throw $this->error('expecting list, %s received', $value[0]); |
||
6046 | } |
||
6047 | |||
6048 | return $value; |
||
6049 | } |
||
6050 | |||
6051 | /** |
||
6052 | * Assert value is a color |
||
6053 | * |
||
6054 | * @api |
||
6055 | * |
||
6056 | * @param array $value |
||
6057 | * |
||
6058 | * @return array |
||
6059 | * |
||
6060 | * @throws \Exception |
||
6061 | */ |
||
6062 | public function assertColor($value) |
||
6063 | { |
||
6064 | if ($color = $this->coerceColor($value)) { |
||
6065 | return $color; |
||
6066 | } |
||
6067 | |||
6068 | throw $this->error('expecting color, %s received', $value[0]); |
||
6069 | } |
||
6070 | |||
6071 | /** |
||
6072 | * Assert value is a number |
||
6073 | * |
||
6074 | * @api |
||
6075 | * |
||
6076 | * @param array $value |
||
6077 | * |
||
6078 | * @return integer|float |
||
6079 | * |
||
6080 | * @throws \Exception |
||
6081 | */ |
||
6082 | public function assertNumber($value) |
||
6083 | { |
||
6084 | if ($value[0] !== Type::T_NUMBER) { |
||
6085 | throw $this->error('expecting number, %s received', $value[0]); |
||
6086 | } |
||
6087 | |||
6088 | return $value[1]; |
||
6089 | } |
||
6090 | |||
6091 | /** |
||
6092 | * Make sure a color's components don't go out of bounds |
||
6093 | * |
||
6094 | * @param array $c |
||
6095 | * |
||
6096 | * @return array |
||
6097 | */ |
||
6098 | protected function fixColor($c) |
||
6099 | { |
||
6100 | foreach ([1, 2, 3] as $i) { |
||
6101 | if ($c[$i] < 0) { |
||
6102 | $c[$i] = 0; |
||
6103 | } |
||
6104 | |||
6105 | if ($c[$i] > 255) { |
||
6106 | $c[$i] = 255; |
||
6107 | } |
||
6108 | } |
||
6109 | |||
6110 | return $c; |
||
6111 | } |
||
6112 | |||
6113 | /** |
||
6114 | * Convert RGB to HSL |
||
6115 | * |
||
6116 | * @api |
||
6117 | * |
||
6118 | * @param integer $red |
||
6119 | * @param integer $green |
||
6120 | * @param integer $blue |
||
6121 | * |
||
6122 | * @return array |
||
6123 | */ |
||
6124 | public function toHSL($red, $green, $blue) |
||
6125 | { |
||
6126 | $min = min($red, $green, $blue); |
||
6127 | $max = max($red, $green, $blue); |
||
6128 | |||
6129 | $l = $min + $max; |
||
6130 | $d = $max - $min; |
||
6131 | |||
6132 | if ((int) $d === 0) { |
||
6133 | $h = $s = 0; |
||
6134 | } else { |
||
6135 | if ($l < 255) { |
||
6136 | $s = $d / $l; |
||
6137 | } else { |
||
6138 | $s = $d / (510 - $l); |
||
6139 | } |
||
6140 | |||
6141 | if ($red == $max) { |
||
6142 | $h = 60 * ($green - $blue) / $d; |
||
6143 | } elseif ($green == $max) { |
||
6144 | $h = 60 * ($blue - $red) / $d + 120; |
||
6145 | } elseif ($blue == $max) { |
||
6146 | $h = 60 * ($red - $green) / $d + 240; |
||
6147 | } |
||
6148 | } |
||
6149 | |||
6150 | return [Type::T_HSL, fmod($h, 360), $s * 100, $l / 5.1]; |
||
6151 | } |
||
6152 | |||
6153 | /** |
||
6154 | * Hue to RGB helper |
||
6155 | * |
||
6156 | * @param float $m1 |
||
6157 | * @param float $m2 |
||
6158 | * @param float $h |
||
6159 | * |
||
6160 | * @return float |
||
6161 | */ |
||
6162 | protected function hueToRGB($m1, $m2, $h) |
||
6163 | { |
||
6164 | if ($h < 0) { |
||
6165 | $h += 1; |
||
6166 | } elseif ($h > 1) { |
||
6167 | $h -= 1; |
||
6168 | } |
||
6169 | |||
6170 | if ($h * 6 < 1) { |
||
6171 | return $m1 + ($m2 - $m1) * $h * 6; |
||
6172 | } |
||
6173 | |||
6174 | if ($h * 2 < 1) { |
||
6175 | return $m2; |
||
6176 | } |
||
6177 | |||
6178 | if ($h * 3 < 2) { |
||
6179 | return $m1 + ($m2 - $m1) * (2 / 3 - $h) * 6; |
||
6180 | } |
||
6181 | |||
6182 | return $m1; |
||
6183 | } |
||
6184 | |||
6185 | /** |
||
6186 | * Convert HSL to RGB |
||
6187 | * |
||
6188 | * @api |
||
6189 | * |
||
6190 | * @param integer $hue H from 0 to 360 |
||
6191 | * @param integer $saturation S from 0 to 100 |
||
6192 | * @param integer $lightness L from 0 to 100 |
||
6193 | * |
||
6194 | * @return array |
||
6195 | */ |
||
6196 | public function toRGB($hue, $saturation, $lightness) |
||
6197 | { |
||
6198 | if ($hue < 0) { |
||
6199 | $hue += 360; |
||
6200 | } |
||
6201 | |||
6202 | $h = $hue / 360; |
||
6203 | $s = min(100, max(0, $saturation)) / 100; |
||
6204 | $l = min(100, max(0, $lightness)) / 100; |
||
6205 | |||
6206 | $m2 = $l <= 0.5 ? $l * ($s + 1) : $l + $s - $l * $s; |
||
6207 | $m1 = $l * 2 - $m2; |
||
6208 | |||
6209 | $r = $this->hueToRGB($m1, $m2, $h + 1 / 3) * 255; |
||
6210 | $g = $this->hueToRGB($m1, $m2, $h) * 255; |
||
6211 | $b = $this->hueToRGB($m1, $m2, $h - 1 / 3) * 255; |
||
6212 | |||
6213 | $out = [Type::T_COLOR, $r, $g, $b]; |
||
6214 | |||
6215 | return $out; |
||
6216 | } |
||
6217 | |||
6218 | // Built in functions |
||
6219 | |||
6220 | protected static $libCall = ['name', 'args...']; |
||
6221 | protected function libCall($args, $kwargs) |
||
6222 | { |
||
6223 | $functionReference = $this->reduce(array_shift($args), true); |
||
6224 | |||
6225 | if (in_array($functionReference[0], [Type::T_STRING, Type::T_KEYWORD])) { |
||
6226 | $name = $this->compileStringContent($this->coerceString($this->reduce($functionReference, true))); |
||
6227 | $warning = "DEPRECATION WARNING: Passing a string to call() is deprecated and will be illegal\n" |
||
6228 | . "in Sass 4.0. Use call(function-reference($name)) instead."; |
||
6229 | fwrite($this->stderr, "$warning\n\n"); |
||
6230 | $functionReference = $this->libGetFunction([$functionReference]); |
||
6231 | } |
||
6232 | |||
6233 | if ($functionReference === static::$null) { |
||
6234 | return static::$null; |
||
6235 | } |
||
6236 | |||
6237 | if (! in_array($functionReference[0], [Type::T_FUNCTION_REFERENCE, Type::T_FUNCTION])) { |
||
6238 | throw $this->error('Function reference expected, got ' . $functionReference[0]); |
||
6239 | } |
||
6240 | |||
6241 | $callArgs = []; |
||
6242 | |||
6243 | // $kwargs['args'] is [Type::T_LIST, ',', [..]] |
||
6244 | foreach ($kwargs['args'][2] as $varname => $arg) { |
||
6245 | if (is_numeric($varname)) { |
||
6246 | $varname = null; |
||
6247 | } else { |
||
6248 | $varname = [ 'var', $varname]; |
||
6249 | } |
||
6250 | |||
6251 | $callArgs[] = [$varname, $arg, false]; |
||
6252 | } |
||
6253 | |||
6254 | return $this->reduce([Type::T_FUNCTION_CALL, $functionReference, $callArgs]); |
||
6255 | } |
||
6256 | |||
6257 | |||
6258 | protected static $libGetFunction = [ |
||
6259 | ['name'], |
||
6260 | ['name', 'css'] |
||
6261 | ]; |
||
6262 | protected function libGetFunction($args) |
||
6263 | { |
||
6264 | $name = $this->compileStringContent($this->coerceString($this->reduce(array_shift($args), true))); |
||
6265 | $isCss = false; |
||
6266 | |||
6267 | if (count($args)) { |
||
6268 | $isCss = $this->reduce(array_shift($args), true); |
||
6269 | $isCss = (($isCss === static::$true) ? true : false); |
||
6270 | } |
||
6271 | |||
6272 | if ($isCss) { |
||
6273 | return [Type::T_FUNCTION, $name, [Type::T_LIST, ',', []]]; |
||
6274 | } |
||
6275 | |||
6276 | return $this->getFunctionReference($name, true); |
||
6277 | } |
||
6278 | |||
6279 | protected static $libIf = ['condition', 'if-true', 'if-false:']; |
||
6280 | protected function libIf($args) |
||
6281 | { |
||
6282 | list($cond, $t, $f) = $args; |
||
6283 | |||
6284 | if (! $this->isTruthy($this->reduce($cond, true))) { |
||
6285 | return $this->reduce($f, true); |
||
6286 | } |
||
6287 | |||
6288 | return $this->reduce($t, true); |
||
6289 | } |
||
6290 | |||
6291 | protected static $libIndex = ['list', 'value']; |
||
6292 | protected function libIndex($args) |
||
6293 | { |
||
6294 | list($list, $value) = $args; |
||
6295 | |||
6296 | if ( |
||
6297 | $list[0] === Type::T_MAP || |
||
6298 | $list[0] === Type::T_STRING || |
||
6299 | $list[0] === Type::T_KEYWORD || |
||
6300 | $list[0] === Type::T_INTERPOLATE |
||
6301 | ) { |
||
6302 | $list = $this->coerceList($list, ' '); |
||
6303 | } |
||
6304 | |||
6305 | if ($list[0] !== Type::T_LIST) { |
||
6306 | return static::$null; |
||
6307 | } |
||
6308 | |||
6309 | $values = []; |
||
6310 | |||
6311 | foreach ($list[2] as $item) { |
||
6312 | $values[] = $this->normalizeValue($item); |
||
6313 | } |
||
6314 | |||
6315 | $key = array_search($this->normalizeValue($value), $values); |
||
6316 | |||
6317 | return false === $key ? static::$null : $key + 1; |
||
6318 | } |
||
6319 | |||
6320 | protected static $libRgb = [ |
||
6321 | ['color'], |
||
6322 | ['color', 'alpha'], |
||
6323 | ['channels'], |
||
6324 | ['red', 'green', 'blue'], |
||
6325 | ['red', 'green', 'blue', 'alpha'] ]; |
||
6326 | protected function libRgb($args, $kwargs, $funcName = 'rgb') |
||
6327 | { |
||
6328 | switch (\count($args)) { |
||
6329 | case 1: |
||
6330 | if (! $color = $this->coerceColor($args[0], true)) { |
||
6331 | $color = [Type::T_STRING, '', [$funcName . '(', $args[0], ')']]; |
||
6332 | } |
||
6333 | break; |
||
6334 | |||
6335 | case 3: |
||
6336 | $color = [Type::T_COLOR, $args[0], $args[1], $args[2]]; |
||
6337 | |||
6338 | if (! $color = $this->coerceColor($color)) { |
||
6339 | $color = [Type::T_STRING, '', [$funcName . '(', $args[0], ', ', $args[1], ', ', $args[2], ')']]; |
||
6340 | } |
||
6341 | |||
6342 | return $color; |
||
6343 | |||
6344 | case 2: |
||
6345 | if ($color = $this->coerceColor($args[0], true)) { |
||
6346 | $alpha = $this->compileRGBAValue($args[1], true); |
||
6347 | |||
6348 | if (is_numeric($alpha)) { |
||
6349 | $color[4] = $alpha; |
||
6350 | } else { |
||
6351 | $color = [Type::T_STRING, '', |
||
6352 | [$funcName . '(', $color[1], ', ', $color[2], ', ', $color[3], ', ', $alpha, ')']]; |
||
6353 | } |
||
6354 | } else { |
||
6355 | $color = [Type::T_STRING, '', [$funcName . '(', $args[0], ')']]; |
||
6356 | } |
||
6357 | break; |
||
6358 | |||
6359 | case 4: |
||
6360 | default: |
||
6361 | $color = [Type::T_COLOR, $args[0], $args[1], $args[2], $args[3]]; |
||
6362 | |||
6363 | if (! $color = $this->coerceColor($color)) { |
||
6364 | $color = [Type::T_STRING, '', |
||
6365 | [$funcName . '(', $args[0], ', ', $args[1], ', ', $args[2], ', ', $args[3], ')']]; |
||
6366 | } |
||
6367 | break; |
||
6368 | } |
||
6369 | |||
6370 | return $color; |
||
6371 | } |
||
6372 | |||
6373 | protected static $libRgba = [ |
||
6374 | ['color'], |
||
6375 | ['color', 'alpha'], |
||
6376 | ['channels'], |
||
6377 | ['red', 'green', 'blue'], |
||
6378 | ['red', 'green', 'blue', 'alpha'] ]; |
||
6379 | protected function libRgba($args, $kwargs) |
||
6380 | { |
||
6381 | return $this->libRgb($args, $kwargs, 'rgba'); |
||
6382 | } |
||
6383 | |||
6384 | // helper function for adjust_color, change_color, and scale_color |
||
6385 | protected function alterColor($args, $fn) |
||
6386 | { |
||
6387 | $color = $this->assertColor($args[0]); |
||
6388 | |||
6389 | foreach ([1 => 1, 2 => 2, 3 => 3, 7 => 4] as $iarg => $irgba) { |
||
6390 | if (isset($args[$iarg])) { |
||
6391 | $val = $this->assertNumber($args[$iarg]); |
||
6392 | |||
6393 | if (! isset($color[$irgba])) { |
||
6394 | $color[$irgba] = (($irgba < 4) ? 0 : 1); |
||
6395 | } |
||
6396 | |||
6397 | $color[$irgba] = \call_user_func($fn, $color[$irgba], $val, $iarg); |
||
6398 | } |
||
6399 | } |
||
6400 | |||
6401 | if (! empty($args[4]) || ! empty($args[5]) || ! empty($args[6])) { |
||
6402 | $hsl = $this->toHSL($color[1], $color[2], $color[3]); |
||
6403 | |||
6404 | foreach ([4 => 1, 5 => 2, 6 => 3] as $iarg => $ihsl) { |
||
6405 | if (! empty($args[$iarg])) { |
||
6406 | $val = $this->assertNumber($args[$iarg]); |
||
6407 | $hsl[$ihsl] = \call_user_func($fn, $hsl[$ihsl], $val, $iarg); |
||
6408 | } |
||
6409 | } |
||
6410 | |||
6411 | $rgb = $this->toRGB($hsl[1], $hsl[2], $hsl[3]); |
||
6412 | |||
6413 | if (isset($color[4])) { |
||
6414 | $rgb[4] = $color[4]; |
||
6415 | } |
||
6416 | |||
6417 | $color = $rgb; |
||
6418 | } |
||
6419 | |||
6420 | return $color; |
||
6421 | } |
||
6422 | |||
6423 | protected static $libAdjustColor = [ |
||
6424 | 'color', 'red:null', 'green:null', 'blue:null', |
||
6425 | 'hue:null', 'saturation:null', 'lightness:null', 'alpha:null' |
||
6426 | ]; |
||
6427 | protected function libAdjustColor($args) |
||
6428 | { |
||
6429 | return $this->alterColor($args, function ($base, $alter, $i) { |
||
6430 | return $base + $alter; |
||
6431 | }); |
||
6432 | } |
||
6433 | |||
6434 | protected static $libChangeColor = [ |
||
6435 | 'color', 'red:null', 'green:null', 'blue:null', |
||
6436 | 'hue:null', 'saturation:null', 'lightness:null', 'alpha:null' |
||
6437 | ]; |
||
6438 | protected function libChangeColor($args) |
||
6439 | { |
||
6440 | return $this->alterColor($args, function ($base, $alter, $i) { |
||
6441 | return $alter; |
||
6442 | }); |
||
6443 | } |
||
6444 | |||
6445 | protected static $libScaleColor = [ |
||
6446 | 'color', 'red:null', 'green:null', 'blue:null', |
||
6447 | 'hue:null', 'saturation:null', 'lightness:null', 'alpha:null' |
||
6448 | ]; |
||
6449 | protected function libScaleColor($args) |
||
6450 | { |
||
6451 | return $this->alterColor($args, function ($base, $scale, $i) { |
||
6452 | // 1, 2, 3 - rgb |
||
6453 | // 4, 5, 6 - hsl |
||
6454 | // 7 - a |
||
6455 | switch ($i) { |
||
6456 | case 1: |
||
6457 | case 2: |
||
6458 | case 3: |
||
6459 | $max = 255; |
||
6460 | break; |
||
6461 | |||
6462 | case 4: |
||
6463 | $max = 360; |
||
6464 | break; |
||
6465 | |||
6466 | case 7: |
||
6467 | $max = 1; |
||
6468 | break; |
||
6469 | |||
6470 | default: |
||
6471 | $max = 100; |
||
6472 | } |
||
6473 | |||
6474 | $scale = $scale / 100; |
||
6475 | |||
6476 | if ($scale < 0) { |
||
6477 | return $base * $scale + $base; |
||
6478 | } |
||
6479 | |||
6480 | return ($max - $base) * $scale + $base; |
||
6481 | }); |
||
6482 | } |
||
6483 | |||
6484 | protected static $libIeHexStr = ['color']; |
||
6485 | protected function libIeHexStr($args) |
||
6486 | { |
||
6487 | $color = $this->coerceColor($args[0]); |
||
6488 | |||
6489 | if (\is_null($color)) { |
||
6490 | $this->throwError('Error: argument `$color` of `ie-hex-str($color)` must be a color'); |
||
6491 | } |
||
6492 | |||
6493 | $color[4] = isset($color[4]) ? round(255 * $color[4]) : 255; |
||
6494 | |||
6495 | return [Type::T_STRING, '', [sprintf('#%02X%02X%02X%02X', $color[4], $color[1], $color[2], $color[3])]]; |
||
6496 | } |
||
6497 | |||
6498 | protected static $libRed = ['color']; |
||
6499 | protected function libRed($args) |
||
6500 | { |
||
6501 | $color = $this->coerceColor($args[0]); |
||
6502 | |||
6503 | if (\is_null($color)) { |
||
6504 | $this->throwError('Error: argument `$color` of `red($color)` must be a color'); |
||
6505 | } |
||
6506 | |||
6507 | return $color[1]; |
||
6508 | } |
||
6509 | |||
6510 | protected static $libGreen = ['color']; |
||
6511 | protected function libGreen($args) |
||
6512 | { |
||
6513 | $color = $this->coerceColor($args[0]); |
||
6514 | |||
6515 | if (\is_null($color)) { |
||
6516 | $this->throwError('Error: argument `$color` of `green($color)` must be a color'); |
||
6517 | } |
||
6518 | |||
6519 | return $color[2]; |
||
6520 | } |
||
6521 | |||
6522 | protected static $libBlue = ['color']; |
||
6523 | protected function libBlue($args) |
||
6524 | { |
||
6525 | $color = $this->coerceColor($args[0]); |
||
6526 | |||
6527 | if (\is_null($color)) { |
||
6528 | $this->throwError('Error: argument `$color` of `blue($color)` must be a color'); |
||
6529 | } |
||
6530 | |||
6531 | return $color[3]; |
||
6532 | } |
||
6533 | |||
6534 | protected static $libAlpha = ['color']; |
||
6535 | protected function libAlpha($args) |
||
6536 | { |
||
6537 | if ($color = $this->coerceColor($args[0])) { |
||
6538 | return isset($color[4]) ? $color[4] : 1; |
||
6539 | } |
||
6540 | |||
6541 | // this might be the IE function, so return value unchanged |
||
6542 | return null; |
||
6543 | } |
||
6544 | |||
6545 | protected static $libOpacity = ['color']; |
||
6546 | protected function libOpacity($args) |
||
6547 | { |
||
6548 | $value = $args[0]; |
||
6549 | |||
6550 | if ($value[0] === Type::T_NUMBER) { |
||
6551 | return null; |
||
6552 | } |
||
6553 | |||
6554 | return $this->libAlpha($args); |
||
6555 | } |
||
6556 | |||
6557 | // mix two colors |
||
6558 | protected static $libMix = [ |
||
6559 | ['color1', 'color2', 'weight:0.5'], |
||
6560 | ['color-1', 'color-2', 'weight:0.5'] |
||
6561 | ]; |
||
6562 | protected function libMix($args) |
||
6563 | { |
||
6564 | list($first, $second, $weight) = $args; |
||
6565 | |||
6566 | $first = $this->assertColor($first); |
||
6567 | $second = $this->assertColor($second); |
||
6568 | |||
6569 | if (! isset($weight)) { |
||
6570 | $weight = 0.5; |
||
6571 | } else { |
||
6572 | $weight = $this->coercePercent($weight); |
||
6573 | } |
||
6574 | |||
6575 | $firstAlpha = isset($first[4]) ? $first[4] : 1; |
||
6576 | $secondAlpha = isset($second[4]) ? $second[4] : 1; |
||
6577 | |||
6578 | $w = $weight * 2 - 1; |
||
6579 | $a = $firstAlpha - $secondAlpha; |
||
6580 | |||
6581 | $w1 = (($w * $a === -1 ? $w : ($w + $a) / (1 + $w * $a)) + 1) / 2.0; |
||
6582 | $w2 = 1.0 - $w1; |
||
6583 | |||
6584 | $new = [Type::T_COLOR, |
||
6585 | $w1 * $first[1] + $w2 * $second[1], |
||
6586 | $w1 * $first[2] + $w2 * $second[2], |
||
6587 | $w1 * $first[3] + $w2 * $second[3], |
||
6588 | ]; |
||
6589 | |||
6590 | if ($firstAlpha != 1.0 || $secondAlpha != 1.0) { |
||
6591 | $new[] = $firstAlpha * $weight + $secondAlpha * (1 - $weight); |
||
6592 | } |
||
6593 | |||
6594 | return $this->fixColor($new); |
||
6595 | } |
||
6596 | |||
6597 | protected static $libHsl = [ |
||
6598 | ['channels'], |
||
6599 | ['hue', 'saturation', 'lightness'], |
||
6600 | ['hue', 'saturation', 'lightness', 'alpha'] ]; |
||
6601 | protected function libHsl($args, $kwargs, $funcName = 'hsl') |
||
6602 | { |
||
6603 | $args_to_check = $args; |
||
6604 | |||
6605 | if (\count($args) == 1) { |
||
6606 | if ($args[0][0] !== Type::T_LIST || \count($args[0][2]) < 3 || \count($args[0][2]) > 4) { |
||
6607 | return [Type::T_STRING, '', [$funcName . '(', $args[0], ')']]; |
||
6608 | } |
||
6609 | |||
6610 | $args = $args[0][2]; |
||
6611 | $args_to_check = $kwargs['channels'][2]; |
||
6612 | } |
||
6613 | |||
6614 | $hue = $this->compileColorPartValue($args[0], 0, 360, false, false, true); |
||
6615 | $saturation = $this->compileColorPartValue($args[1], 0, 100, false); |
||
6616 | $lightness = $this->compileColorPartValue($args[2], 0, 100, false); |
||
6617 | |||
6618 | foreach ($kwargs as $k => $arg) { |
||
6619 | if (in_array($arg[0], [Type::T_FUNCTION_CALL]) && in_array($arg[1], ['min', 'max'])) { |
||
6620 | return null; |
||
6621 | } |
||
6622 | } |
||
6623 | |||
6624 | foreach ($args_to_check as $k => $arg) { |
||
6625 | if (in_array($arg[0], [Type::T_FUNCTION_CALL]) && in_array($arg[1], ['min', 'max'])) { |
||
6626 | if (count($kwargs) > 1 || ($k >= 2 && count($args) === 4)) { |
||
6627 | return null; |
||
6628 | } |
||
6629 | |||
6630 | $args[$k] = $this->stringifyFncallArgs($arg); |
||
6631 | $hue = ''; |
||
6632 | } |
||
6633 | |||
6634 | if ( |
||
6635 | $k >= 2 && count($args) === 4 && |
||
6636 | in_array($arg[0], [Type::T_FUNCTION_CALL, Type::T_FUNCTION]) && |
||
6637 | in_array($arg[1], ['calc','env']) |
||
6638 | ) { |
||
6639 | return null; |
||
6640 | } |
||
6641 | } |
||
6642 | |||
6643 | $alpha = null; |
||
6644 | |||
6645 | if (\count($args) === 4) { |
||
6646 | $alpha = $this->compileColorPartValue($args[3], 0, 100, false); |
||
6647 | |||
6648 | if (! is_numeric($hue) || ! is_numeric($saturation) || ! is_numeric($lightness) || ! is_numeric($alpha)) { |
||
6649 | return [Type::T_STRING, '', |
||
6650 | [$funcName . '(', $args[0], ', ', $args[1], ', ', $args[2], ', ', $args[3], ')']]; |
||
6651 | } |
||
6652 | } else { |
||
6653 | if (! is_numeric($hue) || ! is_numeric($saturation) || ! is_numeric($lightness)) { |
||
6654 | return [Type::T_STRING, '', [$funcName . '(', $args[0], ', ', $args[1], ', ', $args[2], ')']]; |
||
6655 | } |
||
6656 | } |
||
6657 | |||
6658 | $color = $this->toRGB($hue, $saturation, $lightness); |
||
6659 | |||
6660 | if (! \is_null($alpha)) { |
||
6661 | $color[4] = $alpha; |
||
6662 | } |
||
6663 | |||
6664 | return $color; |
||
6665 | } |
||
6666 | |||
6667 | protected static $libHsla = [ |
||
6668 | ['channels'], |
||
6669 | ['hue', 'saturation', 'lightness'], |
||
6670 | ['hue', 'saturation', 'lightness', 'alpha']]; |
||
6671 | protected function libHsla($args, $kwargs) |
||
6672 | { |
||
6673 | return $this->libHsl($args, $kwargs, 'hsla'); |
||
6674 | } |
||
6675 | |||
6676 | protected static $libHue = ['color']; |
||
6677 | protected function libHue($args) |
||
6678 | { |
||
6679 | $color = $this->assertColor($args[0]); |
||
6680 | $hsl = $this->toHSL($color[1], $color[2], $color[3]); |
||
6681 | |||
6682 | return new Node\Number($hsl[1], 'deg'); |
||
6683 | } |
||
6684 | |||
6685 | protected static $libSaturation = ['color']; |
||
6686 | protected function libSaturation($args) |
||
6687 | { |
||
6688 | $color = $this->assertColor($args[0]); |
||
6689 | $hsl = $this->toHSL($color[1], $color[2], $color[3]); |
||
6690 | |||
6691 | return new Node\Number($hsl[2], '%'); |
||
6692 | } |
||
6693 | |||
6694 | protected static $libLightness = ['color']; |
||
6695 | protected function libLightness($args) |
||
6696 | { |
||
6697 | $color = $this->assertColor($args[0]); |
||
6698 | $hsl = $this->toHSL($color[1], $color[2], $color[3]); |
||
6699 | |||
6700 | return new Node\Number($hsl[3], '%'); |
||
6701 | } |
||
6702 | |||
6703 | protected function adjustHsl($color, $idx, $amount) |
||
6704 | { |
||
6705 | $hsl = $this->toHSL($color[1], $color[2], $color[3]); |
||
6706 | $hsl[$idx] += $amount; |
||
6707 | $out = $this->toRGB($hsl[1], $hsl[2], $hsl[3]); |
||
6708 | |||
6709 | if (isset($color[4])) { |
||
6710 | $out[4] = $color[4]; |
||
6711 | } |
||
6712 | |||
6713 | return $out; |
||
6714 | } |
||
6715 | |||
6716 | protected static $libAdjustHue = ['color', 'degrees']; |
||
6717 | protected function libAdjustHue($args) |
||
6718 | { |
||
6719 | $color = $this->assertColor($args[0]); |
||
6720 | $degrees = $this->assertNumber($args[1]); |
||
6721 | |||
6722 | return $this->adjustHsl($color, 1, $degrees); |
||
6723 | } |
||
6724 | |||
6725 | protected static $libLighten = ['color', 'amount']; |
||
6726 | protected function libLighten($args) |
||
6727 | { |
||
6728 | $color = $this->assertColor($args[0]); |
||
6729 | $amount = Util::checkRange('amount', new Range(0, 100), $args[1], '%'); |
||
6730 | |||
6731 | return $this->adjustHsl($color, 3, $amount); |
||
6732 | } |
||
6733 | |||
6734 | protected static $libDarken = ['color', 'amount']; |
||
6735 | protected function libDarken($args) |
||
6736 | { |
||
6737 | $color = $this->assertColor($args[0]); |
||
6738 | $amount = Util::checkRange('amount', new Range(0, 100), $args[1], '%'); |
||
6739 | |||
6740 | return $this->adjustHsl($color, 3, -$amount); |
||
6741 | } |
||
6742 | |||
6743 | protected static $libSaturate = [['color', 'amount'], ['amount']]; |
||
6744 | protected function libSaturate($args) |
||
6745 | { |
||
6746 | $value = $args[0]; |
||
6747 | |||
6748 | if ($value[0] === Type::T_NUMBER) { |
||
6749 | return null; |
||
6750 | } |
||
6751 | |||
6752 | if (count($args) === 1) { |
||
6753 | $val = $this->compileValue($value); |
||
6754 | throw $this->error("\$amount: $val is not a number"); |
||
6755 | } |
||
6756 | |||
6757 | $color = $this->assertColor($value); |
||
6758 | $amount = 100 * $this->coercePercent($args[1]); |
||
6759 | |||
6760 | return $this->adjustHsl($color, 2, $amount); |
||
6761 | } |
||
6762 | |||
6763 | protected static $libDesaturate = ['color', 'amount']; |
||
6764 | protected function libDesaturate($args) |
||
6765 | { |
||
6766 | $color = $this->assertColor($args[0]); |
||
6767 | $amount = 100 * $this->coercePercent($args[1]); |
||
6768 | |||
6769 | return $this->adjustHsl($color, 2, -$amount); |
||
6770 | } |
||
6771 | |||
6772 | protected static $libGrayscale = ['color']; |
||
6773 | protected function libGrayscale($args) |
||
6774 | { |
||
6775 | $value = $args[0]; |
||
6776 | |||
6777 | if ($value[0] === Type::T_NUMBER) { |
||
6778 | return null; |
||
6779 | } |
||
6780 | |||
6781 | return $this->adjustHsl($this->assertColor($value), 2, -100); |
||
6782 | } |
||
6783 | |||
6784 | protected static $libComplement = ['color']; |
||
6785 | protected function libComplement($args) |
||
6786 | { |
||
6787 | return $this->adjustHsl($this->assertColor($args[0]), 1, 180); |
||
6788 | } |
||
6789 | |||
6790 | protected static $libInvert = ['color', 'weight:1']; |
||
6791 | protected function libInvert($args) |
||
6792 | { |
||
6793 | list($value, $weight) = $args; |
||
6794 | |||
6795 | if (! isset($weight)) { |
||
6796 | $weight = 1; |
||
6797 | } else { |
||
6798 | $weight = $this->coercePercent($weight); |
||
6799 | } |
||
6800 | |||
6801 | if ($value[0] === Type::T_NUMBER) { |
||
6802 | return null; |
||
6803 | } |
||
6804 | |||
6805 | $color = $this->assertColor($value); |
||
6806 | $inverted = $color; |
||
6807 | $inverted[1] = 255 - $inverted[1]; |
||
6808 | $inverted[2] = 255 - $inverted[2]; |
||
6809 | $inverted[3] = 255 - $inverted[3]; |
||
6810 | |||
6811 | if ($weight < 1) { |
||
6812 | return $this->libMix([$inverted, $color, [Type::T_NUMBER, $weight]]); |
||
6813 | } |
||
6814 | |||
6815 | return $inverted; |
||
6816 | } |
||
6817 | |||
6818 | // increases opacity by amount |
||
6819 | protected static $libOpacify = ['color', 'amount']; |
||
6820 | protected function libOpacify($args) |
||
6821 | { |
||
6822 | $color = $this->assertColor($args[0]); |
||
6823 | $amount = $this->coercePercent($args[1]); |
||
6824 | |||
6825 | $color[4] = (isset($color[4]) ? $color[4] : 1) + $amount; |
||
6826 | $color[4] = min(1, max(0, $color[4])); |
||
6827 | |||
6828 | return $color; |
||
6829 | } |
||
6830 | |||
6831 | protected static $libFadeIn = ['color', 'amount']; |
||
6832 | protected function libFadeIn($args) |
||
6833 | { |
||
6834 | return $this->libOpacify($args); |
||
6835 | } |
||
6836 | |||
6837 | // decreases opacity by amount |
||
6838 | protected static $libTransparentize = ['color', 'amount']; |
||
6839 | protected function libTransparentize($args) |
||
6840 | { |
||
6841 | $color = $this->assertColor($args[0]); |
||
6842 | $amount = $this->coercePercent($args[1]); |
||
6843 | |||
6844 | $color[4] = (isset($color[4]) ? $color[4] : 1) - $amount; |
||
6845 | $color[4] = min(1, max(0, $color[4])); |
||
6846 | |||
6847 | return $color; |
||
6848 | } |
||
6849 | |||
6850 | protected static $libFadeOut = ['color', 'amount']; |
||
6851 | protected function libFadeOut($args) |
||
6852 | { |
||
6853 | return $this->libTransparentize($args); |
||
6854 | } |
||
6855 | |||
6856 | protected static $libUnquote = ['string']; |
||
6857 | protected function libUnquote($args) |
||
6858 | { |
||
6859 | $str = $args[0]; |
||
6860 | |||
6861 | if ($str[0] === Type::T_STRING) { |
||
6862 | $str[1] = ''; |
||
6863 | } |
||
6864 | |||
6865 | return $str; |
||
6866 | } |
||
6867 | |||
6868 | protected static $libQuote = ['string']; |
||
6869 | protected function libQuote($args) |
||
6870 | { |
||
6871 | $value = $args[0]; |
||
6872 | |||
6873 | if ($value[0] === Type::T_STRING && ! empty($value[1])) { |
||
6874 | return $value; |
||
6875 | } |
||
6876 | |||
6877 | return [Type::T_STRING, '"', [$value]]; |
||
6878 | } |
||
6879 | |||
6880 | protected static $libPercentage = ['number']; |
||
6881 | protected function libPercentage($args) |
||
6882 | { |
||
6883 | return new Node\Number($this->coercePercent($args[0]) * 100, '%'); |
||
6884 | } |
||
6885 | |||
6886 | protected static $libRound = ['number']; |
||
6887 | protected function libRound($args) |
||
6888 | { |
||
6889 | $num = $args[0]; |
||
6890 | |||
6891 | return new Node\Number(round($num[1]), $num[2]); |
||
6892 | } |
||
6893 | |||
6894 | protected static $libFloor = ['number']; |
||
6895 | protected function libFloor($args) |
||
6896 | { |
||
6897 | $num = $args[0]; |
||
6898 | |||
6899 | return new Node\Number(floor($num[1]), $num[2]); |
||
6900 | } |
||
6901 | |||
6902 | protected static $libCeil = ['number']; |
||
6903 | protected function libCeil($args) |
||
6904 | { |
||
6905 | $num = $args[0]; |
||
6906 | |||
6907 | return new Node\Number(ceil($num[1]), $num[2]); |
||
6908 | } |
||
6909 | |||
6910 | protected static $libAbs = ['number']; |
||
6911 | protected function libAbs($args) |
||
6912 | { |
||
6913 | $num = $args[0]; |
||
6914 | |||
6915 | return new Node\Number(abs($num[1]), $num[2]); |
||
6916 | } |
||
6917 | |||
6918 | protected function libMin($args) |
||
6919 | { |
||
6920 | $numbers = $this->getNormalizedNumbers($args); |
||
6921 | $minOriginal = null; |
||
6922 | $minNormalized = null; |
||
6923 | |||
6924 | foreach ($numbers as $key => $pair) { |
||
6925 | list($original, $normalized) = $pair; |
||
6926 | |||
6927 | if (\is_null($normalized) || \is_null($minNormalized)) { |
||
6928 | if (\is_null($minOriginal) || $original[1] <= $minOriginal[1]) { |
||
6929 | $minOriginal = $original; |
||
6930 | $minNormalized = $normalized; |
||
6931 | } |
||
6932 | } elseif ($normalized[1] <= $minNormalized[1]) { |
||
6933 | $minOriginal = $original; |
||
6934 | $minNormalized = $normalized; |
||
6935 | } |
||
6936 | } |
||
6937 | |||
6938 | return $minOriginal; |
||
6939 | } |
||
6940 | |||
6941 | protected function libMax($args) |
||
6942 | { |
||
6943 | $numbers = $this->getNormalizedNumbers($args); |
||
6944 | $maxOriginal = null; |
||
6945 | $maxNormalized = null; |
||
6946 | |||
6947 | foreach ($numbers as $key => $pair) { |
||
6948 | list($original, $normalized) = $pair; |
||
6949 | |||
6950 | if (\is_null($normalized) || \is_null($maxNormalized)) { |
||
6951 | if (\is_null($maxOriginal) || $original[1] >= $maxOriginal[1]) { |
||
6952 | $maxOriginal = $original; |
||
6953 | $maxNormalized = $normalized; |
||
6954 | } |
||
6955 | } elseif ($normalized[1] >= $maxNormalized[1]) { |
||
6956 | $maxOriginal = $original; |
||
6957 | $maxNormalized = $normalized; |
||
6958 | } |
||
6959 | } |
||
6960 | |||
6961 | return $maxOriginal; |
||
6962 | } |
||
6963 | |||
6964 | /** |
||
6965 | * Helper to normalize args containing numbers |
||
6966 | * |
||
6967 | * @param array $args |
||
6968 | * |
||
6969 | * @return array |
||
6970 | */ |
||
6971 | protected function getNormalizedNumbers($args) |
||
6972 | { |
||
6973 | $unit = null; |
||
6974 | $originalUnit = null; |
||
6975 | $numbers = []; |
||
6976 | |||
6977 | foreach ($args as $key => $item) { |
||
6978 | if ($item[0] !== Type::T_NUMBER) { |
||
6979 | throw $this->error('%s is not a number', $item[0]); |
||
6980 | } |
||
6981 | |||
6982 | $number = $item->normalize(); |
||
6983 | |||
6984 | if (empty($unit)) { |
||
6985 | $unit = $number[2]; |
||
6986 | $originalUnit = $item->unitStr(); |
||
6987 | } elseif ($number[1] && $unit !== $number[2] && ! empty($number[2])) { |
||
6988 | throw $this->error('Incompatible units: "%s" and "%s".', $originalUnit, $item->unitStr()); |
||
6989 | } |
||
6990 | |||
6991 | $numbers[$key] = [$args[$key], empty($number[2]) ? null : $number]; |
||
6992 | } |
||
6993 | |||
6994 | return $numbers; |
||
6995 | } |
||
6996 | |||
6997 | protected static $libLength = ['list']; |
||
6998 | protected function libLength($args) |
||
6999 | { |
||
7000 | $list = $this->coerceList($args[0], ',', true); |
||
7001 | |||
7002 | return \count($list[2]); |
||
7003 | } |
||
7004 | |||
7005 | //protected static $libListSeparator = ['list...']; |
||
7006 | protected function libListSeparator($args) |
||
7007 | { |
||
7008 | if (\count($args) > 1) { |
||
7009 | return 'comma'; |
||
7010 | } |
||
7011 | |||
7012 | $list = $this->coerceList($args[0]); |
||
7013 | |||
7014 | if (\count($list[2]) <= 1) { |
||
7015 | return 'space'; |
||
7016 | } |
||
7017 | |||
7018 | if ($list[1] === ',') { |
||
7019 | return 'comma'; |
||
7020 | } |
||
7021 | |||
7022 | return 'space'; |
||
7023 | } |
||
7024 | |||
7025 | protected static $libNth = ['list', 'n']; |
||
7026 | protected function libNth($args) |
||
7027 | { |
||
7028 | $list = $this->coerceList($args[0], ',', false); |
||
7029 | $n = $this->assertNumber($args[1]); |
||
7030 | |||
7031 | if ($n > 0) { |
||
7032 | $n--; |
||
7033 | } elseif ($n < 0) { |
||
7034 | $n += \count($list[2]); |
||
7035 | } |
||
7036 | |||
7037 | return isset($list[2][$n]) ? $list[2][$n] : static::$defaultValue; |
||
7038 | } |
||
7039 | |||
7040 | protected static $libSetNth = ['list', 'n', 'value']; |
||
7041 | protected function libSetNth($args) |
||
7042 | { |
||
7043 | $list = $this->coerceList($args[0]); |
||
7044 | $n = $this->assertNumber($args[1]); |
||
7045 | |||
7046 | if ($n > 0) { |
||
7047 | $n--; |
||
7048 | } elseif ($n < 0) { |
||
7049 | $n += \count($list[2]); |
||
7050 | } |
||
7051 | |||
7052 | if (! isset($list[2][$n])) { |
||
7053 | throw $this->error('Invalid argument for "n"'); |
||
7054 | } |
||
7055 | |||
7056 | $list[2][$n] = $args[2]; |
||
7057 | |||
7058 | return $list; |
||
7059 | } |
||
7060 | |||
7061 | protected static $libMapGet = ['map', 'key']; |
||
7062 | protected function libMapGet($args) |
||
7063 | { |
||
7064 | $map = $this->assertMap($args[0]); |
||
7065 | $key = $args[1]; |
||
7066 | |||
7067 | if (! \is_null($key)) { |
||
7068 | $key = $this->compileStringContent($this->coerceString($key)); |
||
7069 | |||
7070 | for ($i = \count($map[1]) - 1; $i >= 0; $i--) { |
||
7071 | if ($key === $this->compileStringContent($this->coerceString($map[1][$i]))) { |
||
7072 | return $map[2][$i]; |
||
7073 | } |
||
7074 | } |
||
7075 | } |
||
7076 | |||
7077 | return static::$null; |
||
7078 | } |
||
7079 | |||
7080 | protected static $libMapKeys = ['map']; |
||
7081 | protected function libMapKeys($args) |
||
7082 | { |
||
7083 | $map = $this->assertMap($args[0]); |
||
7084 | $keys = $map[1]; |
||
7085 | |||
7086 | return [Type::T_LIST, ',', $keys]; |
||
7087 | } |
||
7088 | |||
7089 | protected static $libMapValues = ['map']; |
||
7090 | protected function libMapValues($args) |
||
7091 | { |
||
7092 | $map = $this->assertMap($args[0]); |
||
7093 | $values = $map[2]; |
||
7094 | |||
7095 | return [Type::T_LIST, ',', $values]; |
||
7096 | } |
||
7097 | |||
7098 | protected static $libMapRemove = ['map', 'key...']; |
||
7099 | protected function libMapRemove($args) |
||
7100 | { |
||
7101 | $map = $this->assertMap($args[0]); |
||
7102 | $keyList = $this->assertList($args[1]); |
||
7103 | |||
7104 | $keys = []; |
||
7105 | |||
7106 | foreach ($keyList[2] as $key) { |
||
7107 | $keys[] = $this->compileStringContent($this->coerceString($key)); |
||
7108 | } |
||
7109 | |||
7110 | for ($i = \count($map[1]) - 1; $i >= 0; $i--) { |
||
7111 | if (in_array($this->compileStringContent($this->coerceString($map[1][$i])), $keys)) { |
||
7112 | array_splice($map[1], $i, 1); |
||
7113 | array_splice($map[2], $i, 1); |
||
7114 | } |
||
7115 | } |
||
7116 | |||
7117 | return $map; |
||
7118 | } |
||
7119 | |||
7120 | protected static $libMapHasKey = ['map', 'key']; |
||
7121 | protected function libMapHasKey($args) |
||
7122 | { |
||
7123 | $map = $this->assertMap($args[0]); |
||
7124 | $key = $this->compileStringContent($this->coerceString($args[1])); |
||
7125 | |||
7126 | for ($i = \count($map[1]) - 1; $i >= 0; $i--) { |
||
7127 | if ($key === $this->compileStringContent($this->coerceString($map[1][$i]))) { |
||
7128 | return true; |
||
7129 | } |
||
7130 | } |
||
7131 | |||
7132 | return false; |
||
7133 | } |
||
7134 | |||
7135 | protected static $libMapMerge = [ |
||
7136 | ['map1', 'map2'], |
||
7137 | ['map-1', 'map-2'] |
||
7138 | ]; |
||
7139 | protected function libMapMerge($args) |
||
7140 | { |
||
7141 | $map1 = $this->assertMap($args[0]); |
||
7142 | $map2 = $this->assertMap($args[1]); |
||
7143 | |||
7144 | foreach ($map2[1] as $i2 => $key2) { |
||
7145 | $key = $this->compileStringContent($this->coerceString($key2)); |
||
7146 | |||
7147 | foreach ($map1[1] as $i1 => $key1) { |
||
7148 | if ($key === $this->compileStringContent($this->coerceString($key1))) { |
||
7149 | $map1[2][$i1] = $map2[2][$i2]; |
||
7150 | continue 2; |
||
7151 | } |
||
7152 | } |
||
7153 | |||
7154 | $map1[1][] = $map2[1][$i2]; |
||
7155 | $map1[2][] = $map2[2][$i2]; |
||
7156 | } |
||
7157 | |||
7158 | return $map1; |
||
7159 | } |
||
7160 | |||
7161 | protected static $libKeywords = ['args']; |
||
7162 | protected function libKeywords($args) |
||
7163 | { |
||
7164 | $this->assertList($args[0]); |
||
7165 | |||
7166 | $keys = []; |
||
7167 | $values = []; |
||
7168 | |||
7169 | foreach ($args[0][2] as $name => $arg) { |
||
7170 | $keys[] = [Type::T_KEYWORD, $name]; |
||
7171 | $values[] = $arg; |
||
7172 | } |
||
7173 | |||
7174 | return [Type::T_MAP, $keys, $values]; |
||
7175 | } |
||
7176 | |||
7177 | protected static $libIsBracketed = ['list']; |
||
7178 | protected function libIsBracketed($args) |
||
7179 | { |
||
7180 | $list = $args[0]; |
||
7181 | $this->coerceList($list, ' '); |
||
7182 | |||
7183 | if (! empty($list['enclosing']) && $list['enclosing'] === 'bracket') { |
||
7184 | return true; |
||
7185 | } |
||
7186 | |||
7187 | return false; |
||
7188 | } |
||
7189 | |||
7190 | protected function listSeparatorForJoin($list1, $sep) |
||
7191 | { |
||
7192 | if (! isset($sep)) { |
||
7193 | return $list1[1]; |
||
7194 | } |
||
7195 | |||
7196 | switch ($this->compileValue($sep)) { |
||
7197 | case 'comma': |
||
7198 | return ','; |
||
7199 | |||
7200 | case 'space': |
||
7201 | return ' '; |
||
7202 | |||
7203 | default: |
||
7204 | return $list1[1]; |
||
7205 | } |
||
7206 | } |
||
7207 | |||
7208 | protected static $libJoin = ['list1', 'list2', 'separator:null', 'bracketed:auto']; |
||
7209 | protected function libJoin($args) |
||
7210 | { |
||
7211 | list($list1, $list2, $sep, $bracketed) = $args; |
||
7212 | |||
7213 | $list1 = $this->coerceList($list1, ' ', true); |
||
7214 | $list2 = $this->coerceList($list2, ' ', true); |
||
7215 | $sep = $this->listSeparatorForJoin($list1, $sep); |
||
7216 | |||
7217 | if ($bracketed === static::$true) { |
||
7218 | $bracketed = true; |
||
7219 | } elseif ($bracketed === static::$false) { |
||
7220 | $bracketed = false; |
||
7221 | } elseif ($bracketed === [Type::T_KEYWORD, 'auto']) { |
||
7222 | $bracketed = 'auto'; |
||
7223 | } elseif ($bracketed === static::$null) { |
||
7224 | $bracketed = false; |
||
7225 | } else { |
||
7226 | $bracketed = $this->compileValue($bracketed); |
||
7227 | $bracketed = ! ! $bracketed; |
||
7228 | |||
7229 | if ($bracketed === true) { |
||
7230 | $bracketed = true; |
||
7231 | } |
||
7232 | } |
||
7233 | |||
7234 | if ($bracketed === 'auto') { |
||
7235 | $bracketed = false; |
||
7236 | |||
7237 | if (! empty($list1['enclosing']) && $list1['enclosing'] === 'bracket') { |
||
7238 | $bracketed = true; |
||
7239 | } |
||
7240 | } |
||
7241 | |||
7242 | $res = [Type::T_LIST, $sep, array_merge($list1[2], $list2[2])]; |
||
7243 | |||
7244 | if (isset($list1['enclosing'])) { |
||
7245 | $res['enlcosing'] = $list1['enclosing']; |
||
7246 | } |
||
7247 | |||
7248 | if ($bracketed) { |
||
7249 | $res['enclosing'] = 'bracket'; |
||
7250 | } |
||
7251 | |||
7252 | return $res; |
||
7253 | } |
||
7254 | |||
7255 | protected static $libAppend = ['list', 'val', 'separator:null']; |
||
7256 | protected function libAppend($args) |
||
7257 | { |
||
7258 | list($list1, $value, $sep) = $args; |
||
7259 | |||
7260 | $list1 = $this->coerceList($list1, ' ', true); |
||
7261 | $sep = $this->listSeparatorForJoin($list1, $sep); |
||
7262 | $res = [Type::T_LIST, $sep, array_merge($list1[2], [$value])]; |
||
7263 | |||
7264 | if (isset($list1['enclosing'])) { |
||
7265 | $res['enclosing'] = $list1['enclosing']; |
||
7266 | } |
||
7267 | |||
7268 | return $res; |
||
7269 | } |
||
7270 | |||
7271 | protected function libZip($args) |
||
7272 | { |
||
7273 | foreach ($args as $key => $arg) { |
||
7274 | $args[$key] = $this->coerceList($arg); |
||
7275 | } |
||
7276 | |||
7277 | $lists = []; |
||
7278 | $firstList = array_shift($args); |
||
7279 | |||
7280 | foreach ($firstList[2] as $key => $item) { |
||
7281 | $list = [Type::T_LIST, '', [$item]]; |
||
7282 | |||
7283 | foreach ($args as $arg) { |
||
7284 | if (isset($arg[2][$key])) { |
||
7285 | $list[2][] = $arg[2][$key]; |
||
7286 | } else { |
||
7287 | break 2; |
||
7288 | } |
||
7289 | } |
||
7290 | |||
7291 | $lists[] = $list; |
||
7292 | } |
||
7293 | |||
7294 | return [Type::T_LIST, ',', $lists]; |
||
7295 | } |
||
7296 | |||
7297 | protected static $libTypeOf = ['value']; |
||
7298 | protected function libTypeOf($args) |
||
7299 | { |
||
7300 | $value = $args[0]; |
||
7301 | |||
7302 | switch ($value[0]) { |
||
7303 | case Type::T_KEYWORD: |
||
7304 | if ($value === static::$true || $value === static::$false) { |
||
7305 | return 'bool'; |
||
7306 | } |
||
7307 | |||
7308 | if ($this->coerceColor($value)) { |
||
7309 | return 'color'; |
||
7310 | } |
||
7311 | |||
7312 | // fall-thru |
||
7313 | case Type::T_FUNCTION: |
||
7314 | return 'string'; |
||
7315 | |||
7316 | case Type::T_FUNCTION_REFERENCE: |
||
7317 | return 'function'; |
||
7318 | |||
7319 | case Type::T_LIST: |
||
7320 | if (isset($value[3]) && $value[3]) { |
||
7321 | return 'arglist'; |
||
7322 | } |
||
7323 | |||
7324 | // fall-thru |
||
7325 | default: |
||
7326 | return $value[0]; |
||
7327 | } |
||
7328 | } |
||
7329 | |||
7330 | protected static $libUnit = ['number']; |
||
7331 | protected function libUnit($args) |
||
7332 | { |
||
7333 | $num = $args[0]; |
||
7334 | |||
7335 | if ($num[0] === Type::T_NUMBER) { |
||
7336 | return [Type::T_STRING, '"', [$num->unitStr()]]; |
||
7337 | } |
||
7338 | |||
7339 | return ''; |
||
7340 | } |
||
7341 | |||
7342 | protected static $libUnitless = ['number']; |
||
7343 | protected function libUnitless($args) |
||
7344 | { |
||
7345 | $value = $args[0]; |
||
7346 | |||
7347 | return $value[0] === Type::T_NUMBER && $value->unitless(); |
||
7348 | } |
||
7349 | |||
7350 | protected static $libComparable = [ |
||
7351 | ['number1', 'number2'], |
||
7352 | ['number-1', 'number-2'] |
||
7353 | ]; |
||
7354 | protected function libComparable($args) |
||
7355 | { |
||
7356 | list($number1, $number2) = $args; |
||
7357 | |||
7358 | if ( |
||
7359 | ! isset($number1[0]) || $number1[0] !== Type::T_NUMBER || |
||
7360 | ! isset($number2[0]) || $number2[0] !== Type::T_NUMBER |
||
7361 | ) { |
||
7362 | throw $this->error('Invalid argument(s) for "comparable"'); |
||
7363 | } |
||
7364 | |||
7365 | $number1 = $number1->normalize(); |
||
7366 | $number2 = $number2->normalize(); |
||
7367 | |||
7368 | return $number1[2] === $number2[2] || $number1->unitless() || $number2->unitless(); |
||
7369 | } |
||
7370 | |||
7371 | protected static $libStrIndex = ['string', 'substring']; |
||
7372 | protected function libStrIndex($args) |
||
7373 | { |
||
7374 | $string = $this->coerceString($args[0]); |
||
7375 | $stringContent = $this->compileStringContent($string); |
||
7376 | |||
7377 | $substring = $this->coerceString($args[1]); |
||
7378 | $substringContent = $this->compileStringContent($substring); |
||
7379 | |||
7380 | $result = strpos($stringContent, $substringContent); |
||
7381 | |||
7382 | return $result === false ? static::$null : new Node\Number($result + 1, ''); |
||
7383 | } |
||
7384 | |||
7385 | protected static $libStrInsert = ['string', 'insert', 'index']; |
||
7386 | protected function libStrInsert($args) |
||
7387 | { |
||
7388 | $string = $this->coerceString($args[0]); |
||
7389 | $stringContent = $this->compileStringContent($string); |
||
7390 | |||
7391 | $insert = $this->coerceString($args[1]); |
||
7392 | $insertContent = $this->compileStringContent($insert); |
||
7393 | |||
7394 | list(, $index) = $args[2]; |
||
7395 | |||
7396 | $string[2] = [substr_replace($stringContent, $insertContent, $index - 1, 0)]; |
||
7397 | |||
7398 | return $string; |
||
7399 | } |
||
7400 | |||
7401 | protected static $libStrLength = ['string']; |
||
7402 | protected function libStrLength($args) |
||
7403 | { |
||
7404 | $string = $this->coerceString($args[0]); |
||
7405 | $stringContent = $this->compileStringContent($string); |
||
7406 | |||
7407 | return new Node\Number(\strlen($stringContent), ''); |
||
7408 | } |
||
7409 | |||
7410 | protected static $libStrSlice = ['string', 'start-at', 'end-at:-1']; |
||
7411 | protected function libStrSlice($args) |
||
7412 | { |
||
7413 | if (isset($args[2]) && ! $args[2][1]) { |
||
7414 | return static::$nullString; |
||
7415 | } |
||
7416 | |||
7417 | $string = $this->coerceString($args[0]); |
||
7418 | $stringContent = $this->compileStringContent($string); |
||
7419 | |||
7420 | $start = (int) $args[1][1]; |
||
7421 | |||
7422 | if ($start > 0) { |
||
7423 | $start--; |
||
7424 | } |
||
7425 | |||
7426 | $end = isset($args[2]) ? (int) $args[2][1] : -1; |
||
7427 | $length = $end < 0 ? $end + 1 : ($end > 0 ? $end - $start : $end); |
||
7428 | |||
7429 | $string[2] = $length |
||
7430 | ? [substr($stringContent, $start, $length)] |
||
7431 | : [substr($stringContent, $start)]; |
||
7432 | |||
7433 | return $string; |
||
7434 | } |
||
7435 | |||
7436 | protected static $libToLowerCase = ['string']; |
||
7437 | protected function libToLowerCase($args) |
||
7438 | { |
||
7439 | $string = $this->coerceString($args[0]); |
||
7440 | $stringContent = $this->compileStringContent($string); |
||
7441 | |||
7442 | $string[2] = [\function_exists('mb_strtolower') ? mb_strtolower($stringContent) : strtolower($stringContent)]; |
||
7443 | |||
7444 | return $string; |
||
7445 | } |
||
7446 | |||
7447 | protected static $libToUpperCase = ['string']; |
||
7448 | protected function libToUpperCase($args) |
||
7449 | { |
||
7450 | $string = $this->coerceString($args[0]); |
||
7451 | $stringContent = $this->compileStringContent($string); |
||
7452 | |||
7453 | $string[2] = [\function_exists('mb_strtoupper') ? mb_strtoupper($stringContent) : strtoupper($stringContent)]; |
||
7454 | |||
7455 | return $string; |
||
7456 | } |
||
7457 | |||
7458 | protected static $libFeatureExists = ['feature']; |
||
7459 | protected function libFeatureExists($args) |
||
7460 | { |
||
7461 | $string = $this->coerceString($args[0]); |
||
7462 | $name = $this->compileStringContent($string); |
||
7463 | |||
7464 | return $this->toBool( |
||
7465 | \array_key_exists($name, $this->registeredFeatures) ? $this->registeredFeatures[$name] : false |
||
7466 | ); |
||
7467 | } |
||
7468 | |||
7469 | protected static $libFunctionExists = ['name']; |
||
7470 | protected function libFunctionExists($args) |
||
7471 | { |
||
7472 | $string = $this->coerceString($args[0]); |
||
7473 | $name = $this->compileStringContent($string); |
||
7474 | |||
7475 | // user defined functions |
||
7476 | if ($this->has(static::$namespaces['function'] . $name)) { |
||
7477 | return true; |
||
7478 | } |
||
7479 | |||
7480 | $name = $this->normalizeName($name); |
||
7481 | |||
7482 | if (isset($this->userFunctions[$name])) { |
||
7483 | return true; |
||
7484 | } |
||
7485 | |||
7486 | // built-in functions |
||
7487 | $f = $this->getBuiltinFunction($name); |
||
7488 | |||
7489 | return $this->toBool(\is_callable($f)); |
||
7490 | } |
||
7491 | |||
7492 | protected static $libGlobalVariableExists = ['name']; |
||
7493 | protected function libGlobalVariableExists($args) |
||
7494 | { |
||
7495 | $string = $this->coerceString($args[0]); |
||
7496 | $name = $this->compileStringContent($string); |
||
7497 | |||
7498 | return $this->has($name, $this->rootEnv); |
||
7499 | } |
||
7500 | |||
7501 | protected static $libMixinExists = ['name']; |
||
7502 | protected function libMixinExists($args) |
||
7503 | { |
||
7504 | $string = $this->coerceString($args[0]); |
||
7505 | $name = $this->compileStringContent($string); |
||
7506 | |||
7507 | return $this->has(static::$namespaces['mixin'] . $name); |
||
7508 | } |
||
7509 | |||
7510 | protected static $libVariableExists = ['name']; |
||
7511 | protected function libVariableExists($args) |
||
7512 | { |
||
7513 | $string = $this->coerceString($args[0]); |
||
7514 | $name = $this->compileStringContent($string); |
||
7515 | |||
7516 | return $this->has($name); |
||
7517 | } |
||
7518 | |||
7519 | /** |
||
7520 | * Workaround IE7's content counter bug. |
||
7521 | * |
||
7522 | * @param array $args |
||
7523 | * |
||
7524 | * @return array |
||
7525 | */ |
||
7526 | protected function libCounter($args) |
||
7527 | { |
||
7528 | $list = array_map([$this, 'compileValue'], $args); |
||
7529 | |||
7530 | return [Type::T_STRING, '', ['counter(' . implode(',', $list) . ')']]; |
||
7531 | } |
||
7532 | |||
7533 | protected static $libRandom = ['limit:1']; |
||
7534 | protected function libRandom($args) |
||
7535 | { |
||
7536 | if (isset($args[0])) { |
||
7537 | $n = $this->assertNumber($args[0]); |
||
7538 | |||
7539 | if ($n < 1) { |
||
7540 | throw $this->error("\$limit must be greater than or equal to 1"); |
||
7541 | } |
||
7542 | |||
7543 | if ($n - \intval($n) > 0) { |
||
7544 | throw $this->error("Expected \$limit to be an integer but got $n for `random`"); |
||
7545 | } |
||
7546 | |||
7547 | return new Node\Number(mt_rand(1, \intval($n)), ''); |
||
7548 | } |
||
7549 | |||
7550 | return new Node\Number(mt_rand(1, mt_getrandmax()), ''); |
||
7551 | } |
||
7552 | |||
7553 | protected function libUniqueId() |
||
7554 | { |
||
7555 | static $id; |
||
7556 | |||
7557 | if (! isset($id)) { |
||
7558 | $id = PHP_INT_SIZE === 4 |
||
7559 | ? mt_rand(0, pow(36, 5)) . str_pad(mt_rand(0, pow(36, 5)) % 10000000, 7, '0', STR_PAD_LEFT) |
||
7560 | : mt_rand(0, pow(36, 8)); |
||
7561 | } |
||
7562 | |||
7563 | $id += mt_rand(0, 10) + 1; |
||
7564 | |||
7565 | return [Type::T_STRING, '', ['u' . str_pad(base_convert($id, 10, 36), 8, '0', STR_PAD_LEFT)]]; |
||
7566 | } |
||
7567 | |||
7568 | protected function inspectFormatValue($value, $force_enclosing_display = false) |
||
7569 | { |
||
7570 | if ($value === static::$null) { |
||
7571 | $value = [Type::T_KEYWORD, 'null']; |
||
7572 | } |
||
7573 | |||
7574 | $stringValue = [$value]; |
||
7575 | |||
7576 | if ($value[0] === Type::T_LIST) { |
||
7577 | if (end($value[2]) === static::$null) { |
||
7578 | array_pop($value[2]); |
||
7579 | $value[2][] = [Type::T_STRING, '', ['']]; |
||
7580 | $force_enclosing_display = true; |
||
7581 | } |
||
7582 | |||
7583 | if ( |
||
7584 | ! empty($value['enclosing']) && |
||
7585 | ($force_enclosing_display || |
||
7586 | ($value['enclosing'] === 'bracket') || |
||
7587 | ! \count($value[2])) |
||
7588 | ) { |
||
7589 | $value['enclosing'] = 'forced_' . $value['enclosing']; |
||
7590 | $force_enclosing_display = true; |
||
7591 | } |
||
7592 | |||
7593 | foreach ($value[2] as $k => $listelement) { |
||
7594 | $value[2][$k] = $this->inspectFormatValue($listelement, $force_enclosing_display); |
||
7595 | } |
||
7596 | |||
7597 | $stringValue = [$value]; |
||
7598 | } |
||
7599 | |||
7600 | return [Type::T_STRING, '', $stringValue]; |
||
7601 | } |
||
7602 | |||
7603 | protected static $libInspect = ['value']; |
||
7604 | protected function libInspect($args) |
||
7605 | { |
||
7606 | $value = $args[0]; |
||
7607 | |||
7608 | return $this->inspectFormatValue($value); |
||
7609 | } |
||
7610 | |||
7611 | /** |
||
7612 | * Preprocess selector args |
||
7613 | * |
||
7614 | * @param array $arg |
||
7615 | * |
||
7616 | * @return array|boolean |
||
7617 | */ |
||
7618 | protected function getSelectorArg($arg) |
||
7619 | { |
||
7620 | static $parser = null; |
||
7621 | |||
7622 | if (\is_null($parser)) { |
||
7623 | $parser = $this->parserFactory(__METHOD__); |
||
7624 | } |
||
7625 | |||
7626 | $arg = $this->libUnquote([$arg]); |
||
7627 | $arg = $this->compileValue($arg); |
||
7628 | |||
7629 | $parsedSelector = []; |
||
7630 | |||
7631 | if ($parser->parseSelector($arg, $parsedSelector)) { |
||
7632 | $selector = $this->evalSelectors($parsedSelector); |
||
7633 | $gluedSelector = $this->glueFunctionSelectors($selector); |
||
7634 | |||
7635 | return $gluedSelector; |
||
7636 | } |
||
7637 | |||
7638 | return false; |
||
7639 | } |
||
7640 | |||
7641 | /** |
||
7642 | * Postprocess selector to output in right format |
||
7643 | * |
||
7644 | * @param array $selectors |
||
7645 | * |
||
7646 | * @return string |
||
7647 | */ |
||
7648 | protected function formatOutputSelector($selectors) |
||
7649 | { |
||
7650 | $selectors = $this->collapseSelectors($selectors, true); |
||
7651 | |||
7652 | return $selectors; |
||
7653 | } |
||
7654 | |||
7655 | protected static $libIsSuperselector = ['super', 'sub']; |
||
7656 | protected function libIsSuperselector($args) |
||
7657 | { |
||
7658 | list($super, $sub) = $args; |
||
7659 | |||
7660 | $super = $this->getSelectorArg($super); |
||
7661 | $sub = $this->getSelectorArg($sub); |
||
7662 | |||
7663 | return $this->isSuperSelector($super, $sub); |
||
7664 | } |
||
7665 | |||
7666 | /** |
||
7667 | * Test a $super selector again $sub |
||
7668 | * |
||
7669 | * @param array $super |
||
7670 | * @param array $sub |
||
7671 | * |
||
7672 | * @return boolean |
||
7673 | */ |
||
7674 | protected function isSuperSelector($super, $sub) |
||
7675 | { |
||
7676 | // one and only one selector for each arg |
||
7677 | if (! $super || \count($super) !== 1) { |
||
7678 | throw $this->error('Invalid super selector for isSuperSelector()'); |
||
7679 | } |
||
7680 | |||
7681 | if (! $sub || \count($sub) !== 1) { |
||
7682 | throw $this->error('Invalid sub selector for isSuperSelector()'); |
||
7683 | } |
||
7684 | |||
7685 | $super = reset($super); |
||
7686 | $sub = reset($sub); |
||
7687 | |||
7688 | $i = 0; |
||
7689 | $nextMustMatch = false; |
||
7690 | |||
7691 | foreach ($super as $node) { |
||
7692 | $compound = ''; |
||
7693 | |||
7694 | array_walk_recursive( |
||
7695 | $node, |
||
7696 | function ($value, $key) use (&$compound) { |
||
7697 | $compound .= $value; |
||
7698 | } |
||
7699 | ); |
||
7700 | |||
7701 | if ($this->isImmediateRelationshipCombinator($compound)) { |
||
7702 | if ($node !== $sub[$i]) { |
||
7703 | return false; |
||
7704 | } |
||
7705 | |||
7706 | $nextMustMatch = true; |
||
7707 | $i++; |
||
7708 | } else { |
||
7709 | while ($i < \count($sub) && ! $this->isSuperPart($node, $sub[$i])) { |
||
7710 | if ($nextMustMatch) { |
||
7711 | return false; |
||
7712 | } |
||
7713 | |||
7714 | $i++; |
||
7715 | } |
||
7716 | |||
7717 | if ($i >= \count($sub)) { |
||
7718 | return false; |
||
7719 | } |
||
7720 | |||
7721 | $nextMustMatch = false; |
||
7722 | $i++; |
||
7723 | } |
||
7724 | } |
||
7725 | |||
7726 | return true; |
||
7727 | } |
||
7728 | |||
7729 | /** |
||
7730 | * Test a part of super selector again a part of sub selector |
||
7731 | * |
||
7732 | * @param array $superParts |
||
7733 | * @param array $subParts |
||
7734 | * |
||
7735 | * @return boolean |
||
7736 | */ |
||
7737 | protected function isSuperPart($superParts, $subParts) |
||
7738 | { |
||
7739 | $i = 0; |
||
7740 | |||
7741 | foreach ($superParts as $superPart) { |
||
7742 | while ($i < \count($subParts) && $subParts[$i] !== $superPart) { |
||
7743 | $i++; |
||
7744 | } |
||
7745 | |||
7746 | if ($i >= \count($subParts)) { |
||
7747 | return false; |
||
7748 | } |
||
7749 | |||
7750 | $i++; |
||
7751 | } |
||
7752 | |||
7753 | return true; |
||
7754 | } |
||
7755 | |||
7756 | protected static $libSelectorAppend = ['selector...']; |
||
7757 | protected function libSelectorAppend($args) |
||
7758 | { |
||
7759 | // get the selector... list |
||
7760 | $args = reset($args); |
||
7761 | $args = $args[2]; |
||
7762 | |||
7763 | if (\count($args) < 1) { |
||
7764 | throw $this->error('selector-append() needs at least 1 argument'); |
||
7765 | } |
||
7766 | |||
7767 | $selectors = array_map([$this, 'getSelectorArg'], $args); |
||
7768 | |||
7769 | return $this->formatOutputSelector($this->selectorAppend($selectors)); |
||
7770 | } |
||
7771 | |||
7772 | /** |
||
7773 | * Append parts of the last selector in the list to the previous, recursively |
||
7774 | * |
||
7775 | * @param array $selectors |
||
7776 | * |
||
7777 | * @return array |
||
7778 | * |
||
7779 | * @throws \ScssPhp\ScssPhp\Exception\CompilerException |
||
7780 | */ |
||
7781 | protected function selectorAppend($selectors) |
||
7782 | { |
||
7783 | $lastSelectors = array_pop($selectors); |
||
7784 | |||
7785 | if (! $lastSelectors) { |
||
7786 | throw $this->error('Invalid selector list in selector-append()'); |
||
7787 | } |
||
7788 | |||
7789 | while (\count($selectors)) { |
||
7790 | $previousSelectors = array_pop($selectors); |
||
7791 | |||
7792 | if (! $previousSelectors) { |
||
7793 | throw $this->error('Invalid selector list in selector-append()'); |
||
7794 | } |
||
7795 | |||
7796 | // do the trick, happening $lastSelector to $previousSelector |
||
7797 | $appended = []; |
||
7798 | |||
7799 | foreach ($lastSelectors as $lastSelector) { |
||
7800 | $previous = $previousSelectors; |
||
7801 | |||
7802 | foreach ($lastSelector as $lastSelectorParts) { |
||
7803 | foreach ($lastSelectorParts as $lastSelectorPart) { |
||
7804 | foreach ($previous as $i => $previousSelector) { |
||
7805 | foreach ($previousSelector as $j => $previousSelectorParts) { |
||
7806 | $previous[$i][$j][] = $lastSelectorPart; |
||
7807 | } |
||
7808 | } |
||
7809 | } |
||
7810 | } |
||
7811 | |||
7812 | foreach ($previous as $ps) { |
||
7813 | $appended[] = $ps; |
||
7814 | } |
||
7815 | } |
||
7816 | |||
7817 | $lastSelectors = $appended; |
||
7818 | } |
||
7819 | |||
7820 | return $lastSelectors; |
||
7821 | } |
||
7822 | |||
7823 | protected static $libSelectorExtend = [ |
||
7824 | ['selector', 'extendee', 'extender'], |
||
7825 | ['selectors', 'extendee', 'extender'] |
||
7826 | ]; |
||
7827 | protected function libSelectorExtend($args) |
||
7828 | { |
||
7829 | list($selectors, $extendee, $extender) = $args; |
||
7830 | |||
7831 | $selectors = $this->getSelectorArg($selectors); |
||
7832 | $extendee = $this->getSelectorArg($extendee); |
||
7833 | $extender = $this->getSelectorArg($extender); |
||
7834 | |||
7835 | if (! $selectors || ! $extendee || ! $extender) { |
||
7836 | throw $this->error('selector-extend() invalid arguments'); |
||
7837 | } |
||
7838 | |||
7839 | $extended = $this->extendOrReplaceSelectors($selectors, $extendee, $extender); |
||
7840 | |||
7841 | return $this->formatOutputSelector($extended); |
||
7842 | } |
||
7843 | |||
7844 | protected static $libSelectorReplace = [ |
||
7845 | ['selector', 'original', 'replacement'], |
||
7846 | ['selectors', 'original', 'replacement'] |
||
7847 | ]; |
||
7848 | protected function libSelectorReplace($args) |
||
7863 | } |
||
7864 | |||
7865 | /** |
||
7866 | * Extend/replace in selectors |
||
7867 | * used by selector-extend and selector-replace that use the same logic |
||
7868 | * |
||
7869 | * @param array $selectors |
||
7870 | * @param array $extendee |
||
7871 | * @param array $extender |
||
7872 | * @param boolean $replace |
||
7873 | * |
||
7874 | * @return array |
||
7875 | */ |
||
7876 | protected function extendOrReplaceSelectors($selectors, $extendee, $extender, $replace = false) |
||
7877 | { |
||
7878 | $saveExtends = $this->extends; |
||
7879 | $saveExtendsMap = $this->extendsMap; |
||
7880 | |||
7881 | $this->extends = []; |
||
7882 | $this->extendsMap = []; |
||
7883 | |||
7884 | foreach ($extendee as $es) { |
||
7885 | // only use the first one |
||
7886 | $this->pushExtends(reset($es), $extender, null); |
||
7887 | } |
||
7888 | |||
7889 | $extended = []; |
||
7890 | |||
7891 | foreach ($selectors as $selector) { |
||
7892 | if (! $replace) { |
||
7893 | $extended[] = $selector; |
||
7894 | } |
||
7895 | |||
7896 | $n = \count($extended); |
||
7897 | |||
7898 | $this->matchExtends($selector, $extended); |
||
7899 | |||
7900 | // if didnt match, keep the original selector if we are in a replace operation |
||
7901 | if ($replace && \count($extended) === $n) { |
||
7902 | $extended[] = $selector; |
||
7903 | } |
||
7904 | } |
||
7905 | |||
7906 | $this->extends = $saveExtends; |
||
7907 | $this->extendsMap = $saveExtendsMap; |
||
7908 | |||
7909 | return $extended; |
||
7910 | } |
||
7911 | |||
7912 | protected static $libSelectorNest = ['selector...']; |
||
7913 | protected function libSelectorNest($args) |
||
7914 | { |
||
7915 | // get the selector... list |
||
7916 | $args = reset($args); |
||
7917 | $args = $args[2]; |
||
7918 | |||
7919 | if (\count($args) < 1) { |
||
7920 | throw $this->error('selector-nest() needs at least 1 argument'); |
||
7921 | } |
||
7922 | |||
7923 | $selectorsMap = array_map([$this, 'getSelectorArg'], $args); |
||
7924 | $envs = []; |
||
7925 | |||
7926 | foreach ($selectorsMap as $selectors) { |
||
7927 | $env = new Environment(); |
||
7928 | $env->selectors = $selectors; |
||
7929 | |||
7930 | $envs[] = $env; |
||
7931 | } |
||
7932 | |||
7933 | $envs = array_reverse($envs); |
||
7934 | $env = $this->extractEnv($envs); |
||
7935 | $outputSelectors = $this->multiplySelectors($env); |
||
7936 | |||
7937 | return $this->formatOutputSelector($outputSelectors); |
||
7938 | } |
||
7939 | |||
7940 | protected static $libSelectorParse = [ |
||
7941 | ['selector'], |
||
7942 | ['selectors'] |
||
7943 | ]; |
||
7944 | protected function libSelectorParse($args) |
||
7945 | { |
||
7946 | $selectors = reset($args); |
||
7947 | $selectors = $this->getSelectorArg($selectors); |
||
7948 | |||
7949 | return $this->formatOutputSelector($selectors); |
||
7950 | } |
||
7951 | |||
7952 | protected static $libSelectorUnify = ['selectors1', 'selectors2']; |
||
7953 | protected function libSelectorUnify($args) |
||
7954 | { |
||
7955 | list($selectors1, $selectors2) = $args; |
||
7956 | |||
7957 | $selectors1 = $this->getSelectorArg($selectors1); |
||
7958 | $selectors2 = $this->getSelectorArg($selectors2); |
||
7959 | |||
7960 | if (! $selectors1 || ! $selectors2) { |
||
7961 | throw $this->error('selector-unify() invalid arguments'); |
||
7962 | } |
||
7963 | |||
7964 | // only consider the first compound of each |
||
7965 | $compound1 = reset($selectors1); |
||
7966 | $compound2 = reset($selectors2); |
||
7967 | |||
7968 | // unify them and that's it |
||
7969 | $unified = $this->unifyCompoundSelectors($compound1, $compound2); |
||
7970 | |||
7971 | return $this->formatOutputSelector($unified); |
||
7972 | } |
||
7973 | |||
7974 | /** |
||
7975 | * The selector-unify magic as its best |
||
7976 | * (at least works as expected on test cases) |
||
7977 | * |
||
7978 | * @param array $compound1 |
||
7979 | * @param array $compound2 |
||
7980 | * |
||
7981 | * @return array|mixed |
||
7982 | */ |
||
7983 | protected function unifyCompoundSelectors($compound1, $compound2) |
||
7984 | { |
||
7985 | if (! \count($compound1)) { |
||
7986 | return $compound2; |
||
7987 | } |
||
7988 | |||
7989 | if (! \count($compound2)) { |
||
7990 | return $compound1; |
||
7991 | } |
||
7992 | |||
7993 | // check that last part are compatible |
||
7994 | $lastPart1 = array_pop($compound1); |
||
7995 | $lastPart2 = array_pop($compound2); |
||
7996 | $last = $this->mergeParts($lastPart1, $lastPart2); |
||
7997 | |||
7998 | if (! $last) { |
||
7999 | return [[]]; |
||
8000 | } |
||
8001 | |||
8002 | $unifiedCompound = [$last]; |
||
8003 | $unifiedSelectors = [$unifiedCompound]; |
||
8004 | |||
8005 | // do the rest |
||
8006 | while (\count($compound1) || \count($compound2)) { |
||
8007 | $part1 = end($compound1); |
||
8008 | $part2 = end($compound2); |
||
8009 | |||
8010 | if ($part1 && ($match2 = $this->matchPartInCompound($part1, $compound2))) { |
||
8011 | list($compound2, $part2, $after2) = $match2; |
||
8012 | |||
8013 | if ($after2) { |
||
8014 | $unifiedSelectors = $this->prependSelectors($unifiedSelectors, $after2); |
||
8015 | } |
||
8016 | |||
8017 | $c = $this->mergeParts($part1, $part2); |
||
8018 | $unifiedSelectors = $this->prependSelectors($unifiedSelectors, [$c]); |
||
8019 | |||
8020 | $part1 = $part2 = null; |
||
8021 | |||
8022 | array_pop($compound1); |
||
8023 | } |
||
8024 | |||
8025 | if ($part2 && ($match1 = $this->matchPartInCompound($part2, $compound1))) { |
||
8026 | list($compound1, $part1, $after1) = $match1; |
||
8027 | |||
8028 | if ($after1) { |
||
8029 | $unifiedSelectors = $this->prependSelectors($unifiedSelectors, $after1); |
||
8030 | } |
||
8031 | |||
8032 | $c = $this->mergeParts($part2, $part1); |
||
8033 | $unifiedSelectors = $this->prependSelectors($unifiedSelectors, [$c]); |
||
8034 | |||
8035 | $part1 = $part2 = null; |
||
8036 | |||
8037 | array_pop($compound2); |
||
8038 | } |
||
8039 | |||
8040 | $new = []; |
||
8041 | |||
8042 | if ($part1 && $part2) { |
||
8043 | array_pop($compound1); |
||
8044 | array_pop($compound2); |
||
8045 | |||
8046 | $s = $this->prependSelectors($unifiedSelectors, [$part2]); |
||
8047 | $new = array_merge($new, $this->prependSelectors($s, [$part1])); |
||
8048 | $s = $this->prependSelectors($unifiedSelectors, [$part1]); |
||
8049 | $new = array_merge($new, $this->prependSelectors($s, [$part2])); |
||
8050 | } elseif ($part1) { |
||
8051 | array_pop($compound1); |
||
8052 | |||
8053 | $new = array_merge($new, $this->prependSelectors($unifiedSelectors, [$part1])); |
||
8054 | } elseif ($part2) { |
||
8055 | array_pop($compound2); |
||
8056 | |||
8057 | $new = array_merge($new, $this->prependSelectors($unifiedSelectors, [$part2])); |
||
8058 | } |
||
8059 | |||
8060 | if ($new) { |
||
8061 | $unifiedSelectors = $new; |
||
8062 | } |
||
8063 | } |
||
8064 | |||
8065 | return $unifiedSelectors; |
||
8066 | } |
||
8067 | |||
8068 | /** |
||
8069 | * Prepend each selector from $selectors with $parts |
||
8070 | * |
||
8071 | * @param array $selectors |
||
8072 | * @param array $parts |
||
8073 | * |
||
8074 | * @return array |
||
8075 | */ |
||
8076 | protected function prependSelectors($selectors, $parts) |
||
8077 | { |
||
8078 | $new = []; |
||
8079 | |||
8080 | foreach ($selectors as $compoundSelector) { |
||
8081 | array_unshift($compoundSelector, $parts); |
||
8082 | |||
8083 | $new[] = $compoundSelector; |
||
8084 | } |
||
8085 | |||
8086 | return $new; |
||
8087 | } |
||
8088 | |||
8089 | /** |
||
8090 | * Try to find a matching part in a compound: |
||
8091 | * - with same html tag name |
||
8092 | * - with some class or id or something in common |
||
8093 | * |
||
8094 | * @param array $part |
||
8095 | * @param array $compound |
||
8096 | * |
||
8097 | * @return array|boolean |
||
8098 | */ |
||
8099 | protected function matchPartInCompound($part, $compound) |
||
8100 | { |
||
8101 | $partTag = $this->findTagName($part); |
||
8102 | $before = $compound; |
||
8103 | $after = []; |
||
8104 | |||
8105 | // try to find a match by tag name first |
||
8106 | while (\count($before)) { |
||
8107 | $p = array_pop($before); |
||
8108 | |||
8109 | if ($partTag && $partTag !== '*' && $partTag == $this->findTagName($p)) { |
||
8110 | return [$before, $p, $after]; |
||
8111 | } |
||
8112 | |||
8113 | $after[] = $p; |
||
8114 | } |
||
8115 | |||
8116 | // try again matching a non empty intersection and a compatible tagname |
||
8117 | $before = $compound; |
||
8118 | $after = []; |
||
8119 | |||
8120 | while (\count($before)) { |
||
8121 | $p = array_pop($before); |
||
8122 | |||
8123 | if ($this->checkCompatibleTags($partTag, $this->findTagName($p))) { |
||
8124 | if (\count(array_intersect($part, $p))) { |
||
8125 | return [$before, $p, $after]; |
||
8126 | } |
||
8127 | } |
||
8128 | |||
8129 | $after[] = $p; |
||
8130 | } |
||
8131 | |||
8132 | return false; |
||
8133 | } |
||
8134 | |||
8135 | /** |
||
8136 | * Merge two part list taking care that |
||
8137 | * - the html tag is coming first - if any |
||
8138 | * - the :something are coming last |
||
8139 | * |
||
8140 | * @param array $parts1 |
||
8141 | * @param array $parts2 |
||
8142 | * |
||
8143 | * @return array |
||
8144 | */ |
||
8145 | protected function mergeParts($parts1, $parts2) |
||
8146 | { |
||
8147 | $tag1 = $this->findTagName($parts1); |
||
8148 | $tag2 = $this->findTagName($parts2); |
||
8149 | $tag = $this->checkCompatibleTags($tag1, $tag2); |
||
8150 | |||
8151 | // not compatible tags |
||
8152 | if ($tag === false) { |
||
8153 | return []; |
||
8154 | } |
||
8155 | |||
8156 | if ($tag) { |
||
8157 | if ($tag1) { |
||
8158 | $parts1 = array_diff($parts1, [$tag1]); |
||
8159 | } |
||
8160 | |||
8161 | if ($tag2) { |
||
8162 | $parts2 = array_diff($parts2, [$tag2]); |
||
8163 | } |
||
8164 | } |
||
8165 | |||
8166 | $mergedParts = array_merge($parts1, $parts2); |
||
8167 | $mergedOrderedParts = []; |
||
8168 | |||
8169 | foreach ($mergedParts as $part) { |
||
8170 | if (strpos($part, ':') === 0) { |
||
8171 | $mergedOrderedParts[] = $part; |
||
8172 | } |
||
8173 | } |
||
8174 | |||
8175 | $mergedParts = array_diff($mergedParts, $mergedOrderedParts); |
||
8176 | $mergedParts = array_merge($mergedParts, $mergedOrderedParts); |
||
8177 | |||
8178 | if ($tag) { |
||
8179 | array_unshift($mergedParts, $tag); |
||
8180 | } |
||
8181 | |||
8182 | return $mergedParts; |
||
8183 | } |
||
8184 | |||
8185 | /** |
||
8186 | * Check the compatibility between two tag names: |
||
8187 | * if both are defined they should be identical or one has to be '*' |
||
8188 | * |
||
8189 | * @param string $tag1 |
||
8190 | * @param string $tag2 |
||
8191 | * |
||
8192 | * @return array|boolean |
||
8193 | */ |
||
8194 | protected function checkCompatibleTags($tag1, $tag2) |
||
8195 | { |
||
8196 | $tags = [$tag1, $tag2]; |
||
8197 | $tags = array_unique($tags); |
||
8198 | $tags = array_filter($tags); |
||
8199 | |||
8200 | if (\count($tags) > 1) { |
||
8201 | $tags = array_diff($tags, ['*']); |
||
8202 | } |
||
8203 | |||
8204 | // not compatible nodes |
||
8205 | if (\count($tags) > 1) { |
||
8206 | return false; |
||
8207 | } |
||
8208 | |||
8209 | return $tags; |
||
8210 | } |
||
8211 | |||
8212 | /** |
||
8213 | * Find the html tag name in a selector parts list |
||
8214 | * |
||
8215 | * @param array $parts |
||
8216 | * |
||
8217 | * @return mixed|string |
||
8218 | */ |
||
8219 | protected function findTagName($parts) |
||
8220 | { |
||
8221 | foreach ($parts as $part) { |
||
8222 | if (! preg_match('/^[\[.:#%_-]/', $part)) { |
||
8223 | return $part; |
||
8224 | } |
||
8225 | } |
||
8226 | |||
8227 | return ''; |
||
8228 | } |
||
8229 | |||
8230 | protected static $libSimpleSelectors = ['selector']; |
||
8231 | protected function libSimpleSelectors($args) |
||
8249 | } |
||
8250 | |||
8251 | protected static $libScssphpGlob = ['pattern']; |
||
8252 | protected function libScssphpGlob($args) |
||
8253 | { |
||
8254 | $string = $this->coerceString($args[0]); |
||
8255 | $pattern = $this->compileStringContent($string); |
||
8256 | $matches = glob($pattern); |
||
8257 | $listParts = []; |
||
8258 | |||
8259 | foreach ($matches as $match) { |
||
8260 | if (! is_file($match)) { |
||
8261 | continue; |
||
8262 | } |
||
8263 | |||
8264 | $listParts[] = [Type::T_STRING, '"', [$match]]; |
||
8265 | } |
||
8266 | |||
8267 | return [Type::T_LIST, ',', $listParts]; |
||
8268 | } |
||
8269 | } |
||
8270 |