1<?php 2 3/** 4 * lessphp v0.5.0 5 * http://leafo.net/lessphp 6 * 7 * LESS CSS compiler, adapted from http://lesscss.org 8 * 9 * Copyright 2013, Leaf Corcoran <leafot@gmail.com> 10 * Licensed under MIT or GPLv3, see LICENSE 11 */ 12 13 14/** 15 * The LESS compiler and parser. 16 * 17 * Converting LESS to CSS is a three stage process. The incoming file is parsed 18 * by `lessc_parser` into a syntax tree, then it is compiled into another tree 19 * representing the CSS structure by `lessc`. The CSS tree is fed into a 20 * formatter, like `lessc_formatter` which then outputs CSS as a string. 21 * 22 * During the first compile, all values are *reduced*, which means that their 23 * types are brought to the lowest form before being dump as strings. This 24 * handles math equations, variable dereferences, and the like. 25 * 26 * The `parse` function of `lessc` is the entry point. 27 * 28 * In summary: 29 * 30 * The `lessc` class creates an instance of the parser, feeds it LESS code, 31 * then transforms the resulting tree to a CSS tree. This class also holds the 32 * evaluation context, such as all available mixins and variables at any given 33 * time. 34 * 35 * The `lessc_parser` class is only concerned with parsing its input. 36 * 37 * The `lessc_formatter` takes a CSS tree, and dumps it to a formatted string, 38 * handling things like indentation. 39 */ 40class lessc { 41 public static $VERSION = "v0.5.0"; 42 43 public static $TRUE = array("keyword", "true"); 44 public static $FALSE = array("keyword", "false"); 45 46 protected $libFunctions = array(); 47 protected $registeredVars = array(); 48 protected $preserveComments = false; 49 50 public $vPrefix = '@'; // prefix of abstract properties 51 public $mPrefix = '$'; // prefix of abstract blocks 52 public $parentSelector = '&'; 53 54 public $importDisabled = false; 55 public $importDir = ''; 56 57 protected $numberPrecision = null; 58 59 protected $allParsedFiles = array(); 60 61 // set to the parser that generated the current line when compiling 62 // so we know how to create error messages 63 protected $sourceParser = null; 64 protected $sourceLoc = null; 65 66 protected static $nextImportId = 0; // uniquely identify imports 67 68 // attempts to find the path of an import url, returns null for css files 69 protected function findImport($url) { 70 foreach ((array)$this->importDir as $dir) { 71 $full = $dir.(substr($dir, -1) != '/' ? '/' : '').$url; 72 if ($this->fileExists($file = $full.'.less') || $this->fileExists($file = $full)) { 73 return $file; 74 } 75 } 76 77 return null; 78 } 79 80 protected function fileExists($name) { 81 return is_file($name); 82 } 83 84 public static function compressList($items, $delim) { 85 if (!isset($items[1]) && isset($items[0])) return $items[0]; 86 else return array('list', $delim, $items); 87 } 88 89 public static function preg_quote($what) { 90 return preg_quote($what, '/'); 91 } 92 93 protected function tryImport($importPath, $parentBlock, $out) { 94 if ($importPath[0] == "function" && $importPath[1] == "url") { 95 $importPath = $this->flattenList($importPath[2]); 96 } 97 98 $str = $this->coerceString($importPath); 99 if ($str === null) return false; 100 101 $url = $this->compileValue($this->lib_e($str)); 102 103 // don't import if it ends in css 104 if (substr_compare($url, '.css', -4, 4) === 0) return false; 105 106 $realPath = $this->findImport($url); 107 108 if ($realPath === null) return false; 109 110 if ($this->importDisabled) { 111 return array(false, "/* import disabled */"); 112 } 113 114 if (isset($this->allParsedFiles[realpath($realPath)])) { 115 return array(false, null); 116 } 117 118 $this->addParsedFile($realPath); 119 $parser = $this->makeParser($realPath); 120 $root = $parser->parse(file_get_contents($realPath)); 121 122 // set the parents of all the block props 123 foreach ($root->props as $prop) { 124 if ($prop[0] == "block") { 125 $prop[1]->parent = $parentBlock; 126 } 127 } 128 129 // copy mixins into scope, set their parents 130 // bring blocks from import into current block 131 // TODO: need to mark the source parser these came from this file 132 foreach ($root->children as $childName => $child) { 133 if (isset($parentBlock->children[$childName])) { 134 $parentBlock->children[$childName] = array_merge( 135 $parentBlock->children[$childName], 136 $child); 137 } else { 138 $parentBlock->children[$childName] = $child; 139 } 140 } 141 142 $pi = pathinfo($realPath); 143 $dir = $pi["dirname"]; 144 145 list($top, $bottom) = $this->sortProps($root->props, true); 146 $this->compileImportedProps($top, $parentBlock, $out, $parser, $dir); 147 148 return array(true, $bottom, $parser, $dir); 149 } 150 151 protected function compileImportedProps($props, $block, $out, $sourceParser, $importDir) { 152 $oldSourceParser = $this->sourceParser; 153 154 $oldImport = $this->importDir; 155 156 // TODO: this is because the importDir api is stupid 157 $this->importDir = (array)$this->importDir; 158 array_unshift($this->importDir, $importDir); 159 160 foreach ($props as $prop) { 161 $this->compileProp($prop, $block, $out); 162 } 163 164 $this->importDir = $oldImport; 165 $this->sourceParser = $oldSourceParser; 166 } 167 168 /** 169 * Recursively compiles a block. 170 * 171 * A block is analogous to a CSS block in most cases. A single LESS document 172 * is encapsulated in a block when parsed, but it does not have parent tags 173 * so all of it's children appear on the root level when compiled. 174 * 175 * Blocks are made up of props and children. 176 * 177 * Props are property instructions, array tuples which describe an action 178 * to be taken, eg. write a property, set a variable, mixin a block. 179 * 180 * The children of a block are just all the blocks that are defined within. 181 * This is used to look up mixins when performing a mixin. 182 * 183 * Compiling the block involves pushing a fresh environment on the stack, 184 * and iterating through the props, compiling each one. 185 * 186 * See lessc::compileProp() 187 * 188 */ 189 protected function compileBlock($block) { 190 switch ($block->type) { 191 case "root": 192 $this->compileRoot($block); 193 break; 194 case null: 195 $this->compileCSSBlock($block); 196 break; 197 case "media": 198 $this->compileMedia($block); 199 break; 200 case "directive": 201 $name = "@" . $block->name; 202 if (!empty($block->value)) { 203 $name .= " " . $this->compileValue($this->reduce($block->value)); 204 } 205 206 $this->compileNestedBlock($block, array($name)); 207 break; 208 default: 209 $this->throwError("unknown block type: $block->type\n"); 210 } 211 } 212 213 protected function compileCSSBlock($block) { 214 $env = $this->pushEnv(); 215 216 $selectors = $this->compileSelectors($block->tags); 217 $env->selectors = $this->multiplySelectors($selectors); 218 $out = $this->makeOutputBlock(null, $env->selectors); 219 220 $this->scope->children[] = $out; 221 $this->compileProps($block, $out); 222 223 $block->scope = $env; // mixins carry scope with them! 224 $this->popEnv(); 225 } 226 227 protected function compileMedia($media) { 228 $env = $this->pushEnv($media); 229 $parentScope = $this->mediaParent($this->scope); 230 231 $query = $this->compileMediaQuery($this->multiplyMedia($env)); 232 233 $this->scope = $this->makeOutputBlock($media->type, array($query)); 234 $parentScope->children[] = $this->scope; 235 236 $this->compileProps($media, $this->scope); 237 238 if (count($this->scope->lines) > 0) { 239 $orphanSelelectors = $this->findClosestSelectors(); 240 if (!is_null($orphanSelelectors)) { 241 $orphan = $this->makeOutputBlock(null, $orphanSelelectors); 242 $orphan->lines = $this->scope->lines; 243 array_unshift($this->scope->children, $orphan); 244 $this->scope->lines = array(); 245 } 246 } 247 248 $this->scope = $this->scope->parent; 249 $this->popEnv(); 250 } 251 252 protected function mediaParent($scope) { 253 while (!empty($scope->parent)) { 254 if (!empty($scope->type) && $scope->type != "media") { 255 break; 256 } 257 $scope = $scope->parent; 258 } 259 260 return $scope; 261 } 262 263 protected function compileNestedBlock($block, $selectors) { 264 $this->pushEnv($block); 265 $this->scope = $this->makeOutputBlock($block->type, $selectors); 266 $this->scope->parent->children[] = $this->scope; 267 268 $this->compileProps($block, $this->scope); 269 270 $this->scope = $this->scope->parent; 271 $this->popEnv(); 272 } 273 274 protected function compileRoot($root) { 275 $this->pushEnv(); 276 $this->scope = $this->makeOutputBlock($root->type); 277 $this->compileProps($root, $this->scope); 278 $this->popEnv(); 279 } 280 281 protected function compileProps($block, $out) { 282 foreach ($this->sortProps($block->props) as $prop) { 283 $this->compileProp($prop, $block, $out); 284 } 285 $out->lines = $this->deduplicate($out->lines); 286 } 287 288 /** 289 * Deduplicate lines in a block. Comments are not deduplicated. If a 290 * duplicate rule is detected, the comments immediately preceding each 291 * occurence are consolidated. 292 */ 293 protected function deduplicate($lines) { 294 $unique = array(); 295 $comments = array(); 296 297 foreach ($lines as $line) { 298 if (strpos($line, '/*') === 0) { 299 $comments[] = $line; 300 continue; 301 } 302 if (!in_array($line, $unique)) { 303 $unique[] = $line; 304 } 305 array_splice($unique, array_search($line, $unique), 0, $comments); 306 $comments = array(); 307 } 308 return array_merge($unique, $comments); 309 } 310 311 protected function sortProps($props, $split = false) { 312 $vars = array(); 313 $imports = array(); 314 $other = array(); 315 $stack = array(); 316 317 foreach ($props as $prop) { 318 switch ($prop[0]) { 319 case "comment": 320 $stack[] = $prop; 321 break; 322 case "assign": 323 $stack[] = $prop; 324 if (isset($prop[1][0]) && $prop[1][0] == $this->vPrefix) { 325 $vars = array_merge($vars, $stack); 326 } else { 327 $other = array_merge($other, $stack); 328 } 329 $stack = array(); 330 break; 331 case "import": 332 $id = self::$nextImportId++; 333 $prop[] = $id; 334 $stack[] = $prop; 335 $imports = array_merge($imports, $stack); 336 $other[] = array("import_mixin", $id); 337 $stack = array(); 338 break; 339 default: 340 $stack[] = $prop; 341 $other = array_merge($other, $stack); 342 $stack = array(); 343 break; 344 } 345 } 346 $other = array_merge($other, $stack); 347 348 if ($split) { 349 return array(array_merge($imports, $vars), $other); 350 } else { 351 return array_merge($imports, $vars, $other); 352 } 353 } 354 355 protected function compileMediaQuery($queries) { 356 $compiledQueries = array(); 357 foreach ($queries as $query) { 358 $parts = array(); 359 foreach ($query as $q) { 360 switch ($q[0]) { 361 case "mediaType": 362 $parts[] = implode(" ", array_slice($q, 1)); 363 break; 364 case "mediaExp": 365 if (isset($q[2])) { 366 $parts[] = "($q[1]: " . 367 $this->compileValue($this->reduce($q[2])) . ")"; 368 } else { 369 $parts[] = "($q[1])"; 370 } 371 break; 372 case "variable": 373 $parts[] = $this->compileValue($this->reduce($q)); 374 break; 375 } 376 } 377 378 if (count($parts) > 0) { 379 $compiledQueries[] = implode(" and ", $parts); 380 } 381 } 382 383 $out = "@media"; 384 if (!empty($parts)) { 385 $out .= " " . 386 implode($this->formatter->selectorSeparator, $compiledQueries); 387 } 388 return $out; 389 } 390 391 protected function multiplyMedia($env, $childQueries = null) { 392 if (is_null($env) || 393 !empty($env->block->type) && $env->block->type != "media" 394 ) { 395 return $childQueries; 396 } 397 398 // plain old block, skip 399 if (empty($env->block->type)) { 400 return $this->multiplyMedia($env->parent, $childQueries); 401 } 402 403 $out = array(); 404 $queries = $env->block->queries; 405 if (is_null($childQueries)) { 406 $out = $queries; 407 } else { 408 foreach ($queries as $parent) { 409 foreach ($childQueries as $child) { 410 $out[] = array_merge($parent, $child); 411 } 412 } 413 } 414 415 return $this->multiplyMedia($env->parent, $out); 416 } 417 418 protected function expandParentSelectors(&$tag, $replace) { 419 $parts = explode("$&$", $tag); 420 $count = 0; 421 foreach ($parts as &$part) { 422 $part = str_replace($this->parentSelector, $replace, $part, $c); 423 $count += $c; 424 } 425 $tag = implode($this->parentSelector, $parts); 426 return $count; 427 } 428 429 protected function findClosestSelectors() { 430 $env = $this->env; 431 $selectors = null; 432 while ($env !== null) { 433 if (isset($env->selectors)) { 434 $selectors = $env->selectors; 435 break; 436 } 437 $env = $env->parent; 438 } 439 440 return $selectors; 441 } 442 443 444 // multiply $selectors against the nearest selectors in env 445 protected function multiplySelectors($selectors) { 446 // find parent selectors 447 448 $parentSelectors = $this->findClosestSelectors(); 449 if (is_null($parentSelectors)) { 450 // kill parent reference in top level selector 451 foreach ($selectors as &$s) { 452 $this->expandParentSelectors($s, ""); 453 } 454 455 return $selectors; 456 } 457 458 $out = array(); 459 foreach ($parentSelectors as $parent) { 460 foreach ($selectors as $child) { 461 $count = $this->expandParentSelectors($child, $parent); 462 463 // don't prepend the parent tag if & was used 464 if ($count > 0) { 465 $out[] = trim($child); 466 } else { 467 $out[] = trim($parent . ' ' . $child); 468 } 469 } 470 } 471 472 return $out; 473 } 474 475 // reduces selector expressions 476 protected function compileSelectors($selectors) { 477 $out = array(); 478 479 foreach ($selectors as $s) { 480 if (is_array($s)) { 481 list(, $value) = $s; 482 $out[] = trim($this->compileValue($this->reduce($value))); 483 } else { 484 $out[] = $s; 485 } 486 } 487 488 return $out; 489 } 490 491 protected function eq($left, $right) { 492 return $left == $right; 493 } 494 495 protected function patternMatch($block, $orderedArgs, $keywordArgs) { 496 // match the guards if it has them 497 // any one of the groups must have all its guards pass for a match 498 if (!empty($block->guards)) { 499 $groupPassed = false; 500 foreach ($block->guards as $guardGroup) { 501 foreach ($guardGroup as $guard) { 502 $this->pushEnv(); 503 $this->zipSetArgs($block->args, $orderedArgs, $keywordArgs); 504 505 $negate = false; 506 if ($guard[0] == "negate") { 507 $guard = $guard[1]; 508 $negate = true; 509 } 510 511 $passed = $this->reduce($guard) == self::$TRUE; 512 if ($negate) $passed = !$passed; 513 514 $this->popEnv(); 515 516 if ($passed) { 517 $groupPassed = true; 518 } else { 519 $groupPassed = false; 520 break; 521 } 522 } 523 524 if ($groupPassed) break; 525 } 526 527 if (!$groupPassed) { 528 return false; 529 } 530 } 531 532 if (empty($block->args)) { 533 return $block->isVararg || empty($orderedArgs) && empty($keywordArgs); 534 } 535 536 $remainingArgs = $block->args; 537 if ($keywordArgs) { 538 $remainingArgs = array(); 539 foreach ($block->args as $arg) { 540 if ($arg[0] == "arg" && isset($keywordArgs[$arg[1]])) { 541 continue; 542 } 543 544 $remainingArgs[] = $arg; 545 } 546 } 547 548 $i = -1; // no args 549 // try to match by arity or by argument literal 550 foreach ($remainingArgs as $i => $arg) { 551 switch ($arg[0]) { 552 case "lit": 553 if (empty($orderedArgs[$i]) || !$this->eq($arg[1], $orderedArgs[$i])) { 554 return false; 555 } 556 break; 557 case "arg": 558 // no arg and no default value 559 if (!isset($orderedArgs[$i]) && !isset($arg[2])) { 560 return false; 561 } 562 break; 563 case "rest": 564 $i--; // rest can be empty 565 break 2; 566 } 567 } 568 569 if ($block->isVararg) { 570 return true; // not having enough is handled above 571 } else { 572 $numMatched = $i + 1; 573 // greater than because default values always match 574 return $numMatched >= count($orderedArgs); 575 } 576 } 577 578 protected function patternMatchAll($blocks, $orderedArgs, $keywordArgs, $skip = array()) { 579 $matches = null; 580 foreach ($blocks as $block) { 581 // skip seen blocks that don't have arguments 582 if (isset($skip[$block->id]) && !isset($block->args)) { 583 continue; 584 } 585 586 if ($this->patternMatch($block, $orderedArgs, $keywordArgs)) { 587 $matches[] = $block; 588 } 589 } 590 591 return $matches; 592 } 593 594 // attempt to find blocks matched by path and args 595 protected function findBlocks($searchIn, $path, $orderedArgs, $keywordArgs, $seen = array()) { 596 if ($searchIn == null) return null; 597 if (isset($seen[$searchIn->id])) return null; 598 $seen[$searchIn->id] = true; 599 600 $name = $path[0]; 601 602 if (isset($searchIn->children[$name])) { 603 $blocks = $searchIn->children[$name]; 604 if (count($path) == 1) { 605 $matches = $this->patternMatchAll($blocks, $orderedArgs, $keywordArgs, $seen); 606 if (!empty($matches)) { 607 // This will return all blocks that match in the closest 608 // scope that has any matching block, like lessjs 609 return $matches; 610 } 611 } else { 612 $matches = array(); 613 foreach ($blocks as $subBlock) { 614 $subMatches = $this->findBlocks($subBlock, 615 array_slice($path, 1), $orderedArgs, $keywordArgs, $seen); 616 617 if (!is_null($subMatches)) { 618 foreach ($subMatches as $sm) { 619 $matches[] = $sm; 620 } 621 } 622 } 623 624 return count($matches) > 0 ? $matches : null; 625 } 626 } 627 if ($searchIn->parent === $searchIn) return null; 628 return $this->findBlocks($searchIn->parent, $path, $orderedArgs, $keywordArgs, $seen); 629 } 630 631 // sets all argument names in $args to either the default value 632 // or the one passed in through $values 633 protected function zipSetArgs($args, $orderedValues, $keywordValues) { 634 $assignedValues = array(); 635 636 $i = 0; 637 foreach ($args as $a) { 638 if ($a[0] == "arg") { 639 if (isset($keywordValues[$a[1]])) { 640 // has keyword arg 641 $value = $keywordValues[$a[1]]; 642 } elseif (isset($orderedValues[$i])) { 643 // has ordered arg 644 $value = $orderedValues[$i]; 645 $i++; 646 } elseif (isset($a[2])) { 647 // has default value 648 $value = $a[2]; 649 } else { 650 $this->throwError("Failed to assign arg " . $a[1]); 651 $value = null; // :( 652 } 653 654 $value = $this->reduce($value); 655 $this->set($a[1], $value); 656 $assignedValues[] = $value; 657 } else { 658 // a lit 659 $i++; 660 } 661 } 662 663 // check for a rest 664 $last = end($args); 665 if ($last[0] == "rest") { 666 $rest = array_slice($orderedValues, count($args) - 1); 667 $this->set($last[1], $this->reduce(array("list", " ", $rest))); 668 } 669 670 // wow is this the only true use of PHP's + operator for arrays? 671 $this->env->arguments = $assignedValues + $orderedValues; 672 } 673 674 // compile a prop and update $lines or $blocks appropriately 675 protected function compileProp($prop, $block, $out) { 676 // set error position context 677 $this->sourceLoc = isset($prop[-1]) ? $prop[-1] : -1; 678 679 switch ($prop[0]) { 680 case 'assign': 681 list(, $name, $value) = $prop; 682 if ($name[0] == $this->vPrefix) { 683 $this->set($name, $value); 684 } else { 685 $out->lines[] = $this->formatter->property($name, 686 $this->compileValue($this->reduce($value))); 687 } 688 break; 689 case 'block': 690 list(, $child) = $prop; 691 $this->compileBlock($child); 692 break; 693 case 'mixin': 694 list(, $path, $args, $suffix) = $prop; 695 696 $orderedArgs = array(); 697 $keywordArgs = array(); 698 foreach ((array)$args as $arg) { 699 $argval = null; 700 switch ($arg[0]) { 701 case "arg": 702 if (!isset($arg[2])) { 703 $orderedArgs[] = $this->reduce(array("variable", $arg[1])); 704 } else { 705 $keywordArgs[$arg[1]] = $this->reduce($arg[2]); 706 } 707 break; 708 709 case "lit": 710 $orderedArgs[] = $this->reduce($arg[1]); 711 break; 712 default: 713 $this->throwError("Unknown arg type: " . $arg[0]); 714 } 715 } 716 717 $mixins = $this->findBlocks($block, $path, $orderedArgs, $keywordArgs); 718 719 if ($mixins === null) { 720 $this->throwError("{$prop[1][0]} is undefined"); 721 } 722 723 foreach ($mixins as $mixin) { 724 if ($mixin === $block && !$orderedArgs) { 725 continue; 726 } 727 728 $haveScope = false; 729 if (isset($mixin->parent->scope)) { 730 $haveScope = true; 731 $mixinParentEnv = $this->pushEnv(); 732 $mixinParentEnv->storeParent = $mixin->parent->scope; 733 } 734 735 $haveArgs = false; 736 if (isset($mixin->args)) { 737 $haveArgs = true; 738 $this->pushEnv(); 739 $this->zipSetArgs($mixin->args, $orderedArgs, $keywordArgs); 740 } 741 742 $oldParent = $mixin->parent; 743 if ($mixin != $block) $mixin->parent = $block; 744 745 foreach ($this->sortProps($mixin->props) as $subProp) { 746 if ($suffix !== null && 747 $subProp[0] == "assign" && 748 is_string($subProp[1]) && 749 $subProp[1][0] != $this->vPrefix 750 ) { 751 $subProp[2] = array( 752 'list', ' ', 753 array($subProp[2], array('keyword', $suffix)) 754 ); 755 } 756 757 $this->compileProp($subProp, $mixin, $out); 758 } 759 760 $mixin->parent = $oldParent; 761 762 if ($haveArgs) $this->popEnv(); 763 if ($haveScope) $this->popEnv(); 764 } 765 766 break; 767 case 'raw': 768 $out->lines[] = $prop[1]; 769 break; 770 case "directive": 771 list(, $name, $value) = $prop; 772 $out->lines[] = "@$name " . $this->compileValue($this->reduce($value)).';'; 773 break; 774 case "comment": 775 $out->lines[] = $prop[1]; 776 break; 777 case "import": 778 list(, $importPath, $importId) = $prop; 779 $importPath = $this->reduce($importPath); 780 781 if (!isset($this->env->imports)) { 782 $this->env->imports = array(); 783 } 784 785 $result = $this->tryImport($importPath, $block, $out); 786 787 $this->env->imports[$importId] = $result === false ? 788 array(false, "@import " . $this->compileValue($importPath).";") : 789 $result; 790 791 break; 792 case "import_mixin": 793 list(,$importId) = $prop; 794 $import = $this->env->imports[$importId]; 795 if ($import[0] === false) { 796 if (isset($import[1])) { 797 $out->lines[] = $import[1]; 798 } 799 } else { 800 list(, $bottom, $parser, $importDir) = $import; 801 $this->compileImportedProps($bottom, $block, $out, $parser, $importDir); 802 } 803 804 break; 805 default: 806 $this->throwError("unknown op: {$prop[0]}\n"); 807 } 808 } 809 810 811 /** 812 * Compiles a primitive value into a CSS property value. 813 * 814 * Values in lessphp are typed by being wrapped in arrays, their format is 815 * typically: 816 * 817 * array(type, contents [, additional_contents]*) 818 * 819 * The input is expected to be reduced. This function will not work on 820 * things like expressions and variables. 821 */ 822 public function compileValue($value) { 823 switch ($value[0]) { 824 case 'list': 825 // [1] - delimiter 826 // [2] - array of values 827 return implode($value[1], array_map(array($this, 'compileValue'), $value[2])); 828 case 'raw_color': 829 if (!empty($this->formatter->compressColors)) { 830 return $this->compileValue($this->coerceColor($value)); 831 } 832 return $value[1]; 833 case 'keyword': 834 // [1] - the keyword 835 return $value[1]; 836 case 'number': 837 list(, $num, $unit) = $value; 838 // [1] - the number 839 // [2] - the unit 840 if ($this->numberPrecision !== null) { 841 $num = round($num, $this->numberPrecision); 842 } 843 return $num . $unit; 844 case 'string': 845 // [1] - contents of string (includes quotes) 846 list(, $delim, $content) = $value; 847 foreach ($content as &$part) { 848 if (is_array($part)) { 849 $part = $this->compileValue($part); 850 } 851 } 852 return $delim . implode($content) . $delim; 853 case 'color': 854 // [1] - red component (either number or a %) 855 // [2] - green component 856 // [3] - blue component 857 // [4] - optional alpha component 858 list(, $r, $g, $b) = $value; 859 $r = round($r); 860 $g = round($g); 861 $b = round($b); 862 863 if (count($value) == 5 && $value[4] != 1) { // rgba 864 return 'rgba('.$r.','.$g.','.$b.','.$value[4].')'; 865 } 866 867 $h = sprintf("#%02x%02x%02x", $r, $g, $b); 868 869 if (!empty($this->formatter->compressColors)) { 870 // Converting hex color to short notation (e.g. #003399 to #039) 871 if ($h[1] === $h[2] && $h[3] === $h[4] && $h[5] === $h[6]) { 872 $h = '#' . $h[1] . $h[3] . $h[5]; 873 } 874 } 875 876 return $h; 877 878 case 'function': 879 list(, $name, $args) = $value; 880 return $name.'('.$this->compileValue($args).')'; 881 default: // assumed to be unit 882 $this->throwError("unknown value type: $value[0]"); 883 } 884 } 885 886 protected function lib_pow($args) { 887 list($base, $exp) = $this->assertArgs($args, 2, "pow"); 888 return pow($this->assertNumber($base), $this->assertNumber($exp)); 889 } 890 891 protected function lib_pi() { 892 return pi(); 893 } 894 895 protected function lib_mod($args) { 896 list($a, $b) = $this->assertArgs($args, 2, "mod"); 897 return $this->assertNumber($a) % $this->assertNumber($b); 898 } 899 900 protected function lib_tan($num) { 901 return tan($this->assertNumber($num)); 902 } 903 904 protected function lib_sin($num) { 905 return sin($this->assertNumber($num)); 906 } 907 908 protected function lib_cos($num) { 909 return cos($this->assertNumber($num)); 910 } 911 912 protected function lib_atan($num) { 913 $num = atan($this->assertNumber($num)); 914 return array("number", $num, "rad"); 915 } 916 917 protected function lib_asin($num) { 918 $num = asin($this->assertNumber($num)); 919 return array("number", $num, "rad"); 920 } 921 922 protected function lib_acos($num) { 923 $num = acos($this->assertNumber($num)); 924 return array("number", $num, "rad"); 925 } 926 927 protected function lib_sqrt($num) { 928 return sqrt($this->assertNumber($num)); 929 } 930 931 protected function lib_extract($value) { 932 list($list, $idx) = $this->assertArgs($value, 2, "extract"); 933 $idx = $this->assertNumber($idx); 934 // 1 indexed 935 if ($list[0] == "list" && isset($list[2][$idx - 1])) { 936 return $list[2][$idx - 1]; 937 } 938 } 939 940 protected function lib_isnumber($value) { 941 return $this->toBool($value[0] == "number"); 942 } 943 944 protected function lib_isstring($value) { 945 return $this->toBool($value[0] == "string"); 946 } 947 948 protected function lib_iscolor($value) { 949 return $this->toBool($this->coerceColor($value)); 950 } 951 952 protected function lib_iskeyword($value) { 953 return $this->toBool($value[0] == "keyword"); 954 } 955 956 protected function lib_ispixel($value) { 957 return $this->toBool($value[0] == "number" && $value[2] == "px"); 958 } 959 960 protected function lib_ispercentage($value) { 961 return $this->toBool($value[0] == "number" && $value[2] == "%"); 962 } 963 964 protected function lib_isem($value) { 965 return $this->toBool($value[0] == "number" && $value[2] == "em"); 966 } 967 968 protected function lib_isrem($value) { 969 return $this->toBool($value[0] == "number" && $value[2] == "rem"); 970 } 971 972 protected function lib_rgbahex($color) { 973 $color = $this->coerceColor($color); 974 if (is_null($color)) { 975 $this->throwError("color expected for rgbahex"); 976 } 977 978 return sprintf("#%02x%02x%02x%02x", 979 isset($color[4]) ? $color[4] * 255 : 255, 980 $color[1], 981 $color[2], 982 $color[3] 983 ); 984 } 985 986 protected function lib_argb($color){ 987 return $this->lib_rgbahex($color); 988 } 989 990 /** 991 * Given an url, decide whether to output a regular link or the base64-encoded contents of the file 992 * 993 * @param array $value either an argument list (two strings) or a single string 994 * @return string formatted url(), either as a link or base64-encoded 995 */ 996 protected function lib_data_uri($value) { 997 $mime = ($value[0] === 'list') ? $value[2][0][2] : null; 998 $url = ($value[0] === 'list') ? $value[2][1][2][0] : $value[2][0]; 999 1000 $fullpath = $this->findImport($url); 1001 1002 if ($fullpath && ($fsize = filesize($fullpath)) !== false) { 1003 // IE8 can't handle data uris larger than 32KB 1004 if ($fsize/1024 < 32) { 1005 if (is_null($mime)) { 1006 if (class_exists('finfo')) { // php 5.3+ 1007 $finfo = new finfo(FILEINFO_MIME); 1008 $mime = explode('; ', $finfo->file($fullpath)); 1009 $mime = $mime[0]; 1010 } elseif (function_exists('mime_content_type')) { // PHP 5.2 1011 $mime = mime_content_type($fullpath); 1012 } 1013 } 1014 1015 if (!is_null($mime)) // fallback if the mime type is still unknown 1016 $url = sprintf('data:%s;base64,%s', $mime, base64_encode(file_get_contents($fullpath))); 1017 } 1018 } 1019 1020 return 'url("'.$url.'")'; 1021 } 1022 1023 // utility func to unquote a string 1024 protected function lib_e($arg) { 1025 switch ($arg[0]) { 1026 case "list": 1027 $items = $arg[2]; 1028 if (isset($items[0])) { 1029 return $this->lib_e($items[0]); 1030 } 1031 $this->throwError("unrecognised input"); 1032 case "string": 1033 $arg[1] = ""; 1034 return $arg; 1035 case "keyword": 1036 return $arg; 1037 default: 1038 return array("keyword", $this->compileValue($arg)); 1039 } 1040 } 1041 1042 protected function lib__sprintf($args) { 1043 if ($args[0] != "list") return $args; 1044 $values = $args[2]; 1045 $string = array_shift($values); 1046 $template = $this->compileValue($this->lib_e($string)); 1047 1048 $i = 0; 1049 if (preg_match_all('/%[dsa]/', $template, $m)) { 1050 foreach ($m[0] as $match) { 1051 $val = isset($values[$i]) ? 1052 $this->reduce($values[$i]) : array('keyword', ''); 1053 1054 // lessjs compat, renders fully expanded color, not raw color 1055 if ($color = $this->coerceColor($val)) { 1056 $val = $color; 1057 } 1058 1059 $i++; 1060 $rep = $this->compileValue($this->lib_e($val)); 1061 $template = preg_replace('/'.self::preg_quote($match).'/', 1062 $rep, $template, 1); 1063 } 1064 } 1065 1066 $d = $string[0] == "string" ? $string[1] : '"'; 1067 return array("string", $d, array($template)); 1068 } 1069 1070 protected function lib_floor($arg) { 1071 $value = $this->assertNumber($arg); 1072 return array("number", floor($value), $arg[2]); 1073 } 1074 1075 protected function lib_ceil($arg) { 1076 $value = $this->assertNumber($arg); 1077 return array("number", ceil($value), $arg[2]); 1078 } 1079 1080 protected function lib_round($arg) { 1081 if ($arg[0] != "list") { 1082 $value = $this->assertNumber($arg); 1083 return array("number", round($value), $arg[2]); 1084 } else { 1085 $value = $this->assertNumber($arg[2][0]); 1086 $precision = $this->assertNumber($arg[2][1]); 1087 return array("number", round($value, $precision), $arg[2][0][2]); 1088 } 1089 } 1090 1091 protected function lib_unit($arg) { 1092 if ($arg[0] == "list") { 1093 list($number, $newUnit) = $arg[2]; 1094 return array("number", $this->assertNumber($number), 1095 $this->compileValue($this->lib_e($newUnit))); 1096 } else { 1097 return array("number", $this->assertNumber($arg), ""); 1098 } 1099 } 1100 1101 /** 1102 * Helper function to get arguments for color manipulation functions. 1103 * takes a list that contains a color like thing and a percentage 1104 */ 1105 public function colorArgs($args) { 1106 if ($args[0] != 'list' || count($args[2]) < 2) { 1107 return array(array('color', 0, 0, 0), 0); 1108 } 1109 list($color, $delta) = $args[2]; 1110 $color = $this->assertColor($color); 1111 $delta = floatval($delta[1]); 1112 1113 return array($color, $delta); 1114 } 1115 1116 protected function lib_darken($args) { 1117 list($color, $delta) = $this->colorArgs($args); 1118 1119 $hsl = $this->toHSL($color); 1120 $hsl[3] = $this->clamp($hsl[3] - $delta, 100); 1121 return $this->toRGB($hsl); 1122 } 1123 1124 protected function lib_lighten($args) { 1125 list($color, $delta) = $this->colorArgs($args); 1126 1127 $hsl = $this->toHSL($color); 1128 $hsl[3] = $this->clamp($hsl[3] + $delta, 100); 1129 return $this->toRGB($hsl); 1130 } 1131 1132 protected function lib_saturate($args) { 1133 list($color, $delta) = $this->colorArgs($args); 1134 1135 $hsl = $this->toHSL($color); 1136 $hsl[2] = $this->clamp($hsl[2] + $delta, 100); 1137 return $this->toRGB($hsl); 1138 } 1139 1140 protected function lib_desaturate($args) { 1141 list($color, $delta) = $this->colorArgs($args); 1142 1143 $hsl = $this->toHSL($color); 1144 $hsl[2] = $this->clamp($hsl[2] - $delta, 100); 1145 return $this->toRGB($hsl); 1146 } 1147 1148 protected function lib_spin($args) { 1149 list($color, $delta) = $this->colorArgs($args); 1150 1151 $hsl = $this->toHSL($color); 1152 1153 $hsl[1] = $hsl[1] + $delta % 360; 1154 if ($hsl[1] < 0) { 1155 $hsl[1] += 360; 1156 } 1157 1158 return $this->toRGB($hsl); 1159 } 1160 1161 protected function lib_fadeout($args) { 1162 list($color, $delta) = $this->colorArgs($args); 1163 $color[4] = $this->clamp((isset($color[4]) ? $color[4] : 1) - $delta/100); 1164 return $color; 1165 } 1166 1167 protected function lib_fadein($args) { 1168 list($color, $delta) = $this->colorArgs($args); 1169 $color[4] = $this->clamp((isset($color[4]) ? $color[4] : 1) + $delta/100); 1170 return $color; 1171 } 1172 1173 protected function lib_hue($color) { 1174 $hsl = $this->toHSL($this->assertColor($color)); 1175 return round($hsl[1]); 1176 } 1177 1178 protected function lib_saturation($color) { 1179 $hsl = $this->toHSL($this->assertColor($color)); 1180 return round($hsl[2]); 1181 } 1182 1183 protected function lib_lightness($color) { 1184 $hsl = $this->toHSL($this->assertColor($color)); 1185 return round($hsl[3]); 1186 } 1187 1188 // get the alpha of a color 1189 // defaults to 1 for non-colors or colors without an alpha 1190 protected function lib_alpha($value) { 1191 if (!is_null($color = $this->coerceColor($value))) { 1192 return isset($color[4]) ? $color[4] : 1; 1193 } 1194 } 1195 1196 // set the alpha of the color 1197 protected function lib_fade($args) { 1198 list($color, $alpha) = $this->colorArgs($args); 1199 $color[4] = $this->clamp($alpha / 100.0); 1200 return $color; 1201 } 1202 1203 protected function lib_percentage($arg) { 1204 $num = $this->assertNumber($arg); 1205 return array("number", $num*100, "%"); 1206 } 1207 1208 /** 1209 * Mix color with white in variable proportion. 1210 * 1211 * It is the same as calling `mix(#ffffff, @color, @weight)`. 1212 * 1213 * tint(@color, [@weight: 50%]); 1214 * 1215 * http://lesscss.org/functions/#color-operations-tint 1216 * 1217 * @return array Color 1218 */ 1219 protected function lib_tint($args) { 1220 $white = ['color', 255, 255, 255]; 1221 if ($args[0] == 'color') { 1222 return $this->lib_mix([ 'list', ',', [$white, $args] ]); 1223 } elseif ($args[0] == "list" && count($args[2]) == 2) { 1224 return $this->lib_mix([ $args[0], $args[1], [$white, $args[2][0], $args[2][1]] ]); 1225 } else { 1226 $this->throwError("tint expects (color, weight)"); 1227 } 1228 } 1229 1230 /** 1231 * Mix color with black in variable proportion. 1232 * 1233 * It is the same as calling `mix(#000000, @color, @weight)` 1234 * 1235 * shade(@color, [@weight: 50%]); 1236 * 1237 * http://lesscss.org/functions/#color-operations-shade 1238 * 1239 * @return array Color 1240 */ 1241 protected function lib_shade($args) { 1242 $black = ['color', 0, 0, 0]; 1243 if ($args[0] == 'color') { 1244 return $this->lib_mix([ 'list', ',', [$black, $args] ]); 1245 } elseif ($args[0] == "list" && count($args[2]) == 2) { 1246 return $this->lib_mix([ $args[0], $args[1], [$black, $args[2][0], $args[2][1]] ]); 1247 } else { 1248 $this->throwError("shade expects (color, weight)"); 1249 } 1250 } 1251 1252 // mixes two colors by weight 1253 // mix(@color1, @color2, [@weight: 50%]); 1254 // http://sass-lang.com/docs/yardoc/Sass/Script/Functions.html#mix-instance_method 1255 protected function lib_mix($args) { 1256 if ($args[0] != "list" || count($args[2]) < 2) 1257 $this->throwError("mix expects (color1, color2, weight)"); 1258 1259 list($first, $second) = $args[2]; 1260 $first = $this->assertColor($first); 1261 $second = $this->assertColor($second); 1262 1263 $first_a = $this->lib_alpha($first); 1264 $second_a = $this->lib_alpha($second); 1265 1266 if (isset($args[2][2])) { 1267 $weight = $args[2][2][1] / 100.0; 1268 } else { 1269 $weight = 0.5; 1270 } 1271 1272 $w = $weight * 2 - 1; 1273 $a = $first_a - $second_a; 1274 1275 $w1 = (($w * $a == -1 ? $w : ($w + $a)/(1 + $w * $a)) + 1) / 2.0; 1276 $w2 = 1.0 - $w1; 1277 1278 $new = array('color', 1279 $w1 * $first[1] + $w2 * $second[1], 1280 $w1 * $first[2] + $w2 * $second[2], 1281 $w1 * $first[3] + $w2 * $second[3], 1282 ); 1283 1284 if ($first_a != 1.0 || $second_a != 1.0) { 1285 $new[] = $first_a * $weight + $second_a * ($weight - 1); 1286 } 1287 1288 return $this->fixColor($new); 1289 } 1290 1291 protected function lib_contrast($args) { 1292 $darkColor = array('color', 0, 0, 0); 1293 $lightColor = array('color', 255, 255, 255); 1294 $threshold = 0.43; 1295 1296 if ( $args[0] == 'list' ) { 1297 $inputColor = ( isset($args[2][0]) ) ? $this->assertColor($args[2][0]) : $lightColor; 1298 $darkColor = ( isset($args[2][1]) ) ? $this->assertColor($args[2][1]) : $darkColor; 1299 $lightColor = ( isset($args[2][2]) ) ? $this->assertColor($args[2][2]) : $lightColor; 1300 $threshold = ( isset($args[2][3]) ) ? $this->assertNumber($args[2][3]) : $threshold; 1301 } 1302 else { 1303 $inputColor = $this->assertColor($args); 1304 } 1305 1306 $inputColor = $this->coerceColor($inputColor); 1307 $darkColor = $this->coerceColor($darkColor); 1308 $lightColor = $this->coerceColor($lightColor); 1309 1310 //Figure out which is actually light and dark! 1311 if ( $this->toLuma($darkColor) > $this->toLuma($lightColor) ) { 1312 $t = $lightColor; 1313 $lightColor = $darkColor; 1314 $darkColor = $t; 1315 } 1316 1317 $inputColor_alpha = $this->lib_alpha($inputColor); 1318 if ( ( $this->toLuma($inputColor) * $inputColor_alpha) < $threshold) { 1319 return $lightColor; 1320 } 1321 return $darkColor; 1322 } 1323 1324 private function toLuma($color) { 1325 list(, $r, $g, $b) = $this->coerceColor($color); 1326 1327 $r = $r / 255; 1328 $g = $g / 255; 1329 $b = $b / 255; 1330 1331 $r = ($r <= 0.03928) ? $r / 12.92 : pow((($r + 0.055) / 1.055), 2.4); 1332 $g = ($g <= 0.03928) ? $g / 12.92 : pow((($g + 0.055) / 1.055), 2.4); 1333 $b = ($b <= 0.03928) ? $b / 12.92 : pow((($b + 0.055) / 1.055), 2.4); 1334 1335 return (0.2126 * $r) + (0.7152 * $g) + (0.0722 * $b); 1336 } 1337 1338 protected function lib_luma($color) { 1339 return array("number", round($this->toLuma($color) * 100, 8), "%"); 1340 } 1341 1342 1343 public function assertColor($value, $error = "expected color value") { 1344 $color = $this->coerceColor($value); 1345 if (is_null($color)) $this->throwError($error); 1346 return $color; 1347 } 1348 1349 public function assertNumber($value, $error = "expecting number") { 1350 if ($value[0] == "number") return $value[1]; 1351 $this->throwError($error); 1352 } 1353 1354 public function assertArgs($value, $expectedArgs, $name = "") { 1355 if ($expectedArgs == 1) { 1356 return $value; 1357 } else { 1358 if ($value[0] !== "list" || $value[1] != ",") $this->throwError("expecting list"); 1359 $values = $value[2]; 1360 $numValues = count($values); 1361 if ($expectedArgs != $numValues) { 1362 if ($name) { 1363 $name = $name . ": "; 1364 } 1365 1366 $this->throwError("${name}expecting $expectedArgs arguments, got $numValues"); 1367 } 1368 1369 return $values; 1370 } 1371 } 1372 1373 protected function toHSL($color) { 1374 if ($color[0] === 'hsl') { 1375 return $color; 1376 } 1377 1378 $r = $color[1] / 255; 1379 $g = $color[2] / 255; 1380 $b = $color[3] / 255; 1381 1382 $min = min($r, $g, $b); 1383 $max = max($r, $g, $b); 1384 1385 $L = ($min + $max) / 2; 1386 if ($min == $max) { 1387 $S = $H = 0; 1388 } else { 1389 if ($L < 0.5) { 1390 $S = ($max - $min) / ($max + $min); 1391 } else { 1392 $S = ($max - $min) / (2.0 - $max - $min); 1393 } 1394 if ($r == $max) { 1395 $H = ($g - $b) / ($max - $min); 1396 } elseif ($g == $max) { 1397 $H = 2.0 + ($b - $r) / ($max - $min); 1398 } elseif ($b == $max) { 1399 $H = 4.0 + ($r - $g) / ($max - $min); 1400 } 1401 1402 } 1403 1404 $out = array('hsl', 1405 ($H < 0 ? $H + 6 : $H)*60, 1406 $S * 100, 1407 $L * 100, 1408 ); 1409 1410 if (count($color) > 4) { 1411 // copy alpha 1412 $out[] = $color[4]; 1413 } 1414 return $out; 1415 } 1416 1417 protected function toRGB_helper($comp, $temp1, $temp2) { 1418 if ($comp < 0) { 1419 $comp += 1.0; 1420 } elseif ($comp > 1) { 1421 $comp -= 1.0; 1422 } 1423 1424 if (6 * $comp < 1) { 1425 return $temp1 + ($temp2 - $temp1) * 6 * $comp; 1426 } 1427 if (2 * $comp < 1) { 1428 return $temp2; 1429 } 1430 if (3 * $comp < 2) { 1431 return $temp1 + ($temp2 - $temp1)*((2/3) - $comp) * 6; 1432 } 1433 1434 return $temp1; 1435 } 1436 1437 /** 1438 * Converts a hsl array into a color value in rgb. 1439 * Expects H to be in range of 0 to 360, S and L in 0 to 100 1440 */ 1441 protected function toRGB($color) { 1442 if ($color[0] === 'color') { 1443 return $color; 1444 } 1445 1446 $H = $color[1] / 360; 1447 $S = $color[2] / 100; 1448 $L = $color[3] / 100; 1449 1450 if ($S == 0) { 1451 $r = $g = $b = $L; 1452 } else { 1453 $temp2 = $L < 0.5 ? 1454 $L * (1.0 + $S) : 1455 $L + $S - $L * $S; 1456 1457 $temp1 = 2.0 * $L - $temp2; 1458 1459 $r = $this->toRGB_helper($H + 1/3, $temp1, $temp2); 1460 $g = $this->toRGB_helper($H, $temp1, $temp2); 1461 $b = $this->toRGB_helper($H - 1/3, $temp1, $temp2); 1462 } 1463 1464 // $out = array('color', round($r*255), round($g*255), round($b*255)); 1465 $out = array('color', $r*255, $g*255, $b*255); 1466 if (count($color) > 4) { 1467 // copy alpha 1468 $out[] = $color[4]; 1469 } 1470 return $out; 1471 } 1472 1473 protected function clamp($v, $max = 1, $min = 0) { 1474 return min($max, max($min, $v)); 1475 } 1476 1477 /** 1478 * Convert the rgb, rgba, hsl color literals of function type 1479 * as returned by the parser into values of color type. 1480 */ 1481 protected function funcToColor($func) { 1482 $fname = $func[1]; 1483 if ($func[2][0] != 'list') { 1484 // need a list of arguments 1485 return false; 1486 } 1487 $rawComponents = $func[2][2]; 1488 1489 if ($fname == 'hsl' || $fname == 'hsla') { 1490 $hsl = array('hsl'); 1491 $i = 0; 1492 foreach ($rawComponents as $c) { 1493 $val = $this->reduce($c); 1494 $val = isset($val[1]) ? floatval($val[1]) : 0; 1495 1496 if ($i == 0) { 1497 $clamp = 360; 1498 } elseif ($i < 3) { 1499 $clamp = 100; 1500 } else { 1501 $clamp = 1; 1502 } 1503 1504 $hsl[] = $this->clamp($val, $clamp); 1505 $i++; 1506 } 1507 1508 while (count($hsl) < 4) { 1509 $hsl[] = 0; 1510 } 1511 return $this->toRGB($hsl); 1512 1513 } elseif ($fname == 'rgb' || $fname == 'rgba') { 1514 $components = array(); 1515 $i = 1; 1516 foreach ($rawComponents as $c) { 1517 $c = $this->reduce($c); 1518 if ($i < 4) { 1519 if ($c[0] == "number" && $c[2] == "%") { 1520 $components[] = 255 * ($c[1] / 100); 1521 } else { 1522 $components[] = floatval($c[1]); 1523 } 1524 } elseif ($i == 4) { 1525 if ($c[0] == "number" && $c[2] == "%") { 1526 $components[] = 1.0 * ($c[1] / 100); 1527 } else { 1528 $components[] = floatval($c[1]); 1529 } 1530 } else break; 1531 1532 $i++; 1533 } 1534 while (count($components) < 3) { 1535 $components[] = 0; 1536 } 1537 array_unshift($components, 'color'); 1538 return $this->fixColor($components); 1539 } 1540 1541 return false; 1542 } 1543 1544 protected function reduce($value, $forExpression = false) { 1545 switch ($value[0]) { 1546 case "interpolate": 1547 $reduced = $this->reduce($value[1]); 1548 $var = $this->compileValue($reduced); 1549 $res = $this->reduce(array("variable", $this->vPrefix . $var)); 1550 1551 if ($res[0] == "raw_color") { 1552 $res = $this->coerceColor($res); 1553 } 1554 1555 if (empty($value[2])) $res = $this->lib_e($res); 1556 1557 return $res; 1558 case "variable": 1559 $key = $value[1]; 1560 if (is_array($key)) { 1561 $key = $this->reduce($key); 1562 $key = $this->vPrefix . $this->compileValue($this->lib_e($key)); 1563 } 1564 1565 $seen =& $this->env->seenNames; 1566 1567 if (!empty($seen[$key])) { 1568 $this->throwError("infinite loop detected: $key"); 1569 } 1570 1571 $seen[$key] = true; 1572 $out = $this->reduce($this->get($key)); 1573 $seen[$key] = false; 1574 return $out; 1575 case "list": 1576 foreach ($value[2] as &$item) { 1577 $item = $this->reduce($item, $forExpression); 1578 } 1579 return $value; 1580 case "expression": 1581 return $this->evaluate($value); 1582 case "string": 1583 foreach ($value[2] as &$part) { 1584 if (is_array($part)) { 1585 $strip = $part[0] == "variable"; 1586 $part = $this->reduce($part); 1587 if ($strip) $part = $this->lib_e($part); 1588 } 1589 } 1590 return $value; 1591 case "escape": 1592 list(,$inner) = $value; 1593 return $this->lib_e($this->reduce($inner)); 1594 case "function": 1595 $color = $this->funcToColor($value); 1596 if ($color) return $color; 1597 1598 list(, $name, $args) = $value; 1599 if ($name == "%") $name = "_sprintf"; 1600 1601 $f = isset($this->libFunctions[$name]) ? 1602 $this->libFunctions[$name] : array($this, 'lib_'.str_replace('-', '_', $name)); 1603 1604 if (is_callable($f)) { 1605 if ($args[0] == 'list') 1606 $args = self::compressList($args[2], $args[1]); 1607 1608 $ret = call_user_func($f, $this->reduce($args, true), $this); 1609 1610 if (is_null($ret)) { 1611 return array("string", "", array( 1612 $name, "(", $args, ")" 1613 )); 1614 } 1615 1616 // convert to a typed value if the result is a php primitive 1617 if (is_numeric($ret)) { 1618 $ret = array('number', $ret, ""); 1619 } elseif (!is_array($ret)) { 1620 $ret = array('keyword', $ret); 1621 } 1622 1623 return $ret; 1624 } 1625 1626 // plain function, reduce args 1627 $value[2] = $this->reduce($value[2]); 1628 return $value; 1629 case "unary": 1630 list(, $op, $exp) = $value; 1631 $exp = $this->reduce($exp); 1632 1633 if ($exp[0] == "number") { 1634 switch ($op) { 1635 case "+": 1636 return $exp; 1637 case "-": 1638 $exp[1] *= -1; 1639 return $exp; 1640 } 1641 } 1642 return array("string", "", array($op, $exp)); 1643 } 1644 1645 if ($forExpression) { 1646 switch ($value[0]) { 1647 case "keyword": 1648 if ($color = $this->coerceColor($value)) { 1649 return $color; 1650 } 1651 break; 1652 case "raw_color": 1653 return $this->coerceColor($value); 1654 } 1655 } 1656 1657 return $value; 1658 } 1659 1660 1661 // coerce a value for use in color operation 1662 protected function coerceColor($value) { 1663 switch ($value[0]) { 1664 case 'color': return $value; 1665 case 'raw_color': 1666 $c = array("color", 0, 0, 0); 1667 $colorStr = substr($value[1], 1); 1668 $num = hexdec($colorStr); 1669 $width = strlen($colorStr) == 3 ? 16 : 256; 1670 1671 for ($i = 3; $i > 0; $i--) { // 3 2 1 1672 $t = $num % $width; 1673 $num /= $width; 1674 1675 $c[$i] = $t * (256/$width) + $t * floor(16/$width); 1676 } 1677 1678 return $c; 1679 case 'keyword': 1680 $name = $value[1]; 1681 if (isset(self::$cssColors[$name])) { 1682 $rgba = explode(',', self::$cssColors[$name]); 1683 1684 if (isset($rgba[3])) { 1685 return array('color', $rgba[0], $rgba[1], $rgba[2], $rgba[3]); 1686 } 1687 return array('color', $rgba[0], $rgba[1], $rgba[2]); 1688 } 1689 return null; 1690 } 1691 } 1692 1693 // make something string like into a string 1694 protected function coerceString($value) { 1695 switch ($value[0]) { 1696 case "string": 1697 return $value; 1698 case "keyword": 1699 return array("string", "", array($value[1])); 1700 } 1701 return null; 1702 } 1703 1704 // turn list of length 1 into value type 1705 protected function flattenList($value) { 1706 if ($value[0] == "list" && count($value[2]) == 1) { 1707 return $this->flattenList($value[2][0]); 1708 } 1709 return $value; 1710 } 1711 1712 public function toBool($a) { 1713 return $a ? self::$TRUE : self::$FALSE; 1714 } 1715 1716 // evaluate an expression 1717 protected function evaluate($exp) { 1718 list(, $op, $left, $right, $whiteBefore, $whiteAfter) = $exp; 1719 1720 $left = $this->reduce($left, true); 1721 $right = $this->reduce($right, true); 1722 1723 if ($leftColor = $this->coerceColor($left)) { 1724 $left = $leftColor; 1725 } 1726 1727 if ($rightColor = $this->coerceColor($right)) { 1728 $right = $rightColor; 1729 } 1730 1731 $ltype = $left[0]; 1732 $rtype = $right[0]; 1733 1734 // operators that work on all types 1735 if ($op == "and") { 1736 return $this->toBool($left == self::$TRUE && $right == self::$TRUE); 1737 } 1738 1739 if ($op == "=") { 1740 return $this->toBool($this->eq($left, $right) ); 1741 } 1742 1743 if ($op == "+" && !is_null($str = $this->stringConcatenate($left, $right))) { 1744 return $str; 1745 } 1746 1747 // type based operators 1748 $fname = "op_${ltype}_${rtype}"; 1749 if (is_callable(array($this, $fname))) { 1750 $out = $this->$fname($op, $left, $right); 1751 if (!is_null($out)) return $out; 1752 } 1753 1754 // make the expression look it did before being parsed 1755 $paddedOp = $op; 1756 if ($whiteBefore) { 1757 $paddedOp = " " . $paddedOp; 1758 } 1759 if ($whiteAfter) { 1760 $paddedOp .= " "; 1761 } 1762 1763 return array("string", "", array($left, $paddedOp, $right)); 1764 } 1765 1766 protected function stringConcatenate($left, $right) { 1767 if ($strLeft = $this->coerceString($left)) { 1768 if ($right[0] == "string") { 1769 $right[1] = ""; 1770 } 1771 $strLeft[2][] = $right; 1772 return $strLeft; 1773 } 1774 1775 if ($strRight = $this->coerceString($right)) { 1776 array_unshift($strRight[2], $left); 1777 return $strRight; 1778 } 1779 } 1780 1781 1782 // make sure a color's components don't go out of bounds 1783 protected function fixColor($c) { 1784 foreach (range(1, 3) as $i) { 1785 if ($c[$i] < 0) $c[$i] = 0; 1786 if ($c[$i] > 255) $c[$i] = 255; 1787 } 1788 1789 return $c; 1790 } 1791 1792 protected function op_number_color($op, $lft, $rgt) { 1793 if ($op == '+' || $op == '*') { 1794 return $this->op_color_number($op, $rgt, $lft); 1795 } 1796 } 1797 1798 protected function op_color_number($op, $lft, $rgt) { 1799 if ($rgt[0] == '%') $rgt[1] /= 100; 1800 1801 return $this->op_color_color($op, $lft, 1802 array_fill(1, count($lft) - 1, $rgt[1])); 1803 } 1804 1805 protected function op_color_color($op, $left, $right) { 1806 $out = array('color'); 1807 $max = count($left) > count($right) ? count($left) : count($right); 1808 foreach (range(1, $max - 1) as $i) { 1809 $lval = isset($left[$i]) ? $left[$i] : 0; 1810 $rval = isset($right[$i]) ? $right[$i] : 0; 1811 switch ($op) { 1812 case '+': 1813 $out[] = $lval + $rval; 1814 break; 1815 case '-': 1816 $out[] = $lval - $rval; 1817 break; 1818 case '*': 1819 $out[] = $lval * $rval; 1820 break; 1821 case '%': 1822 $out[] = $lval % $rval; 1823 break; 1824 case '/': 1825 if ($rval == 0) { 1826 $this->throwError("evaluate error: can't divide by zero"); 1827 } 1828 $out[] = $lval / $rval; 1829 break; 1830 default: 1831 $this->throwError('evaluate error: color op number failed on op '.$op); 1832 } 1833 } 1834 return $this->fixColor($out); 1835 } 1836 1837 public function lib_red($color){ 1838 $color = $this->coerceColor($color); 1839 if (is_null($color)) { 1840 $this->throwError('color expected for red()'); 1841 } 1842 1843 return $color[1]; 1844 } 1845 1846 public function lib_green($color){ 1847 $color = $this->coerceColor($color); 1848 if (is_null($color)) { 1849 $this->throwError('color expected for green()'); 1850 } 1851 1852 return $color[2]; 1853 } 1854 1855 public function lib_blue($color){ 1856 $color = $this->coerceColor($color); 1857 if (is_null($color)) { 1858 $this->throwError('color expected for blue()'); 1859 } 1860 1861 return $color[3]; 1862 } 1863 1864 1865 // operator on two numbers 1866 protected function op_number_number($op, $left, $right) { 1867 $unit = empty($left[2]) ? $right[2] : $left[2]; 1868 1869 $value = 0; 1870 switch ($op) { 1871 case '+': 1872 $value = $left[1] + $right[1]; 1873 break; 1874 case '*': 1875 $value = $left[1] * $right[1]; 1876 break; 1877 case '-': 1878 $value = $left[1] - $right[1]; 1879 break; 1880 case '%': 1881 $value = $left[1] % $right[1]; 1882 break; 1883 case '/': 1884 if ($right[1] == 0) $this->throwError('parse error: divide by zero'); 1885 $value = $left[1] / $right[1]; 1886 break; 1887 case '<': 1888 return $this->toBool($left[1] < $right[1]); 1889 case '>': 1890 return $this->toBool($left[1] > $right[1]); 1891 case '>=': 1892 return $this->toBool($left[1] >= $right[1]); 1893 case '=<': 1894 return $this->toBool($left[1] <= $right[1]); 1895 default: 1896 $this->throwError('parse error: unknown number operator: '.$op); 1897 } 1898 1899 return array("number", $value, $unit); 1900 } 1901 1902 1903 /* environment functions */ 1904 1905 protected function makeOutputBlock($type, $selectors = null) { 1906 $b = new stdclass; 1907 $b->lines = array(); 1908 $b->children = array(); 1909 $b->selectors = $selectors; 1910 $b->type = $type; 1911 $b->parent = $this->scope; 1912 return $b; 1913 } 1914 1915 // the state of execution 1916 protected function pushEnv($block = null) { 1917 $e = new stdclass; 1918 $e->parent = $this->env; 1919 $e->store = array(); 1920 $e->block = $block; 1921 1922 $this->env = $e; 1923 return $e; 1924 } 1925 1926 // pop something off the stack 1927 protected function popEnv() { 1928 $old = $this->env; 1929 $this->env = $this->env->parent; 1930 return $old; 1931 } 1932 1933 // set something in the current env 1934 protected function set($name, $value) { 1935 $this->env->store[$name] = $value; 1936 } 1937 1938 1939 // get the highest occurrence entry for a name 1940 protected function get($name) { 1941 $current = $this->env; 1942 1943 $isArguments = $name == $this->vPrefix . 'arguments'; 1944 while ($current) { 1945 if ($isArguments && isset($current->arguments)) { 1946 return array('list', ' ', $current->arguments); 1947 } 1948 1949 if (isset($current->store[$name])) { 1950 return $current->store[$name]; 1951 } 1952 1953 $current = isset($current->storeParent) ? 1954 $current->storeParent : 1955 $current->parent; 1956 } 1957 1958 $this->throwError("variable $name is undefined"); 1959 } 1960 1961 // inject array of unparsed strings into environment as variables 1962 protected function injectVariables($args) { 1963 $this->pushEnv(); 1964 $parser = new lessc_parser($this, __METHOD__); 1965 foreach ($args as $name => $strValue) { 1966 if ($name[0] !== '@') { 1967 $name = '@' . $name; 1968 } 1969 $parser->count = 0; 1970 $parser->buffer = (string)$strValue; 1971 if (!$parser->propertyValue($value)) { 1972 throw new Exception("failed to parse passed in variable $name: $strValue"); 1973 } 1974 1975 $this->set($name, $value); 1976 } 1977 } 1978 1979 /** 1980 * Initialize any static state, can initialize parser for a file 1981 * $opts isn't used yet 1982 */ 1983 public function __construct($fname = null) { 1984 if ($fname !== null) { 1985 // used for deprecated parse method 1986 $this->_parseFile = $fname; 1987 } 1988 } 1989 1990 public function compile($string, $name = null) { 1991 $locale = setlocale(LC_NUMERIC, 0); 1992 setlocale(LC_NUMERIC, "C"); 1993 1994 $this->parser = $this->makeParser($name); 1995 $root = $this->parser->parse($string); 1996 1997 $this->env = null; 1998 $this->scope = null; 1999 2000 $this->formatter = $this->newFormatter(); 2001 2002 if (!empty($this->registeredVars)) { 2003 $this->injectVariables($this->registeredVars); 2004 } 2005 2006 $this->sourceParser = $this->parser; // used for error messages 2007 $this->compileBlock($root); 2008 2009 ob_start(); 2010 $this->formatter->block($this->scope); 2011 $out = ob_get_clean(); 2012 setlocale(LC_NUMERIC, $locale); 2013 return $out; 2014 } 2015 2016 public function compileFile($fname, $outFname = null) { 2017 if (!is_readable($fname)) { 2018 throw new Exception('load error: failed to find '.$fname); 2019 } 2020 2021 $pi = pathinfo($fname); 2022 2023 $oldImport = $this->importDir; 2024 2025 $this->importDir = (array)$this->importDir; 2026 $this->importDir[] = $pi['dirname'].'/'; 2027 2028 $this->addParsedFile($fname); 2029 2030 $out = $this->compile(file_get_contents($fname), $fname); 2031 2032 $this->importDir = $oldImport; 2033 2034 if ($outFname !== null) { 2035 return file_put_contents($outFname, $out); 2036 } 2037 2038 return $out; 2039 } 2040 2041 // compile only if changed input has changed or output doesn't exist 2042 public function checkedCompile($in, $out) { 2043 if (!is_file($out) || filemtime($in) > filemtime($out)) { 2044 $this->compileFile($in, $out); 2045 return true; 2046 } 2047 return false; 2048 } 2049 2050 /** 2051 * Execute lessphp on a .less file or a lessphp cache structure 2052 * 2053 * The lessphp cache structure contains information about a specific 2054 * less file having been parsed. It can be used as a hint for future 2055 * calls to determine whether or not a rebuild is required. 2056 * 2057 * The cache structure contains two important keys that may be used 2058 * externally: 2059 * 2060 * compiled: The final compiled CSS 2061 * updated: The time (in seconds) the CSS was last compiled 2062 * 2063 * The cache structure is a plain-ol' PHP associative array and can 2064 * be serialized and unserialized without a hitch. 2065 * 2066 * @param mixed $in Input 2067 * @param bool $force Force rebuild? 2068 * @return array lessphp cache structure 2069 */ 2070 public function cachedCompile($in, $force = false) { 2071 // assume no root 2072 $root = null; 2073 2074 if (is_string($in)) { 2075 $root = $in; 2076 } elseif (is_array($in) && isset($in['root'])) { 2077 if ($force || !isset($in['files'])) { 2078 // If we are forcing a recompile or if for some reason the 2079 // structure does not contain any file information we should 2080 // specify the root to trigger a rebuild. 2081 $root = $in['root']; 2082 } elseif (isset($in['files']) && is_array($in['files'])) { 2083 foreach ($in['files'] as $fname => $ftime) { 2084 if (!file_exists($fname) || filemtime($fname) > $ftime) { 2085 // One of the files we knew about previously has changed 2086 // so we should look at our incoming root again. 2087 $root = $in['root']; 2088 break; 2089 } 2090 } 2091 } 2092 } else { 2093 // TODO: Throw an exception? We got neither a string nor something 2094 // that looks like a compatible lessphp cache structure. 2095 return null; 2096 } 2097 2098 if ($root !== null) { 2099 // If we have a root value which means we should rebuild. 2100 $out = array(); 2101 $out['root'] = $root; 2102 $out['compiled'] = $this->compileFile($root); 2103 $out['files'] = $this->allParsedFiles(); 2104 $out['updated'] = time(); 2105 return $out; 2106 } else { 2107 // No changes, pass back the structure 2108 // we were given initially. 2109 return $in; 2110 } 2111 2112 } 2113 2114 // parse and compile buffer 2115 // This is deprecated 2116 public function parse($str = null, $initialVariables = null) { 2117 if (is_array($str)) { 2118 $initialVariables = $str; 2119 $str = null; 2120 } 2121 2122 $oldVars = $this->registeredVars; 2123 if ($initialVariables !== null) { 2124 $this->setVariables($initialVariables); 2125 } 2126 2127 if ($str == null) { 2128 if (empty($this->_parseFile)) { 2129 throw new exception("nothing to parse"); 2130 } 2131 2132 $out = $this->compileFile($this->_parseFile); 2133 } else { 2134 $out = $this->compile($str); 2135 } 2136 2137 $this->registeredVars = $oldVars; 2138 return $out; 2139 } 2140 2141 protected function makeParser($name) { 2142 $parser = new lessc_parser($this, $name); 2143 $parser->writeComments = $this->preserveComments; 2144 2145 return $parser; 2146 } 2147 2148 public function setFormatter($name) { 2149 $this->formatterName = $name; 2150 } 2151 2152 protected function newFormatter() { 2153 $className = "lessc_formatter_lessjs"; 2154 if (!empty($this->formatterName)) { 2155 if (!is_string($this->formatterName)) 2156 return $this->formatterName; 2157 $className = "lessc_formatter_$this->formatterName"; 2158 } 2159 2160 return new $className; 2161 } 2162 2163 public function setPreserveComments($preserve) { 2164 $this->preserveComments = $preserve; 2165 } 2166 2167 public function registerFunction($name, $func) { 2168 $this->libFunctions[$name] = $func; 2169 } 2170 2171 public function unregisterFunction($name) { 2172 unset($this->libFunctions[$name]); 2173 } 2174 2175 public function setVariables($variables) { 2176 $this->registeredVars = array_merge($this->registeredVars, $variables); 2177 } 2178 2179 public function unsetVariable($name) { 2180 unset($this->registeredVars[$name]); 2181 } 2182 2183 public function setImportDir($dirs) { 2184 $this->importDir = (array)$dirs; 2185 } 2186 2187 public function addImportDir($dir) { 2188 $this->importDir = (array)$this->importDir; 2189 $this->importDir[] = $dir; 2190 } 2191 2192 public function allParsedFiles() { 2193 return $this->allParsedFiles; 2194 } 2195 2196 public function addParsedFile($file) { 2197 $this->allParsedFiles[realpath($file)] = filemtime($file); 2198 } 2199 2200 /** 2201 * Uses the current value of $this->count to show line and line number 2202 */ 2203 public function throwError($msg = null) { 2204 if ($this->sourceLoc >= 0) { 2205 $this->sourceParser->throwError($msg, $this->sourceLoc); 2206 } 2207 throw new exception($msg); 2208 } 2209 2210 // compile file $in to file $out if $in is newer than $out 2211 // returns true when it compiles, false otherwise 2212 public static function ccompile($in, $out, $less = null) { 2213 if ($less === null) { 2214 $less = new self; 2215 } 2216 return $less->checkedCompile($in, $out); 2217 } 2218 2219 public static function cexecute($in, $force = false, $less = null) { 2220 if ($less === null) { 2221 $less = new self; 2222 } 2223 return $less->cachedCompile($in, $force); 2224 } 2225 2226 protected static $cssColors = array( 2227 'aliceblue' => '240,248,255', 2228 'antiquewhite' => '250,235,215', 2229 'aqua' => '0,255,255', 2230 'aquamarine' => '127,255,212', 2231 'azure' => '240,255,255', 2232 'beige' => '245,245,220', 2233 'bisque' => '255,228,196', 2234 'black' => '0,0,0', 2235 'blanchedalmond' => '255,235,205', 2236 'blue' => '0,0,255', 2237 'blueviolet' => '138,43,226', 2238 'brown' => '165,42,42', 2239 'burlywood' => '222,184,135', 2240 'cadetblue' => '95,158,160', 2241 'chartreuse' => '127,255,0', 2242 'chocolate' => '210,105,30', 2243 'coral' => '255,127,80', 2244 'cornflowerblue' => '100,149,237', 2245 'cornsilk' => '255,248,220', 2246 'crimson' => '220,20,60', 2247 'cyan' => '0,255,255', 2248 'darkblue' => '0,0,139', 2249 'darkcyan' => '0,139,139', 2250 'darkgoldenrod' => '184,134,11', 2251 'darkgray' => '169,169,169', 2252 'darkgreen' => '0,100,0', 2253 'darkgrey' => '169,169,169', 2254 'darkkhaki' => '189,183,107', 2255 'darkmagenta' => '139,0,139', 2256 'darkolivegreen' => '85,107,47', 2257 'darkorange' => '255,140,0', 2258 'darkorchid' => '153,50,204', 2259 'darkred' => '139,0,0', 2260 'darksalmon' => '233,150,122', 2261 'darkseagreen' => '143,188,143', 2262 'darkslateblue' => '72,61,139', 2263 'darkslategray' => '47,79,79', 2264 'darkslategrey' => '47,79,79', 2265 'darkturquoise' => '0,206,209', 2266 'darkviolet' => '148,0,211', 2267 'deeppink' => '255,20,147', 2268 'deepskyblue' => '0,191,255', 2269 'dimgray' => '105,105,105', 2270 'dimgrey' => '105,105,105', 2271 'dodgerblue' => '30,144,255', 2272 'firebrick' => '178,34,34', 2273 'floralwhite' => '255,250,240', 2274 'forestgreen' => '34,139,34', 2275 'fuchsia' => '255,0,255', 2276 'gainsboro' => '220,220,220', 2277 'ghostwhite' => '248,248,255', 2278 'gold' => '255,215,0', 2279 'goldenrod' => '218,165,32', 2280 'gray' => '128,128,128', 2281 'green' => '0,128,0', 2282 'greenyellow' => '173,255,47', 2283 'grey' => '128,128,128', 2284 'honeydew' => '240,255,240', 2285 'hotpink' => '255,105,180', 2286 'indianred' => '205,92,92', 2287 'indigo' => '75,0,130', 2288 'ivory' => '255,255,240', 2289 'khaki' => '240,230,140', 2290 'lavender' => '230,230,250', 2291 'lavenderblush' => '255,240,245', 2292 'lawngreen' => '124,252,0', 2293 'lemonchiffon' => '255,250,205', 2294 'lightblue' => '173,216,230', 2295 'lightcoral' => '240,128,128', 2296 'lightcyan' => '224,255,255', 2297 'lightgoldenrodyellow' => '250,250,210', 2298 'lightgray' => '211,211,211', 2299 'lightgreen' => '144,238,144', 2300 'lightgrey' => '211,211,211', 2301 'lightpink' => '255,182,193', 2302 'lightsalmon' => '255,160,122', 2303 'lightseagreen' => '32,178,170', 2304 'lightskyblue' => '135,206,250', 2305 'lightslategray' => '119,136,153', 2306 'lightslategrey' => '119,136,153', 2307 'lightsteelblue' => '176,196,222', 2308 'lightyellow' => '255,255,224', 2309 'lime' => '0,255,0', 2310 'limegreen' => '50,205,50', 2311 'linen' => '250,240,230', 2312 'magenta' => '255,0,255', 2313 'maroon' => '128,0,0', 2314 'mediumaquamarine' => '102,205,170', 2315 'mediumblue' => '0,0,205', 2316 'mediumorchid' => '186,85,211', 2317 'mediumpurple' => '147,112,219', 2318 'mediumseagreen' => '60,179,113', 2319 'mediumslateblue' => '123,104,238', 2320 'mediumspringgreen' => '0,250,154', 2321 'mediumturquoise' => '72,209,204', 2322 'mediumvioletred' => '199,21,133', 2323 'midnightblue' => '25,25,112', 2324 'mintcream' => '245,255,250', 2325 'mistyrose' => '255,228,225', 2326 'moccasin' => '255,228,181', 2327 'navajowhite' => '255,222,173', 2328 'navy' => '0,0,128', 2329 'oldlace' => '253,245,230', 2330 'olive' => '128,128,0', 2331 'olivedrab' => '107,142,35', 2332 'orange' => '255,165,0', 2333 'orangered' => '255,69,0', 2334 'orchid' => '218,112,214', 2335 'palegoldenrod' => '238,232,170', 2336 'palegreen' => '152,251,152', 2337 'paleturquoise' => '175,238,238', 2338 'palevioletred' => '219,112,147', 2339 'papayawhip' => '255,239,213', 2340 'peachpuff' => '255,218,185', 2341 'peru' => '205,133,63', 2342 'pink' => '255,192,203', 2343 'plum' => '221,160,221', 2344 'powderblue' => '176,224,230', 2345 'purple' => '128,0,128', 2346 'red' => '255,0,0', 2347 'rosybrown' => '188,143,143', 2348 'royalblue' => '65,105,225', 2349 'saddlebrown' => '139,69,19', 2350 'salmon' => '250,128,114', 2351 'sandybrown' => '244,164,96', 2352 'seagreen' => '46,139,87', 2353 'seashell' => '255,245,238', 2354 'sienna' => '160,82,45', 2355 'silver' => '192,192,192', 2356 'skyblue' => '135,206,235', 2357 'slateblue' => '106,90,205', 2358 'slategray' => '112,128,144', 2359 'slategrey' => '112,128,144', 2360 'snow' => '255,250,250', 2361 'springgreen' => '0,255,127', 2362 'steelblue' => '70,130,180', 2363 'tan' => '210,180,140', 2364 'teal' => '0,128,128', 2365 'thistle' => '216,191,216', 2366 'tomato' => '255,99,71', 2367 'transparent' => '0,0,0,0', 2368 'turquoise' => '64,224,208', 2369 'violet' => '238,130,238', 2370 'wheat' => '245,222,179', 2371 'white' => '255,255,255', 2372 'whitesmoke' => '245,245,245', 2373 'yellow' => '255,255,0', 2374 'yellowgreen' => '154,205,50' 2375 ); 2376} 2377 2378// responsible for taking a string of LESS code and converting it into a 2379// syntax tree 2380class lessc_parser { 2381 protected static $nextBlockId = 0; // used to uniquely identify blocks 2382 2383 protected static $precedence = array( 2384 '=<' => 0, 2385 '>=' => 0, 2386 '=' => 0, 2387 '<' => 0, 2388 '>' => 0, 2389 2390 '+' => 1, 2391 '-' => 1, 2392 '*' => 2, 2393 '/' => 2, 2394 '%' => 2, 2395 ); 2396 2397 protected static $whitePattern; 2398 protected static $commentMulti; 2399 2400 protected static $commentSingle = "//"; 2401 protected static $commentMultiLeft = "/*"; 2402 protected static $commentMultiRight = "*/"; 2403 2404 // regex string to match any of the operators 2405 protected static $operatorString; 2406 2407 // these properties will supress division unless it's inside parenthases 2408 protected static $supressDivisionProps = 2409 array('/border-radius$/i', '/^font$/i'); 2410 2411 protected $blockDirectives = array("font-face", "keyframes", "page", "-moz-document", "viewport", "-moz-viewport", "-o-viewport", "-ms-viewport"); 2412 protected $lineDirectives = array("charset"); 2413 2414 /** 2415 * if we are in parens we can be more liberal with whitespace around 2416 * operators because it must evaluate to a single value and thus is less 2417 * ambiguous. 2418 * 2419 * Consider: 2420 * property1: 10 -5; // is two numbers, 10 and -5 2421 * property2: (10 -5); // should evaluate to 5 2422 */ 2423 protected $inParens = false; 2424 2425 // caches preg escaped literals 2426 protected static $literalCache = array(); 2427 2428 public function __construct($lessc, $sourceName = null) { 2429 $this->eatWhiteDefault = true; 2430 // reference to less needed for vPrefix, mPrefix, and parentSelector 2431 $this->lessc = $lessc; 2432 2433 $this->sourceName = $sourceName; // name used for error messages 2434 2435 $this->writeComments = false; 2436 2437 if (!self::$operatorString) { 2438 self::$operatorString = 2439 '('.implode('|', array_map(array('lessc', 'preg_quote'), 2440 array_keys(self::$precedence))).')'; 2441 2442 $commentSingle = lessc::preg_quote(self::$commentSingle); 2443 $commentMultiLeft = lessc::preg_quote(self::$commentMultiLeft); 2444 $commentMultiRight = lessc::preg_quote(self::$commentMultiRight); 2445 2446 self::$commentMulti = $commentMultiLeft.'.*?'.$commentMultiRight; 2447 self::$whitePattern = '/'.$commentSingle.'[^\n]*\s*|('.self::$commentMulti.')\s*|\s+/Ais'; 2448 } 2449 } 2450 2451 public function parse($buffer) { 2452 $this->count = 0; 2453 $this->line = 1; 2454 2455 $this->env = null; // block stack 2456 $this->buffer = $this->writeComments ? $buffer : $this->removeComments($buffer); 2457 $this->pushSpecialBlock("root"); 2458 $this->eatWhiteDefault = true; 2459 $this->seenComments = array(); 2460 2461 // trim whitespace on head 2462 // if (preg_match('/^\s+/', $this->buffer, $m)) { 2463 // $this->line += substr_count($m[0], "\n"); 2464 // $this->buffer = ltrim($this->buffer); 2465 // } 2466 $this->whitespace(); 2467 2468 // parse the entire file 2469 while (false !== $this->parseChunk()); 2470 2471 if ($this->count != strlen($this->buffer)) 2472 $this->throwError(); 2473 2474 // TODO report where the block was opened 2475 if ( !property_exists($this->env, 'parent') || !is_null($this->env->parent) ) 2476 throw new exception('parse error: unclosed block'); 2477 2478 return $this->env; 2479 } 2480 2481 /** 2482 * Parse a single chunk off the head of the buffer and append it to the 2483 * current parse environment. 2484 * Returns false when the buffer is empty, or when there is an error. 2485 * 2486 * This function is called repeatedly until the entire document is 2487 * parsed. 2488 * 2489 * This parser is most similar to a recursive descent parser. Single 2490 * functions represent discrete grammatical rules for the language, and 2491 * they are able to capture the text that represents those rules. 2492 * 2493 * Consider the function lessc::keyword(). (all parse functions are 2494 * structured the same) 2495 * 2496 * The function takes a single reference argument. When calling the 2497 * function it will attempt to match a keyword on the head of the buffer. 2498 * If it is successful, it will place the keyword in the referenced 2499 * argument, advance the position in the buffer, and return true. If it 2500 * fails then it won't advance the buffer and it will return false. 2501 * 2502 * All of these parse functions are powered by lessc::match(), which behaves 2503 * the same way, but takes a literal regular expression. Sometimes it is 2504 * more convenient to use match instead of creating a new function. 2505 * 2506 * Because of the format of the functions, to parse an entire string of 2507 * grammatical rules, you can chain them together using &&. 2508 * 2509 * But, if some of the rules in the chain succeed before one fails, then 2510 * the buffer position will be left at an invalid state. In order to 2511 * avoid this, lessc::seek() is used to remember and set buffer positions. 2512 * 2513 * Before parsing a chain, use $s = $this->seek() to remember the current 2514 * position into $s. Then if a chain fails, use $this->seek($s) to 2515 * go back where we started. 2516 */ 2517 protected function parseChunk() { 2518 if (empty($this->buffer)) return false; 2519 $s = $this->seek(); 2520 2521 if ($this->whitespace()) { 2522 return true; 2523 } 2524 2525 // setting a property 2526 if ($this->keyword($key) && $this->assign() && 2527 $this->propertyValue($value, $key) && $this->end() 2528 ) { 2529 $this->append(array('assign', $key, $value), $s); 2530 return true; 2531 } else { 2532 $this->seek($s); 2533 } 2534 2535 2536 // look for special css blocks 2537 if ($this->literal('@', false)) { 2538 $this->count--; 2539 2540 // media 2541 if ($this->literal('@media')) { 2542 if (($this->mediaQueryList($mediaQueries) || true) 2543 && $this->literal('{') 2544 ) { 2545 $media = $this->pushSpecialBlock("media"); 2546 $media->queries = is_null($mediaQueries) ? array() : $mediaQueries; 2547 return true; 2548 } else { 2549 $this->seek($s); 2550 return false; 2551 } 2552 } 2553 2554 if ($this->literal("@", false) && $this->keyword($dirName)) { 2555 if ($this->isDirective($dirName, $this->blockDirectives)) { 2556 if (($this->openString("{", $dirValue, null, array(";")) || true) && 2557 $this->literal("{") 2558 ) { 2559 $dir = $this->pushSpecialBlock("directive"); 2560 $dir->name = $dirName; 2561 if (isset($dirValue)) $dir->value = $dirValue; 2562 return true; 2563 } 2564 } elseif ($this->isDirective($dirName, $this->lineDirectives)) { 2565 if ($this->propertyValue($dirValue) && $this->end()) { 2566 $this->append(array("directive", $dirName, $dirValue)); 2567 return true; 2568 } 2569 } 2570 } 2571 2572 $this->seek($s); 2573 } 2574 2575 // setting a variable 2576 if ($this->variable($var) && $this->assign() && 2577 $this->propertyValue($value) && $this->end() 2578 ) { 2579 $this->append(array('assign', $var, $value), $s); 2580 return true; 2581 } else { 2582 $this->seek($s); 2583 } 2584 2585 if ($this->import($importValue)) { 2586 $this->append($importValue, $s); 2587 return true; 2588 } 2589 2590 // opening parametric mixin 2591 if ($this->tag($tag, true) && $this->argumentDef($args, $isVararg) && 2592 ($this->guards($guards) || true) && 2593 $this->literal('{') 2594 ) { 2595 $block = $this->pushBlock($this->fixTags(array($tag))); 2596 $block->args = $args; 2597 $block->isVararg = $isVararg; 2598 if (!empty($guards)) $block->guards = $guards; 2599 return true; 2600 } else { 2601 $this->seek($s); 2602 } 2603 2604 // opening a simple block 2605 if ($this->tags($tags) && $this->literal('{', false)) { 2606 $tags = $this->fixTags($tags); 2607 $this->pushBlock($tags); 2608 return true; 2609 } else { 2610 $this->seek($s); 2611 } 2612 2613 // closing a block 2614 if ($this->literal('}', false)) { 2615 try { 2616 $block = $this->pop(); 2617 } catch (exception $e) { 2618 $this->seek($s); 2619 $this->throwError($e->getMessage()); 2620 } 2621 2622 $hidden = false; 2623 if (is_null($block->type)) { 2624 $hidden = true; 2625 if (!isset($block->args)) { 2626 foreach ($block->tags as $tag) { 2627 if (!is_string($tag) || $tag[0] != $this->lessc->mPrefix) { 2628 $hidden = false; 2629 break; 2630 } 2631 } 2632 } 2633 2634 foreach ($block->tags as $tag) { 2635 if (is_string($tag)) { 2636 $this->env->children[$tag][] = $block; 2637 } 2638 } 2639 } 2640 2641 if (!$hidden) { 2642 $this->append(array('block', $block), $s); 2643 } 2644 2645 // this is done here so comments aren't bundled into he block that 2646 // was just closed 2647 $this->whitespace(); 2648 return true; 2649 } 2650 2651 // mixin 2652 if ($this->mixinTags($tags) && 2653 ($this->argumentDef($argv, $isVararg) || true) && 2654 ($this->keyword($suffix) || true) && $this->end() 2655 ) { 2656 $tags = $this->fixTags($tags); 2657 $this->append(array('mixin', $tags, $argv, $suffix), $s); 2658 return true; 2659 } else { 2660 $this->seek($s); 2661 } 2662 2663 // spare ; 2664 if ($this->literal(';')) return true; 2665 2666 return false; // got nothing, throw error 2667 } 2668 2669 protected function isDirective($dirname, $directives) { 2670 // TODO: cache pattern in parser 2671 $pattern = implode("|", 2672 array_map(array("lessc", "preg_quote"), $directives)); 2673 $pattern = '/^(-[a-z-]+-)?(' . $pattern . ')$/i'; 2674 2675 return preg_match($pattern, $dirname); 2676 } 2677 2678 protected function fixTags($tags) { 2679 // move @ tags out of variable namespace 2680 foreach ($tags as &$tag) { 2681 if ($tag[0] == $this->lessc->vPrefix) 2682 $tag[0] = $this->lessc->mPrefix; 2683 } 2684 return $tags; 2685 } 2686 2687 // a list of expressions 2688 protected function expressionList(&$exps) { 2689 $values = array(); 2690 2691 while ($this->expression($exp)) { 2692 $values[] = $exp; 2693 } 2694 2695 if (count($values) == 0) return false; 2696 2697 $exps = lessc::compressList($values, ' '); 2698 return true; 2699 } 2700 2701 /** 2702 * Attempt to consume an expression. 2703 * @link http://en.wikipedia.org/wiki/Operator-precedence_parser#Pseudo-code 2704 */ 2705 protected function expression(&$out) { 2706 if ($this->value($lhs)) { 2707 $out = $this->expHelper($lhs, 0); 2708 2709 // look for / shorthand 2710 if (!empty($this->env->supressedDivision)) { 2711 unset($this->env->supressedDivision); 2712 $s = $this->seek(); 2713 if ($this->literal("/") && $this->value($rhs)) { 2714 $out = array("list", "", 2715 array($out, array("keyword", "/"), $rhs)); 2716 } else { 2717 $this->seek($s); 2718 } 2719 } 2720 2721 return true; 2722 } 2723 return false; 2724 } 2725 2726 /** 2727 * recursively parse infix equation with $lhs at precedence $minP 2728 */ 2729 protected function expHelper($lhs, $minP) { 2730 $this->inExp = true; 2731 $ss = $this->seek(); 2732 2733 while (true) { 2734 $whiteBefore = isset($this->buffer[$this->count - 1]) && 2735 ctype_space($this->buffer[$this->count - 1]); 2736 2737 // If there is whitespace before the operator, then we require 2738 // whitespace after the operator for it to be an expression 2739 $needWhite = $whiteBefore && !$this->inParens; 2740 2741 if ($this->match(self::$operatorString.($needWhite ? '\s' : ''), $m) && self::$precedence[$m[1]] >= $minP) { 2742 if (!$this->inParens && isset($this->env->currentProperty) && $m[1] == "/" && empty($this->env->supressedDivision)) { 2743 foreach (self::$supressDivisionProps as $pattern) { 2744 if (preg_match($pattern, $this->env->currentProperty)) { 2745 $this->env->supressedDivision = true; 2746 break 2; 2747 } 2748 } 2749 } 2750 2751 2752 $whiteAfter = isset($this->buffer[$this->count - 1]) && 2753 ctype_space($this->buffer[$this->count - 1]); 2754 2755 if (!$this->value($rhs)) break; 2756 2757 // peek for next operator to see what to do with rhs 2758 if ($this->peek(self::$operatorString, $next) && self::$precedence[$next[1]] > self::$precedence[$m[1]]) { 2759 $rhs = $this->expHelper($rhs, self::$precedence[$next[1]]); 2760 } 2761 2762 $lhs = array('expression', $m[1], $lhs, $rhs, $whiteBefore, $whiteAfter); 2763 $ss = $this->seek(); 2764 2765 continue; 2766 } 2767 2768 break; 2769 } 2770 2771 $this->seek($ss); 2772 2773 return $lhs; 2774 } 2775 2776 // consume a list of values for a property 2777 public function propertyValue(&$value, $keyName = null) { 2778 $values = array(); 2779 2780 if ($keyName !== null) $this->env->currentProperty = $keyName; 2781 2782 $s = null; 2783 while ($this->expressionList($v)) { 2784 $values[] = $v; 2785 $s = $this->seek(); 2786 if (!$this->literal(',')) break; 2787 } 2788 2789 if ($s) $this->seek($s); 2790 2791 if ($keyName !== null) unset($this->env->currentProperty); 2792 2793 if (count($values) == 0) return false; 2794 2795 $value = lessc::compressList($values, ', '); 2796 return true; 2797 } 2798 2799 protected function parenValue(&$out) { 2800 $s = $this->seek(); 2801 2802 // speed shortcut 2803 if (isset($this->buffer[$this->count]) && $this->buffer[$this->count] != "(") { 2804 return false; 2805 } 2806 2807 $inParens = $this->inParens; 2808 if ($this->literal("(") && 2809 ($this->inParens = true) && $this->expression($exp) && 2810 $this->literal(")") 2811 ) { 2812 $out = $exp; 2813 $this->inParens = $inParens; 2814 return true; 2815 } else { 2816 $this->inParens = $inParens; 2817 $this->seek($s); 2818 } 2819 2820 return false; 2821 } 2822 2823 // a single value 2824 protected function value(&$value) { 2825 $s = $this->seek(); 2826 2827 // speed shortcut 2828 if (isset($this->buffer[$this->count]) && $this->buffer[$this->count] == "-") { 2829 // negation 2830 if ($this->literal("-", false) && 2831 (($this->variable($inner) && $inner = array("variable", $inner)) || 2832 $this->unit($inner) || 2833 $this->parenValue($inner)) 2834 ) { 2835 $value = array("unary", "-", $inner); 2836 return true; 2837 } else { 2838 $this->seek($s); 2839 } 2840 } 2841 2842 if ($this->parenValue($value)) return true; 2843 if ($this->unit($value)) return true; 2844 if ($this->color($value)) return true; 2845 if ($this->func($value)) return true; 2846 if ($this->string($value)) return true; 2847 2848 if ($this->keyword($word)) { 2849 $value = array('keyword', $word); 2850 return true; 2851 } 2852 2853 // try a variable 2854 if ($this->variable($var)) { 2855 $value = array('variable', $var); 2856 return true; 2857 } 2858 2859 // unquote string (should this work on any type? 2860 if ($this->literal("~") && $this->string($str)) { 2861 $value = array("escape", $str); 2862 return true; 2863 } else { 2864 $this->seek($s); 2865 } 2866 2867 // css hack: \0 2868 if ($this->literal('\\') && $this->match('([0-9]+)', $m)) { 2869 $value = array('keyword', '\\'.$m[1]); 2870 return true; 2871 } else { 2872 $this->seek($s); 2873 } 2874 2875 return false; 2876 } 2877 2878 // an import statement 2879 protected function import(&$out) { 2880 if (!$this->literal('@import')) return false; 2881 2882 // @import "something.css" media; 2883 // @import url("something.css") media; 2884 // @import url(something.css) media; 2885 2886 if ($this->propertyValue($value)) { 2887 $out = array("import", $value); 2888 return true; 2889 } 2890 } 2891 2892 protected function mediaQueryList(&$out) { 2893 if ($this->genericList($list, "mediaQuery", ",", false)) { 2894 $out = $list[2]; 2895 return true; 2896 } 2897 return false; 2898 } 2899 2900 protected function mediaQuery(&$out) { 2901 $s = $this->seek(); 2902 2903 $expressions = null; 2904 $parts = array(); 2905 2906 if (($this->literal("only") && ($only = true) || $this->literal("not") && ($not = true) || true) && $this->keyword($mediaType)) { 2907 $prop = array("mediaType"); 2908 if (isset($only)) $prop[] = "only"; 2909 if (isset($not)) $prop[] = "not"; 2910 $prop[] = $mediaType; 2911 $parts[] = $prop; 2912 } else { 2913 $this->seek($s); 2914 } 2915 2916 2917 if (!empty($mediaType) && !$this->literal("and")) { 2918 // ~ 2919 } else { 2920 $this->genericList($expressions, "mediaExpression", "and", false); 2921 if (is_array($expressions)) $parts = array_merge($parts, $expressions[2]); 2922 } 2923 2924 if (count($parts) == 0) { 2925 $this->seek($s); 2926 return false; 2927 } 2928 2929 $out = $parts; 2930 return true; 2931 } 2932 2933 protected function mediaExpression(&$out) { 2934 $s = $this->seek(); 2935 $value = null; 2936 if ($this->literal("(") && 2937 $this->keyword($feature) && 2938 ($this->literal(":") && $this->expression($value) || true) && 2939 $this->literal(")") 2940 ) { 2941 $out = array("mediaExp", $feature); 2942 if ($value) $out[] = $value; 2943 return true; 2944 } elseif ($this->variable($variable)) { 2945 $out = array('variable', $variable); 2946 return true; 2947 } 2948 2949 $this->seek($s); 2950 return false; 2951 } 2952 2953 // an unbounded string stopped by $end 2954 protected function openString($end, &$out, $nestingOpen = null, $rejectStrs = null) { 2955 $oldWhite = $this->eatWhiteDefault; 2956 $this->eatWhiteDefault = false; 2957 2958 $stop = array("'", '"', "@{", $end); 2959 $stop = array_map(array("lessc", "preg_quote"), $stop); 2960 // $stop[] = self::$commentMulti; 2961 2962 if (!is_null($rejectStrs)) { 2963 $stop = array_merge($stop, $rejectStrs); 2964 } 2965 2966 $patt = '(.*?)('.implode("|", $stop).')'; 2967 2968 $nestingLevel = 0; 2969 2970 $content = array(); 2971 while ($this->match($patt, $m, false)) { 2972 if (!empty($m[1])) { 2973 $content[] = $m[1]; 2974 if ($nestingOpen) { 2975 $nestingLevel += substr_count($m[1], $nestingOpen); 2976 } 2977 } 2978 2979 $tok = $m[2]; 2980 2981 $this->count-= strlen($tok); 2982 if ($tok == $end) { 2983 if ($nestingLevel == 0) { 2984 break; 2985 } else { 2986 $nestingLevel--; 2987 } 2988 } 2989 2990 if (($tok == "'" || $tok == '"') && $this->string($str)) { 2991 $content[] = $str; 2992 continue; 2993 } 2994 2995 if ($tok == "@{" && $this->interpolation($inter)) { 2996 $content[] = $inter; 2997 continue; 2998 } 2999 3000 if (!empty($rejectStrs) && in_array($tok, $rejectStrs)) { 3001 break; 3002 } 3003 3004 $content[] = $tok; 3005 $this->count+= strlen($tok); 3006 } 3007 3008 $this->eatWhiteDefault = $oldWhite; 3009 3010 if (count($content) == 0) return false; 3011 3012 // trim the end 3013 if (is_string(end($content))) { 3014 $content[count($content) - 1] = rtrim(end($content)); 3015 } 3016 3017 $out = array("string", "", $content); 3018 return true; 3019 } 3020 3021 protected function string(&$out) { 3022 $s = $this->seek(); 3023 if ($this->literal('"', false)) { 3024 $delim = '"'; 3025 } elseif ($this->literal("'", false)) { 3026 $delim = "'"; 3027 } else { 3028 return false; 3029 } 3030 3031 $content = array(); 3032 3033 // look for either ending delim , escape, or string interpolation 3034 $patt = '([^\n]*?)(@\{|\\\\|' . 3035 lessc::preg_quote($delim).')'; 3036 3037 $oldWhite = $this->eatWhiteDefault; 3038 $this->eatWhiteDefault = false; 3039 3040 while ($this->match($patt, $m, false)) { 3041 $content[] = $m[1]; 3042 if ($m[2] == "@{") { 3043 $this->count -= strlen($m[2]); 3044 if ($this->interpolation($inter, false)) { 3045 $content[] = $inter; 3046 } else { 3047 $this->count += strlen($m[2]); 3048 $content[] = "@{"; // ignore it 3049 } 3050 } elseif ($m[2] == '\\') { 3051 $content[] = $m[2]; 3052 if ($this->literal($delim, false)) { 3053 $content[] = $delim; 3054 } 3055 } else { 3056 $this->count -= strlen($delim); 3057 break; // delim 3058 } 3059 } 3060 3061 $this->eatWhiteDefault = $oldWhite; 3062 3063 if ($this->literal($delim)) { 3064 $out = array("string", $delim, $content); 3065 return true; 3066 } 3067 3068 $this->seek($s); 3069 return false; 3070 } 3071 3072 protected function interpolation(&$out) { 3073 $oldWhite = $this->eatWhiteDefault; 3074 $this->eatWhiteDefault = true; 3075 3076 $s = $this->seek(); 3077 if ($this->literal("@{") && 3078 $this->openString("}", $interp, null, array("'", '"', ";")) && 3079 $this->literal("}", false) 3080 ) { 3081 $out = array("interpolate", $interp); 3082 $this->eatWhiteDefault = $oldWhite; 3083 if ($this->eatWhiteDefault) $this->whitespace(); 3084 return true; 3085 } 3086 3087 $this->eatWhiteDefault = $oldWhite; 3088 $this->seek($s); 3089 return false; 3090 } 3091 3092 protected function unit(&$unit) { 3093 // speed shortcut 3094 if (isset($this->buffer[$this->count])) { 3095 $char = $this->buffer[$this->count]; 3096 if (!ctype_digit($char) && $char != ".") return false; 3097 } 3098 3099 if ($this->match('([0-9]+(?:\.[0-9]*)?|\.[0-9]+)([%a-zA-Z]+)?', $m)) { 3100 $unit = array("number", $m[1], empty($m[2]) ? "" : $m[2]); 3101 return true; 3102 } 3103 return false; 3104 } 3105 3106 // a # color 3107 protected function color(&$out) { 3108 if ($this->match('(#(?:[0-9a-f]{8}|[0-9a-f]{6}|[0-9a-f]{3}))', $m)) { 3109 if (strlen($m[1]) > 7) { 3110 $out = array("string", "", array($m[1])); 3111 } else { 3112 $out = array("raw_color", $m[1]); 3113 } 3114 return true; 3115 } 3116 3117 return false; 3118 } 3119 3120 // consume an argument definition list surrounded by () 3121 // each argument is a variable name with optional value 3122 // or at the end a ... or a variable named followed by ... 3123 // arguments are separated by , unless a ; is in the list, then ; is the 3124 // delimiter. 3125 protected function argumentDef(&$args, &$isVararg) { 3126 $s = $this->seek(); 3127 if (!$this->literal('(')) { 3128 return false; 3129 } 3130 3131 $values = array(); 3132 $delim = ","; 3133 $method = "expressionList"; 3134 3135 $isVararg = false; 3136 while (true) { 3137 if ($this->literal("...")) { 3138 $isVararg = true; 3139 break; 3140 } 3141 3142 if ($this->$method($value)) { 3143 if ($value[0] == "variable") { 3144 $arg = array("arg", $value[1]); 3145 $ss = $this->seek(); 3146 3147 if ($this->assign() && $this->$method($rhs)) { 3148 $arg[] = $rhs; 3149 } else { 3150 $this->seek($ss); 3151 if ($this->literal("...")) { 3152 $arg[0] = "rest"; 3153 $isVararg = true; 3154 } 3155 } 3156 3157 $values[] = $arg; 3158 if ($isVararg) { 3159 break; 3160 } 3161 continue; 3162 } else { 3163 $values[] = array("lit", $value); 3164 } 3165 } 3166 3167 3168 if (!$this->literal($delim)) { 3169 if ($delim == "," && $this->literal(";")) { 3170 // found new delim, convert existing args 3171 $delim = ";"; 3172 $method = "propertyValue"; 3173 3174 // transform arg list 3175 if (isset($values[1])) { // 2 items 3176 $newList = array(); 3177 foreach ($values as $i => $arg) { 3178 switch ($arg[0]) { 3179 case "arg": 3180 if ($i) { 3181 $this->throwError("Cannot mix ; and , as delimiter types"); 3182 } 3183 $newList[] = $arg[2]; 3184 break; 3185 case "lit": 3186 $newList[] = $arg[1]; 3187 break; 3188 case "rest": 3189 $this->throwError("Unexpected rest before semicolon"); 3190 } 3191 } 3192 3193 $newList = array("list", ", ", $newList); 3194 3195 switch ($values[0][0]) { 3196 case "arg": 3197 $newArg = array("arg", $values[0][1], $newList); 3198 break; 3199 case "lit": 3200 $newArg = array("lit", $newList); 3201 break; 3202 } 3203 3204 } elseif ($values) { // 1 item 3205 $newArg = $values[0]; 3206 } 3207 3208 if ($newArg) { 3209 $values = array($newArg); 3210 } 3211 } else { 3212 break; 3213 } 3214 } 3215 } 3216 3217 if (!$this->literal(')')) { 3218 $this->seek($s); 3219 return false; 3220 } 3221 3222 $args = $values; 3223 3224 return true; 3225 } 3226 3227 // consume a list of tags 3228 // this accepts a hanging delimiter 3229 protected function tags(&$tags, $simple = false, $delim = ',') { 3230 $tags = array(); 3231 while ($this->tag($tt, $simple)) { 3232 $tags[] = $tt; 3233 if (!$this->literal($delim)) break; 3234 } 3235 if (count($tags) == 0) return false; 3236 3237 return true; 3238 } 3239 3240 // list of tags of specifying mixin path 3241 // optionally separated by > (lazy, accepts extra >) 3242 protected function mixinTags(&$tags) { 3243 $tags = array(); 3244 while ($this->tag($tt, true)) { 3245 $tags[] = $tt; 3246 $this->literal(">"); 3247 } 3248 3249 if (!$tags) { 3250 return false; 3251 } 3252 3253 return true; 3254 } 3255 3256 // a bracketed value (contained within in a tag definition) 3257 protected function tagBracket(&$parts, &$hasExpression) { 3258 // speed shortcut 3259 if (isset($this->buffer[$this->count]) && $this->buffer[$this->count] != "[") { 3260 return false; 3261 } 3262 3263 $s = $this->seek(); 3264 3265 $hasInterpolation = false; 3266 3267 if ($this->literal("[", false)) { 3268 $attrParts = array("["); 3269 // keyword, string, operator 3270 while (true) { 3271 if ($this->literal("]", false)) { 3272 $this->count--; 3273 break; // get out early 3274 } 3275 3276 if ($this->match('\s+', $m)) { 3277 $attrParts[] = " "; 3278 continue; 3279 } 3280 if ($this->string($str)) { 3281 // escape parent selector, (yuck) 3282 foreach ($str[2] as &$chunk) { 3283 $chunk = str_replace($this->lessc->parentSelector, "$&$", $chunk); 3284 } 3285 3286 $attrParts[] = $str; 3287 $hasInterpolation = true; 3288 continue; 3289 } 3290 3291 if ($this->keyword($word)) { 3292 $attrParts[] = $word; 3293 continue; 3294 } 3295 3296 if ($this->interpolation($inter, false)) { 3297 $attrParts[] = $inter; 3298 $hasInterpolation = true; 3299 continue; 3300 } 3301 3302 // operator, handles attr namespace too 3303 if ($this->match('[|-~\$\*\^=]+', $m)) { 3304 $attrParts[] = $m[0]; 3305 continue; 3306 } 3307 3308 break; 3309 } 3310 3311 if ($this->literal("]", false)) { 3312 $attrParts[] = "]"; 3313 foreach ($attrParts as $part) { 3314 $parts[] = $part; 3315 } 3316 $hasExpression = $hasExpression || $hasInterpolation; 3317 return true; 3318 } 3319 $this->seek($s); 3320 } 3321 3322 $this->seek($s); 3323 return false; 3324 } 3325 3326 // a space separated list of selectors 3327 protected function tag(&$tag, $simple = false) { 3328 if ($simple) { 3329 $chars = '^@,:;{}\][>\(\) "\''; 3330 } else { 3331 $chars = '^@,;{}["\''; 3332 } 3333 $s = $this->seek(); 3334 3335 $hasExpression = false; 3336 $parts = array(); 3337 while ($this->tagBracket($parts, $hasExpression)); 3338 3339 $oldWhite = $this->eatWhiteDefault; 3340 $this->eatWhiteDefault = false; 3341 3342 while (true) { 3343 if ($this->match('(['.$chars.'0-9]['.$chars.']*)', $m)) { 3344 $parts[] = $m[1]; 3345 if ($simple) break; 3346 3347 while ($this->tagBracket($parts, $hasExpression)); 3348 continue; 3349 } 3350 3351 if (isset($this->buffer[$this->count]) && $this->buffer[$this->count] == "@") { 3352 if ($this->interpolation($interp)) { 3353 $hasExpression = true; 3354 $interp[2] = true; // don't unescape 3355 $parts[] = $interp; 3356 continue; 3357 } 3358 3359 if ($this->literal("@")) { 3360 $parts[] = "@"; 3361 continue; 3362 } 3363 } 3364 3365 if ($this->unit($unit)) { // for keyframes 3366 $parts[] = $unit[1]; 3367 $parts[] = $unit[2]; 3368 continue; 3369 } 3370 3371 break; 3372 } 3373 3374 $this->eatWhiteDefault = $oldWhite; 3375 if (!$parts) { 3376 $this->seek($s); 3377 return false; 3378 } 3379 3380 if ($hasExpression) { 3381 $tag = array("exp", array("string", "", $parts)); 3382 } else { 3383 $tag = trim(implode($parts)); 3384 } 3385 3386 $this->whitespace(); 3387 return true; 3388 } 3389 3390 // a css function 3391 protected function func(&$func) { 3392 $s = $this->seek(); 3393 3394 if ($this->match('(%|[\w\-_][\w\-_:\.]+|[\w_])', $m) && $this->literal('(')) { 3395 $fname = $m[1]; 3396 3397 $sPreArgs = $this->seek(); 3398 3399 $args = array(); 3400 while (true) { 3401 $ss = $this->seek(); 3402 // this ugly nonsense is for ie filter properties 3403 if ($this->keyword($name) && $this->literal('=') && $this->expressionList($value)) { 3404 $args[] = array("string", "", array($name, "=", $value)); 3405 } else { 3406 $this->seek($ss); 3407 if ($this->expressionList($value)) { 3408 $args[] = $value; 3409 } 3410 } 3411 3412 if (!$this->literal(',')) break; 3413 } 3414 $args = array('list', ',', $args); 3415 3416 if ($this->literal(')')) { 3417 $func = array('function', $fname, $args); 3418 return true; 3419 } elseif ($fname == 'url') { 3420 // couldn't parse and in url? treat as string 3421 $this->seek($sPreArgs); 3422 if ($this->openString(")", $string) && $this->literal(")")) { 3423 $func = array('function', $fname, $string); 3424 return true; 3425 } 3426 } 3427 } 3428 3429 $this->seek($s); 3430 return false; 3431 } 3432 3433 // consume a less variable 3434 protected function variable(&$name) { 3435 $s = $this->seek(); 3436 if ($this->literal($this->lessc->vPrefix, false) && 3437 ($this->variable($sub) || $this->keyword($name)) 3438 ) { 3439 if (!empty($sub)) { 3440 $name = array('variable', $sub); 3441 } else { 3442 $name = $this->lessc->vPrefix.$name; 3443 } 3444 return true; 3445 } 3446 3447 $name = null; 3448 $this->seek($s); 3449 return false; 3450 } 3451 3452 /** 3453 * Consume an assignment operator 3454 * Can optionally take a name that will be set to the current property name 3455 */ 3456 protected function assign($name = null) { 3457 if ($name) $this->currentProperty = $name; 3458 return $this->literal(':') || $this->literal('='); 3459 } 3460 3461 // consume a keyword 3462 protected function keyword(&$word) { 3463 if ($this->match('([\w_\-\*!"][\w\-_"]*)', $m)) { 3464 $word = $m[1]; 3465 return true; 3466 } 3467 return false; 3468 } 3469 3470 // consume an end of statement delimiter 3471 protected function end() { 3472 if ($this->literal(';', false)) { 3473 return true; 3474 } elseif ($this->count == strlen($this->buffer) || $this->buffer[$this->count] == '}') { 3475 // if there is end of file or a closing block next then we don't need a ; 3476 return true; 3477 } 3478 return false; 3479 } 3480 3481 protected function guards(&$guards) { 3482 $s = $this->seek(); 3483 3484 if (!$this->literal("when")) { 3485 $this->seek($s); 3486 return false; 3487 } 3488 3489 $guards = array(); 3490 3491 while ($this->guardGroup($g)) { 3492 $guards[] = $g; 3493 if (!$this->literal(",")) break; 3494 } 3495 3496 if (count($guards) == 0) { 3497 $guards = null; 3498 $this->seek($s); 3499 return false; 3500 } 3501 3502 return true; 3503 } 3504 3505 // a bunch of guards that are and'd together 3506 // TODO rename to guardGroup 3507 protected function guardGroup(&$guardGroup) { 3508 $s = $this->seek(); 3509 $guardGroup = array(); 3510 while ($this->guard($guard)) { 3511 $guardGroup[] = $guard; 3512 if (!$this->literal("and")) break; 3513 } 3514 3515 if (count($guardGroup) == 0) { 3516 $guardGroup = null; 3517 $this->seek($s); 3518 return false; 3519 } 3520 3521 return true; 3522 } 3523 3524 protected function guard(&$guard) { 3525 $s = $this->seek(); 3526 $negate = $this->literal("not"); 3527 3528 if ($this->literal("(") && $this->expression($exp) && $this->literal(")")) { 3529 $guard = $exp; 3530 if ($negate) $guard = array("negate", $guard); 3531 return true; 3532 } 3533 3534 $this->seek($s); 3535 return false; 3536 } 3537 3538 /* raw parsing functions */ 3539 3540 protected function literal($what, $eatWhitespace = null) { 3541 if ($eatWhitespace === null) $eatWhitespace = $this->eatWhiteDefault; 3542 3543 // shortcut on single letter 3544 if (!isset($what[1]) && isset($this->buffer[$this->count])) { 3545 if ($this->buffer[$this->count] == $what) { 3546 if (!$eatWhitespace) { 3547 $this->count++; 3548 return true; 3549 } 3550 // goes below... 3551 } else { 3552 return false; 3553 } 3554 } 3555 3556 if (!isset(self::$literalCache[$what])) { 3557 self::$literalCache[$what] = lessc::preg_quote($what); 3558 } 3559 3560 return $this->match(self::$literalCache[$what], $m, $eatWhitespace); 3561 } 3562 3563 protected function genericList(&$out, $parseItem, $delim = "", $flatten = true) { 3564 $s = $this->seek(); 3565 $items = array(); 3566 while ($this->$parseItem($value)) { 3567 $items[] = $value; 3568 if ($delim) { 3569 if (!$this->literal($delim)) break; 3570 } 3571 } 3572 3573 if (count($items) == 0) { 3574 $this->seek($s); 3575 return false; 3576 } 3577 3578 if ($flatten && count($items) == 1) { 3579 $out = $items[0]; 3580 } else { 3581 $out = array("list", $delim, $items); 3582 } 3583 3584 return true; 3585 } 3586 3587 3588 // advance counter to next occurrence of $what 3589 // $until - don't include $what in advance 3590 // $allowNewline, if string, will be used as valid char set 3591 protected function to($what, &$out, $until = false, $allowNewline = false) { 3592 if (is_string($allowNewline)) { 3593 $validChars = $allowNewline; 3594 } else { 3595 $validChars = $allowNewline ? "." : "[^\n]"; 3596 } 3597 if (!$this->match('('.$validChars.'*?)'.lessc::preg_quote($what), $m, !$until)) return false; 3598 if ($until) $this->count -= strlen($what); // give back $what 3599 $out = $m[1]; 3600 return true; 3601 } 3602 3603 // try to match something on head of buffer 3604 protected function match($regex, &$out, $eatWhitespace = null) { 3605 if ($eatWhitespace === null) $eatWhitespace = $this->eatWhiteDefault; 3606 3607 $r = '/'.$regex.($eatWhitespace && !$this->writeComments ? '\s*' : '').'/Ais'; 3608 if (preg_match($r, $this->buffer, $out, null, $this->count)) { 3609 $this->count += strlen($out[0]); 3610 if ($eatWhitespace && $this->writeComments) $this->whitespace(); 3611 return true; 3612 } 3613 return false; 3614 } 3615 3616 // match some whitespace 3617 protected function whitespace() { 3618 if ($this->writeComments) { 3619 $gotWhite = false; 3620 while (preg_match(self::$whitePattern, $this->buffer, $m, null, $this->count)) { 3621 if (isset($m[1]) && empty($this->seenComments[$this->count])) { 3622 $this->append(array("comment", $m[1])); 3623 $this->seenComments[$this->count] = true; 3624 } 3625 $this->count += strlen($m[0]); 3626 $gotWhite = true; 3627 } 3628 return $gotWhite; 3629 } else { 3630 $this->match("", $m); 3631 return strlen($m[0]) > 0; 3632 } 3633 } 3634 3635 // match something without consuming it 3636 protected function peek($regex, &$out = null, $from = null) { 3637 if (is_null($from)) $from = $this->count; 3638 $r = '/'.$regex.'/Ais'; 3639 $result = preg_match($r, $this->buffer, $out, null, $from); 3640 3641 return $result; 3642 } 3643 3644 // seek to a spot in the buffer or return where we are on no argument 3645 protected function seek($where = null) { 3646 if ($where === null) return $this->count; 3647 else $this->count = $where; 3648 return true; 3649 } 3650 3651 /* misc functions */ 3652 3653 public function throwError($msg = "parse error", $count = null) { 3654 $count = is_null($count) ? $this->count : $count; 3655 3656 $line = $this->line + 3657 substr_count(substr($this->buffer, 0, $count), "\n"); 3658 3659 if (!empty($this->sourceName)) { 3660 $loc = "$this->sourceName on line $line"; 3661 } else { 3662 $loc = "line: $line"; 3663 } 3664 3665 // TODO this depends on $this->count 3666 if ($this->peek("(.*?)(\n|$)", $m, $count)) { 3667 throw new exception("$msg: failed at `$m[1]` $loc"); 3668 } else { 3669 throw new exception("$msg: $loc"); 3670 } 3671 } 3672 3673 protected function pushBlock($selectors = null, $type = null) { 3674 $b = new stdclass; 3675 $b->parent = $this->env; 3676 3677 $b->type = $type; 3678 $b->id = self::$nextBlockId++; 3679 3680 $b->isVararg = false; // TODO: kill me from here 3681 $b->tags = $selectors; 3682 3683 $b->props = array(); 3684 $b->children = array(); 3685 3686 $this->env = $b; 3687 return $b; 3688 } 3689 3690 // push a block that doesn't multiply tags 3691 protected function pushSpecialBlock($type) { 3692 return $this->pushBlock(null, $type); 3693 } 3694 3695 // append a property to the current block 3696 protected function append($prop, $pos = null) { 3697 if ($pos !== null) $prop[-1] = $pos; 3698 $this->env->props[] = $prop; 3699 } 3700 3701 // pop something off the stack 3702 protected function pop() { 3703 $old = $this->env; 3704 $this->env = $this->env->parent; 3705 return $old; 3706 } 3707 3708 // remove comments from $text 3709 // todo: make it work for all functions, not just url 3710 protected function removeComments($text) { 3711 $look = array( 3712 'url(', '//', '/*', '"', "'" 3713 ); 3714 3715 $out = ''; 3716 $min = null; 3717 while (true) { 3718 // find the next item 3719 foreach ($look as $token) { 3720 $pos = strpos($text, $token); 3721 if ($pos !== false) { 3722 if (!isset($min) || $pos < $min[1]) $min = array($token, $pos); 3723 } 3724 } 3725 3726 if (is_null($min)) break; 3727 3728 $count = $min[1]; 3729 $skip = 0; 3730 $newlines = 0; 3731 switch ($min[0]) { 3732 case 'url(': 3733 if (preg_match('/url\(.*?\)/', $text, $m, 0, $count)) 3734 $count += strlen($m[0]) - strlen($min[0]); 3735 break; 3736 case '"': 3737 case "'": 3738 if (preg_match('/'.$min[0].'.*?(?<!\\\\)'.$min[0].'/', $text, $m, 0, $count)) 3739 $count += strlen($m[0]) - 1; 3740 break; 3741 case '//': 3742 $skip = strpos($text, "\n", $count); 3743 if ($skip === false) $skip = strlen($text) - $count; 3744 else $skip -= $count; 3745 break; 3746 case '/*': 3747 if (preg_match('/\/\*.*?\*\//s', $text, $m, 0, $count)) { 3748 $skip = strlen($m[0]); 3749 $newlines = substr_count($m[0], "\n"); 3750 } 3751 break; 3752 } 3753 3754 if ($skip == 0) $count += strlen($min[0]); 3755 3756 $out .= substr($text, 0, $count).str_repeat("\n", $newlines); 3757 $text = substr($text, $count + $skip); 3758 3759 $min = null; 3760 } 3761 3762 return $out.$text; 3763 } 3764 3765} 3766 3767class lessc_formatter_classic { 3768 public $indentChar = " "; 3769 3770 public $break = "\n"; 3771 public $open = " {"; 3772 public $close = "}"; 3773 public $selectorSeparator = ", "; 3774 public $assignSeparator = ":"; 3775 3776 public $openSingle = " { "; 3777 public $closeSingle = " }"; 3778 3779 public $disableSingle = false; 3780 public $breakSelectors = false; 3781 3782 public $compressColors = false; 3783 3784 public function __construct() { 3785 $this->indentLevel = 0; 3786 } 3787 3788 public function indentStr($n = 0) { 3789 return str_repeat($this->indentChar, max($this->indentLevel + $n, 0)); 3790 } 3791 3792 public function property($name, $value) { 3793 return $name . $this->assignSeparator . $value . ";"; 3794 } 3795 3796 protected function isEmpty($block) { 3797 if (empty($block->lines)) { 3798 foreach ($block->children as $child) { 3799 if (!$this->isEmpty($child)) return false; 3800 } 3801 3802 return true; 3803 } 3804 return false; 3805 } 3806 3807 public function block($block) { 3808 if ($this->isEmpty($block)) return; 3809 3810 $inner = $pre = $this->indentStr(); 3811 3812 $isSingle = !$this->disableSingle && 3813 is_null($block->type) && count($block->lines) == 1; 3814 3815 if (!empty($block->selectors)) { 3816 $this->indentLevel++; 3817 3818 if ($this->breakSelectors) { 3819 $selectorSeparator = $this->selectorSeparator . $this->break . $pre; 3820 } else { 3821 $selectorSeparator = $this->selectorSeparator; 3822 } 3823 3824 echo $pre . 3825 implode($selectorSeparator, $block->selectors); 3826 if ($isSingle) { 3827 echo $this->openSingle; 3828 $inner = ""; 3829 } else { 3830 echo $this->open . $this->break; 3831 $inner = $this->indentStr(); 3832 } 3833 3834 } 3835 3836 if (!empty($block->lines)) { 3837 $glue = $this->break.$inner; 3838 echo $inner . implode($glue, $block->lines); 3839 if (!$isSingle && !empty($block->children)) { 3840 echo $this->break; 3841 } 3842 } 3843 3844 foreach ($block->children as $child) { 3845 $this->block($child); 3846 } 3847 3848 if (!empty($block->selectors)) { 3849 if (!$isSingle && empty($block->children)) echo $this->break; 3850 3851 if ($isSingle) { 3852 echo $this->closeSingle . $this->break; 3853 } else { 3854 echo $pre . $this->close . $this->break; 3855 } 3856 3857 $this->indentLevel--; 3858 } 3859 } 3860} 3861 3862class lessc_formatter_compressed extends lessc_formatter_classic { 3863 public $disableSingle = true; 3864 public $open = "{"; 3865 public $selectorSeparator = ","; 3866 public $assignSeparator = ":"; 3867 public $break = ""; 3868 public $compressColors = true; 3869 3870 public function indentStr($n = 0) { 3871 return ""; 3872 } 3873} 3874 3875class lessc_formatter_lessjs extends lessc_formatter_classic { 3876 public $disableSingle = true; 3877 public $breakSelectors = true; 3878 public $assignSeparator = ": "; 3879 public $selectorSeparator = ","; 3880} 3881