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