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