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