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