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