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