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