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