Bootstrap Generator
98 |Kick-start your Twitter Bootstrap project the way you want. Simply alter the options below and click "Generate" to get your compiled Bootstrap CSS file.
99 |├── .DS_Store ├── README.md ├── includes └── lessc.inc.php ├── index.php └── lib ├── bootstrap.less ├── forms.less ├── mixins.less ├── patterns.less ├── reset.less ├── scaffolding.less ├── tables.less ├── type.less └── variables.less /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/martinbean/bootstrap-generator/7616be3e49e834694de65606dff972d0ffe6320d/.DS_Store -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Overview 2 | 3 | The Bootstrap Generator is for use in kick-starting your projects that use Twitter's Bootstrap CSS framework. Simply configure the options, click "Generate" and receive your own, compiled Bootstrap CSS file ready to work with! -------------------------------------------------------------------------------- /includes/lessc.inc.php: -------------------------------------------------------------------------------- 1 | 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 two stage process. First the incoming document 18 | * must be parsed. Parsing creates a tree in memory that represents the 19 | * structure of the document. Then, the tree of the document is recursively 20 | * compiled into the CSS text. The compile step has an implicit step called 21 | * reduction, where values are brought to their lowest form before being 22 | * turned to text, eg. mathematical equations are solved, and variables are 23 | * dereferenced. 24 | * 25 | * The parsing stage produces the final structure of the document, for this 26 | * reason mixins are mixed in and attribute accessors are referenced during 27 | * the parse step. A reduction is done on the mixed in block as it is mixed in. 28 | * 29 | * See the following: 30 | * - entry point for parsing and compiling: lessc::parse() 31 | * - parsing: lessc::parseChunk() 32 | * - compiling: lessc::compileBlock() 33 | * 34 | */ 35 | class lessc { 36 | protected $buffer; 37 | protected $count; 38 | protected $line; 39 | protected $libFunctions = array(); 40 | static protected $nextBlockId = 0; 41 | 42 | public $indentLevel; 43 | public $indentChar = ' '; 44 | 45 | protected $env = null; 46 | 47 | protected $allParsedFiles = array(); 48 | 49 | public $vPrefix = '@'; // prefix of abstract properties 50 | public $mPrefix = '$'; // prefix of abstract blocks 51 | public $imPrefix = '!'; // special character to add !important 52 | public $parentSelector = '&'; 53 | 54 | static protected $precedence = array( 55 | '+' => 0, 56 | '-' => 0, 57 | '*' => 1, 58 | '/' => 1, 59 | '%' => 1, 60 | ); 61 | static protected $operatorString; // regex string to match any of the operators 62 | 63 | // types that have delayed computation 64 | static protected $dtypes = array('expression', 'variable', 65 | 'function', 'negative', 'list', 'lookup'); 66 | 67 | /** 68 | * @link http://www.w3.org/TR/css3-values/ 69 | */ 70 | static protected $units = array( 71 | 'em', 'ex', 'px', 'gd', 'rem', 'vw', 'vh', 'vm', 'ch', // Relative length units 72 | 'in', 'cm', 'mm', 'pt', 'pc', // Absolute length units 73 | '%', // Percentages 74 | 'deg', 'grad', 'rad', 'turn', // Angles 75 | 'ms', 's', // Times 76 | 'Hz', 'kHz', //Frequencies 77 | ); 78 | 79 | public $importDisabled = false; 80 | public $importDir = ''; 81 | 82 | public $compat = false; // lessjs compatibility mode, does nothing right now 83 | 84 | /** 85 | * if we are in an expression then we don't need to worry about parsing font shorthand 86 | * $inExp becomes true after the first value in an expression, or if we enter parens 87 | */ 88 | protected $inExp = false; 89 | 90 | /** 91 | * if we are in parens we can be more liberal with whitespace around operators because 92 | * it must evaluate to a single value and thus is less ambiguous. 93 | * 94 | * Consider: 95 | * property1: 10 -5; // is two numbers, 10 and -5 96 | * property2: (10 -5); // should evaluate to 5 97 | */ 98 | protected $inParens = false; 99 | 100 | /** 101 | * Parse a single chunk off the head of the buffer and place it. 102 | * @return false when the buffer is empty, or there is an error 103 | * 104 | * This functions is called repeatedly until the entire document is 105 | * parsed. 106 | * 107 | * This parser is most similar to a recursive descent parser. Single 108 | * functions represent discrete grammatical rules for the language, and 109 | * they are able to capture the text that represents those rules. 110 | * 111 | * Consider the function lessc::keyword(). (all parse functions are 112 | * structured the same) 113 | * 114 | * The function takes a single reference argument. When calling the the 115 | * function it will attempt to match a keyword on the head of the buffer. 116 | * If it is successful, it will place the keyword in the referenced 117 | * argument, advance the position in the buffer, and return true. If it 118 | * fails then it won't advance the buffer and it will return false. 119 | * 120 | * All of these parse functions are powered by lessc::match(), which behaves 121 | * the same way, but takes a literal regular expression. Sometimes it is 122 | * more convenient to use match instead of creating a new function. 123 | * 124 | * Because of the format of the functions, to parse an entire string of 125 | * grammatical rules, you can chain them together using &&. 126 | * 127 | * But, if some of the rules in the chain succeed before one fails, then 128 | * then buffer position will be left at an invalid state. In order to 129 | * avoid this, lessc::seek() is used to remember and set buffer positions. 130 | * 131 | * Before doing a chain, use $s = $this->seek() to remember the current 132 | * position into $s. Then if a chain fails, use $this->seek($s) to 133 | * go back where we started. 134 | */ 135 | function parseChunk() { 136 | if (empty($this->buffer)) return false; 137 | $s = $this->seek(); 138 | 139 | // setting a property 140 | if ($this->keyword($key) && $this->assign() && 141 | $this->propertyValue($value) && $this->end()) 142 | { 143 | $this->append(array('assign', $key, $value)); 144 | return true; 145 | } else { 146 | $this->seek($s); 147 | } 148 | 149 | // look for special css blocks 150 | if ($this->env->parent == null && $this->literal('@', false)) { 151 | $this->count--; 152 | 153 | // a font-face block 154 | if ($this->literal('@font-face') && $this->literal('{')) { 155 | $b = $this->pushSpecialBlock('@font-face'); 156 | return true; 157 | } else { 158 | $this->seek($s); 159 | } 160 | 161 | // charset 162 | if ($this->literal('@charset') && $this->propertyValue($value) && 163 | $this->end()) 164 | { 165 | $this->append(array('charset', $value)); 166 | return true; 167 | } else { 168 | $this->seek($s); 169 | } 170 | 171 | 172 | // media 173 | if ($this->literal('@media') && $this->mediaTypes($types) && 174 | $this->literal('{')) 175 | { 176 | $b = $this->pushSpecialBlock('@media'); 177 | $b->media = $types; 178 | return true; 179 | } else { 180 | $this->seek($s); 181 | } 182 | 183 | // css animations 184 | if ($this->match('(@(-[a-z]+-)?keyframes)', $m) && 185 | $this->propertyValue($value) && $this->literal('{')) 186 | { 187 | $b = $this->pushSpecialBlock(trim($m[0])); 188 | $b->keyframes = $value; 189 | return true; 190 | } else { 191 | $this->seek($s); 192 | } 193 | } 194 | 195 | if (isset($this->env->keyframes)) { 196 | if ($this->match("(to|from|[0-9]+%)", $m) && $this->literal('{')) { 197 | $this->pushSpecialBlock($m[1]); 198 | return true; 199 | } else { 200 | $this->seek($s); 201 | } 202 | } 203 | 204 | // setting a variable 205 | if ($this->variable($name) && $this->assign() && 206 | $this->propertyValue($value) && $this->end()) 207 | { 208 | $this->append(array('assign', $this->vPrefix.$name, $value)); 209 | return true; 210 | } else { 211 | $this->seek($s); 212 | } 213 | 214 | if ($this->import($url, $media)) { 215 | // don't check .css files 216 | if (empty($media) && substr_compare($url, '.css', -4, 4) !== 0) { 217 | if ($this->importDisabled) { 218 | $this->append(array('raw', '/* import disabled */')); 219 | } else { 220 | $path = $this->findImport($url); 221 | if (!is_null($path)) { 222 | $this->append(array('import', $path)); 223 | return true; 224 | } 225 | } 226 | } 227 | 228 | $this->append(array('raw', '@import url("'.$url.'")'. 229 | ($media ? ' '.$media : '').';')); 230 | return true; 231 | } 232 | 233 | // opening parametric mixin 234 | if ($this->tag($tag, true) && $this->argumentDef($args) && 235 | $this->literal('{')) 236 | { 237 | $block = $this->pushBlock($this->fixTags(array($tag))); 238 | $block->args = $args; 239 | return true; 240 | } else { 241 | $this->seek($s); 242 | } 243 | 244 | // opening a simple block 245 | if ($this->tags($tags) && $this->literal('{')) { 246 | $tags = $this->fixTags($tags); 247 | $this->pushBlock($tags); 248 | return true; 249 | } else { 250 | $this->seek($s); 251 | } 252 | 253 | // closing a block 254 | if ($this->literal('}')) { 255 | try { 256 | $block = $this->pop(); 257 | } catch (exception $e) { 258 | $this->seek($s); 259 | $this->throwParseError($e->getMessage()); 260 | } 261 | 262 | $hidden = true; 263 | if (!isset($block->args)) foreach ($block->tags as $tag) { 264 | if ($tag{0} != $this->mPrefix) { 265 | $hidden = false; 266 | break; 267 | } 268 | } 269 | 270 | if (!$hidden) $this->append(array('block', $block)); 271 | foreach ($block->tags as $tag) { 272 | if (isset($this->env->children[$tag])) { 273 | $block = $this->mergeBlock($this->env->children[$tag], $block); 274 | } 275 | $this->env->children[$tag] = $block; 276 | } 277 | 278 | return true; 279 | } 280 | 281 | // mixin 282 | if ($this->mixinTags($tags) && 283 | ($this->argumentValues($argv) || true) && $this->end()) 284 | { 285 | $tags = $this->fixTags($tags); 286 | $this->append(array('mixin', $tags, $argv)); 287 | return true; 288 | } else { 289 | $this->seek($s); 290 | } 291 | 292 | // spare ; 293 | if ($this->literal(';')) return true; 294 | 295 | return false; // got nothing, throw error 296 | } 297 | 298 | function fixTags($tags) { 299 | // move @ tags out of variable namespace 300 | foreach ($tags as &$tag) { 301 | if ($tag{0} == $this->vPrefix) $tag[0] = $this->mPrefix; 302 | } 303 | return $tags; 304 | } 305 | 306 | // attempts to find the path of an import url, returns null for css files 307 | function findImport($url) { 308 | foreach ((array)$this->importDir as $dir) { 309 | $full = $dir.(substr($dir, -1) != '/' ? '/' : '').$url; 310 | if ($this->fileExists($file = $full.'.less') || $this->fileExists($file = $full)) { 311 | return $file; 312 | } 313 | } 314 | 315 | return null; 316 | } 317 | 318 | function fileExists($name) { 319 | // sym link workaround 320 | return file_exists($name) || file_exists(realpath(preg_replace('/\w+\/\.\.\//', '', $name))); 321 | } 322 | 323 | // a list of expressions 324 | function expressionList(&$exps) { 325 | $values = array(); 326 | 327 | while ($this->expression($exp)) { 328 | $values[] = $exp; 329 | } 330 | 331 | if (count($values) == 0) return false; 332 | 333 | $exps = $this->compressList($values, ' '); 334 | return true; 335 | } 336 | 337 | /** 338 | * Attempt to consume an expression. 339 | * @link http://en.wikipedia.org/wiki/Operator-precedence_parser#Pseudo-code 340 | */ 341 | function expression(&$out) { 342 | $s = $this->seek(); 343 | if ($this->literal('(') && ($this->inExp = $this->inParens = true) && $this->expression($exp) && $this->literal(')')) { 344 | $lhs = $exp; 345 | } elseif ($this->seek($s) && $this->value($val)) { 346 | $lhs = $val; 347 | } else { 348 | $this->inParens = $this->inExp = false; 349 | $this->seek($s); 350 | return false; 351 | } 352 | 353 | $out = $this->expHelper($lhs, 0); 354 | $this->inParens = $this->inExp = false; 355 | return true; 356 | } 357 | 358 | /** 359 | * recursively parse infix equation with $lhs at precedence $minP 360 | */ 361 | function expHelper($lhs, $minP) { 362 | $this->inExp = true; 363 | $ss = $this->seek(); 364 | 365 | // if the if there was whitespace before the operator, then we require whitespace after 366 | // the operator for it to be a mathematical operator. 367 | 368 | $needWhite = false; 369 | if (!$this->inParens && preg_match('/\s/', $this->buffer{$this->count - 1})) { 370 | $needWhite = true; 371 | } 372 | 373 | // try to find a valid operator 374 | while ($this->match(self::$operatorString.($needWhite ? '\s' : ''), $m) && self::$precedence[$m[1]] >= $minP) { 375 | // get rhs 376 | $s = $this->seek(); 377 | $p = $this->inParens; 378 | if ($this->literal('(') && ($this->inParens = true) && $this->expression($exp) && $this->literal(')')) { 379 | $this->inParens = $p; 380 | $rhs = $exp; 381 | } else { 382 | $this->inParens = $p; 383 | if ($this->seek($s) && $this->value($val)) { 384 | $rhs = $val; 385 | } else { 386 | break; 387 | } 388 | } 389 | 390 | // peek for next operator to see what to do with rhs 391 | if ($this->peek(self::$operatorString, $next) && self::$precedence[$next[1]] > $minP) { 392 | $rhs = $this->expHelper($rhs, self::$precedence[$next[1]]); 393 | } 394 | 395 | // don't evaluate yet if it is dynamic 396 | if (in_array($rhs[0], self::$dtypes) || in_array($lhs[0], self::$dtypes)) 397 | $lhs = array('expression', $m[1], $lhs, $rhs); 398 | else 399 | $lhs = $this->evaluate($m[1], $lhs, $rhs); 400 | 401 | $ss = $this->seek(); 402 | 403 | $needWhite = false; 404 | if (!$this->inParens && preg_match('/\s/', $this->buffer{$this->count - 1})) { 405 | $needWhite = true; 406 | } 407 | } 408 | $this->seek($ss); 409 | 410 | return $lhs; 411 | } 412 | 413 | // consume a list of values for a property 414 | function propertyValue(&$value) { 415 | $values = array(); 416 | 417 | $s = null; 418 | while ($this->expressionList($v)) { 419 | $values[] = $v; 420 | $s = $this->seek(); 421 | if (!$this->literal(',')) break; 422 | } 423 | 424 | if ($s) $this->seek($s); 425 | 426 | if (count($values) == 0) return false; 427 | 428 | $value = $this->compressList($values, ', '); 429 | return true; 430 | } 431 | 432 | // a single value 433 | function value(&$value) { 434 | // try a unit 435 | if ($this->unit($value)) return true; 436 | 437 | // see if there is a negation 438 | $s = $this->seek(); 439 | if ($this->literal('-', false) && $this->variable($vname)) { 440 | $value = array('negative', array('variable', $this->vPrefix.$vname)); 441 | return true; 442 | } else { 443 | $this->seek($s); 444 | } 445 | 446 | // accessor 447 | // must be done before color 448 | // this needs negation too 449 | if ($this->accessor($a)) { 450 | $a[1] = $this->fixTags($a[1]); 451 | $value = $a; 452 | return true; 453 | } 454 | 455 | // color 456 | if ($this->color($value)) return true; 457 | 458 | // css function 459 | // must be done after color 460 | if ($this->func($value)) return true; 461 | 462 | // string 463 | if ($this->string($tmp, $d)) { 464 | $value = array('string', $d.$tmp.$d); 465 | return true; 466 | } 467 | 468 | // try a keyword 469 | if ($this->keyword($word)) { 470 | $value = array('keyword', $word); 471 | return true; 472 | } 473 | 474 | // try a variable 475 | if ($this->variable($vname)) { 476 | $value = array('variable', $this->vPrefix.$vname); 477 | return true; 478 | } 479 | 480 | // unquote string 481 | if ($this->literal("~") && $this->string($value, $d)) { 482 | $value = array("keyword", $value); 483 | return true; 484 | } else { 485 | $this->seek($s); 486 | } 487 | 488 | // css hack: \0 489 | if ($this->literal('\\') && $this->match('([0-9]+)', $m)) { 490 | $value = array('keyword', '\\'.$m[1]); 491 | return true; 492 | } else { 493 | $this->seek($s); 494 | } 495 | 496 | return false; 497 | } 498 | 499 | // an import statement 500 | function import(&$url, &$media) { 501 | $s = $this->seek(); 502 | if (!$this->literal('@import')) return false; 503 | 504 | // @import "something.css" media; 505 | // @import url("something.css") media; 506 | // @import url(something.css) media; 507 | 508 | if ($this->literal('url(')) $parens = true; else $parens = false; 509 | 510 | if (!$this->string($url)) { 511 | if ($parens && $this->to(')', $url)) { 512 | $parens = false; // got em 513 | } else { 514 | $this->seek($s); 515 | return false; 516 | } 517 | } 518 | 519 | if ($parens && !$this->literal(')')) { 520 | $this->seek($s); 521 | return false; 522 | } 523 | 524 | // now the rest is media 525 | return $this->to(';', $media, false, true); 526 | } 527 | 528 | // a list of media types, very lenient 529 | function mediaTypes(&$types) { 530 | if ($this->to('{', $rest, true, true)) { 531 | $types = trim($rest); 532 | return true; 533 | } 534 | 535 | return false; 536 | } 537 | 538 | // a scoped value accessor 539 | // .hello > @scope1 > @scope2['value']; 540 | function accessor(&$var) { 541 | $s = $this->seek(); 542 | 543 | if (!$this->tags($scope, true, '>') || !$this->literal('[')) { 544 | $this->seek($s); 545 | return false; 546 | } 547 | 548 | // either it is a variable or a property 549 | // why is a property wrapped in quotes, who knows! 550 | if ($this->variable($name)) { 551 | $name = $this->vPrefix.$name; 552 | } elseif ($this->literal("'") && $this->keyword($name) && $this->literal("'")) { 553 | // .. $this->count is messed up if we wanted to test another access type 554 | } else { 555 | $this->seek($s); 556 | return false; 557 | } 558 | 559 | if (!$this->literal(']')) { 560 | $this->seek($s); 561 | return false; 562 | } 563 | 564 | $var = array('lookup', $scope, $name); 565 | return true; 566 | } 567 | 568 | // a string 569 | function string(&$string, &$d = null) { 570 | $s = $this->seek(); 571 | if ($this->literal('"', false)) { 572 | $delim = '"'; 573 | } elseif ($this->literal("'", false)) { 574 | $delim = "'"; 575 | } else { 576 | return false; 577 | } 578 | 579 | if (!$this->to($delim, $string)) { 580 | $this->seek($s); 581 | return false; 582 | } 583 | 584 | $d = $delim; 585 | return true; 586 | } 587 | 588 | /** 589 | * Consume a number and optionally a unit. 590 | * Can also consume a font shorthand if it is a simple case. 591 | * $allowed restricts the types that are matched. 592 | */ 593 | function unit(&$unit, $allowed = null) { 594 | $simpleCase = $allowed == null; 595 | if (!$allowed) $allowed = self::$units; 596 | 597 | if ($this->match('(-?[0-9]*(\.)?[0-9]+)('.implode('|', $allowed).')?', $m, !$simpleCase)) { 598 | if (!isset($m[3])) $m[3] = 'number'; 599 | $unit = array($m[3], $m[1]); 600 | 601 | // check for size/height font unit.. should this even be here? 602 | if ($simpleCase) { 603 | $s = $this->seek(); 604 | if (!$this->inExp && $this->literal('/', false) && $this->unit($right, self::$units)) { 605 | $unit = array('keyword', $this->compileValue($unit).'/'.$this->compileValue($right)); 606 | } else { 607 | // get rid of whitespace 608 | $this->seek($s); 609 | $this->match('', $_); 610 | } 611 | } 612 | 613 | return true; 614 | } 615 | 616 | return false; 617 | } 618 | 619 | // a # color 620 | function color(&$out) { 621 | $color = array('color'); 622 | 623 | if ($this->match('(#([0-9a-f]{6})|#([0-9a-f]{3}))', $m)) { 624 | if (isset($m[3])) { 625 | $num = $m[3]; 626 | $width = 16; 627 | } else { 628 | $num = $m[2]; 629 | $width = 256; 630 | } 631 | 632 | $num = hexdec($num); 633 | foreach (array(3,2,1) as $i) { 634 | $t = $num % $width; 635 | $num /= $width; 636 | 637 | $color[$i] = $t * (256/$width) + $t * floor(16/$width); 638 | } 639 | 640 | $out = $color; 641 | return true; 642 | } 643 | 644 | return false; 645 | } 646 | 647 | // consume a list of property values delimited by ; and wrapped in () 648 | function argumentValues(&$args, $delim = ',') { 649 | $s = $this->seek(); 650 | if (!$this->literal('(')) return false; 651 | 652 | $values = array(); 653 | while (true) { 654 | if ($this->expressionList($value)) $values[] = $value; 655 | if (!$this->literal($delim)) break; 656 | else { 657 | if ($value == null) $values[] = null; 658 | $value = null; 659 | } 660 | } 661 | 662 | if (!$this->literal(')')) { 663 | $this->seek($s); 664 | return false; 665 | } 666 | 667 | $args = $values; 668 | return true; 669 | } 670 | 671 | // consume an argument definition list surrounded by () 672 | // each argument is a variable name with optional value 673 | function argumentDef(&$args, $delim = ',') { 674 | $s = $this->seek(); 675 | if (!$this->literal('(')) return false; 676 | 677 | $values = array(); 678 | while ($this->variable($vname)) { 679 | $arg = array($vname); 680 | if ($this->assign() && $this->expressionList($value)) { 681 | $arg[] = $value; 682 | // let the : slide if there is no value 683 | } 684 | 685 | $values[] = $arg; 686 | if (!$this->literal($delim)) break; 687 | } 688 | 689 | if (!$this->literal(')')) { 690 | $this->seek($s); 691 | return false; 692 | } 693 | 694 | $args = $values; 695 | return true; 696 | } 697 | 698 | // consume a list of tags 699 | // this accepts a hanging delimiter 700 | function tags(&$tags, $simple = false, $delim = ',') { 701 | $tags = array(); 702 | while ($this->tag($tt, $simple)) { 703 | $tags[] = $tt; 704 | if (!$this->literal($delim)) break; 705 | } 706 | if (count($tags) == 0) return false; 707 | 708 | return true; 709 | } 710 | 711 | // list of tags of specifying mixin path 712 | // optionally separated by > (lazy, accepts extra >) 713 | function mixinTags(&$tags) { 714 | $s = $this->seek(); 715 | $tags = array(); 716 | while ($this->tag($tt, true)) { 717 | $tags[] = $tt; 718 | $this->literal(">"); 719 | } 720 | 721 | if (count($tags) == 0) return false; 722 | 723 | return true; 724 | } 725 | 726 | // a bracketed value (contained within in a tag definition) 727 | function tagBracket(&$value) { 728 | $s = $this->seek(); 729 | if ($this->literal('[') && $this->to(']', $c, true) && $this->literal(']', false)) { 730 | $value = '['.$c.']'; 731 | // whitespace? 732 | if ($this->match('', $_)) $value .= $_[0]; 733 | 734 | // escape parent selector 735 | $value = str_replace($this->parentSelector, "&&", $value); 736 | return true; 737 | } 738 | 739 | $this->seek($s); 740 | return false; 741 | } 742 | 743 | // a single tag 744 | function tag(&$tag, $simple = false) { 745 | if ($simple) 746 | $chars = '^,:;{}\][>\(\) "\''; 747 | else 748 | $chars = '^,;{}["\''; 749 | 750 | $tag = ''; 751 | while ($this->tagBracket($first)) $tag .= $first; 752 | while ($this->match('(['.$chars.'0-9]['.$chars.']*)', $m)) { 753 | $tag .= $m[1]; 754 | if ($simple) break; 755 | 756 | while ($this->tagBracket($brack)) $tag .= $brack; 757 | } 758 | $tag = trim($tag); 759 | if ($tag == '') return false; 760 | 761 | return true; 762 | } 763 | 764 | // a css function 765 | function func(&$func) { 766 | $s = $this->seek(); 767 | 768 | if ($this->match('(%|[\w\-_][\w\-_:\.]*)', $m) && $this->literal('(')) { 769 | $fname = $m[1]; 770 | if ($fname == 'url') { 771 | $this->to(')', $content, true); 772 | $args = array('string', $content); 773 | } else { 774 | $args = array(); 775 | while (true) { 776 | $ss = $this->seek(); 777 | if ($this->keyword($name) && $this->literal('=') && $this->expressionList($value)) { 778 | $args[] = array('list', '=', array(array('keyword', $name), $value)); 779 | } else { 780 | $this->seek($ss); 781 | if ($this->expressionList($value)) { 782 | $args[] = $value; 783 | } 784 | } 785 | 786 | if (!$this->literal(',')) break; 787 | } 788 | $args = array('list', ',', $args); 789 | } 790 | 791 | if ($this->literal(')')) { 792 | $func = array('function', $fname, $args); 793 | return true; 794 | } 795 | } 796 | 797 | $this->seek($s); 798 | return false; 799 | } 800 | 801 | // consume a less variable 802 | function variable(&$name) { 803 | $s = $this->seek(); 804 | if ($this->literal($this->vPrefix, false) && $this->keyword($name)) { 805 | return true; 806 | } 807 | 808 | return false; 809 | } 810 | 811 | /** 812 | * Consume an assignment operator 813 | * Can optionally take a name that will be set to the current property name 814 | */ 815 | function assign($name = null) { 816 | if ($name) $this->currentProperty = $name; 817 | return $this->literal(':') || $this->literal('='); 818 | } 819 | 820 | // consume a keyword 821 | function keyword(&$word) { 822 | if ($this->match('([\w_\-\*!"][\w\-_"]*)', $m)) { 823 | $word = $m[1]; 824 | return true; 825 | } 826 | return false; 827 | } 828 | 829 | // consume an end of statement delimiter 830 | function end() { 831 | if ($this->literal(';')) 832 | return true; 833 | elseif ($this->count == strlen($this->buffer) || $this->buffer{$this->count} == '}') { 834 | // if there is end of file or a closing block next then we don't need a ; 835 | return true; 836 | } 837 | return false; 838 | } 839 | 840 | function compressList($items, $delim) { 841 | if (count($items) == 1) return $items[0]; 842 | else return array('list', $delim, $items); 843 | } 844 | 845 | // just do a shallow propety merge, seems to be what lessjs does 846 | function mergeBlock($target, $from) { 847 | $target = clone $target; 848 | $target->props = array_merge($target->props, $from->props); 849 | return $target; 850 | } 851 | 852 | /** 853 | * Recursively compiles a block. 854 | * @param $block the block 855 | * @param $parentTags the tags of the block that contained this one 856 | * 857 | * A block is analogous to a CSS block in most cases. A single less document 858 | * is encapsulated in a block when parsed, but it does not have parent tags 859 | * so all of it's children appear on the root level when compiled. 860 | * 861 | * Blocks are made up of props and children. 862 | * 863 | * Props are property instructions, array tuples which describe an action 864 | * to be taken, eg. write a property, set a variable, mixin a block. 865 | * 866 | * The children of a block are just all the blocks that are defined within. 867 | * 868 | * Compiling the block involves pushing a fresh environment on the stack, 869 | * and iterating through the props, compiling each one. 870 | * 871 | * See lessc::compileProp() 872 | * 873 | */ 874 | function compileBlock($block, $parent_tags = null) { 875 | $isRoot = $parent_tags == null && $block->tags == null; 876 | 877 | $indent = str_repeat($this->indentChar, $this->indentLevel); 878 | 879 | if (!empty($block->no_multiply)) { 880 | $special_block = true; 881 | $this->indentLevel++; 882 | $tags = array(); 883 | } else { 884 | $special_block = false; 885 | $tags = $this->multiplyTags($parent_tags, $block->tags); 886 | } 887 | 888 | $this->pushEnv(); 889 | $lines = array(); 890 | $blocks = array(); 891 | foreach ($block->props as $prop) { 892 | $this->compileProp($prop, $block, $tags, $lines, $blocks); 893 | } 894 | 895 | $this->pop(); 896 | 897 | $nl = $isRoot ? "\n".$indent : 898 | "\n".$indent.$this->indentChar; 899 | 900 | ob_start(); 901 | 902 | if ($special_block) { 903 | $this->indentLevel--; 904 | if (isset($block->media)) { 905 | echo "@media ".$block->media; 906 | } elseif (isset($block->keyframes)) { 907 | echo $block->tags[0]." ". 908 | $this->compileValue($this->reduce($block->keyframes)); 909 | } else { 910 | list($name) = $block->tags; 911 | echo $indent.$name; 912 | } 913 | 914 | echo ' {'.(count($lines) > 0 ? $nl : "\n"); 915 | } 916 | 917 | // dump it 918 | if (count($lines) > 0) { 919 | if (!$special_block && !$isRoot) { 920 | echo $indent.implode(", ", $tags); 921 | if (count($lines) > 1) echo " {".$nl; 922 | else echo " { "; 923 | } 924 | 925 | echo implode($nl, $lines); 926 | 927 | if (!$special_block && !$isRoot) { 928 | if (count($lines) > 1) echo "\n".$indent."}\n"; 929 | else echo " }\n"; 930 | } else echo "\n"; 931 | } 932 | 933 | foreach ($blocks as $b) echo $b; 934 | 935 | if ($special_block) { 936 | echo $indent."}\n"; 937 | } 938 | 939 | return ob_get_clean(); 940 | } 941 | 942 | 943 | // find the fully qualified tags for a block and its parent's tags 944 | function multiplyTags($parents, $current) { 945 | if ($parents == null) return $current; 946 | 947 | $tags = array(); 948 | foreach ($parents as $ptag) { 949 | foreach ($current as $tag) { 950 | // inject parent in place of parent selector, ignoring escaped valuews 951 | $count = 0; 952 | $parts = explode("&&", $tag); 953 | 954 | foreach ($parts as $i => $chunk) { 955 | $parts[$i] = str_replace($this->parentSelector, $ptag, $chunk, $c); 956 | $count += $c; 957 | } 958 | 959 | $tag = implode("&", $parts); 960 | 961 | if ($count > 0) { 962 | $tags[] = trim($tag); 963 | } else { 964 | $tags[] = trim($ptag . ' ' . $tag); 965 | } 966 | } 967 | } 968 | 969 | return $tags; 970 | } 971 | 972 | // attempt to find block pointed at by path within search_in or its parent 973 | function findBlock($search_in, $path, $seen=array()) { 974 | if ($search_in == null) return null; 975 | if (isset($seen[$search_in->id])) return null; 976 | $seen[$search_in->id] = true; 977 | 978 | $name = $path[0]; 979 | 980 | if (isset($search_in->children[$name])) { 981 | $block = $search_in->children[$name]; 982 | if (count($path) == 1) { 983 | return $block; 984 | } else { 985 | return $this->findBlock($block, array_slice($path, 1), $seen); 986 | } 987 | } else { 988 | if ($search_in->parent === $search_in) return null; 989 | return $this->findBlock($search_in->parent, $path, $seen); 990 | } 991 | } 992 | 993 | // sets all argument names in $args to either the default value 994 | // or the one passed in through $values 995 | function zipSetArgs($args, $values) { 996 | $i = 0; 997 | $assigned_values = array(); 998 | foreach ($args as $a) { 999 | if ($i < count($values) && !is_null($values[$i])) { 1000 | $value = $values[$i]; 1001 | } elseif (isset($a[1])) { 1002 | $value = $a[1]; 1003 | } else $value = null; 1004 | 1005 | $value = $this->reduce($value); 1006 | $this->set($this->vPrefix.$a[0], $value); 1007 | $assigned_values[] = $value; 1008 | $i++; 1009 | } 1010 | 1011 | // copy over any extra default args 1012 | for ($i = count($values); $i < count($assigned_values); $i++) { 1013 | $values[] = $assigned_values[$i]; 1014 | } 1015 | 1016 | $this->env->arguments = $values; 1017 | } 1018 | 1019 | // compile a prop and update $lines or $blocks appropriately 1020 | function compileProp($prop, $block, $tags, &$_lines, &$_blocks) { 1021 | switch ($prop[0]) { 1022 | case 'assign': 1023 | list(, $name, $value) = $prop; 1024 | if ($name[0] == $this->vPrefix) { 1025 | $this->set($name, $this->reduce($value)); 1026 | } else { 1027 | $_lines[] = "$name:". 1028 | $this->compileValue($this->reduce($value)).";"; 1029 | } 1030 | break; 1031 | case 'block': 1032 | list(, $child) = $prop; 1033 | $_blocks[] = $this->compileBlock($child, $tags); 1034 | break; 1035 | case 'mixin': 1036 | list(, $path, $args) = $prop; 1037 | 1038 | $mixin = $this->findBlock($block, $path); 1039 | if (is_null($mixin)) { 1040 | // echo "failed to find block: ".implode(" > ", $path)."\n"; 1041 | break; // throw error here?? 1042 | } 1043 | 1044 | $have_args = false; 1045 | if (isset($mixin->args)) { 1046 | $have_args = true; 1047 | $this->pushEnv(); 1048 | $this->zipSetArgs($mixin->args, $args); 1049 | } 1050 | 1051 | $old_parent = $mixin->parent; 1052 | $mixin->parent = $block; 1053 | 1054 | foreach ($mixin->props as $sub_prop) { 1055 | $this->compileProp($sub_prop, $mixin, $tags, $_lines, $_blocks); 1056 | } 1057 | 1058 | $mixin->parent = $old_parent; 1059 | 1060 | if ($have_args) $this->pop(); 1061 | 1062 | break; 1063 | case 'raw': 1064 | $_lines[] = $prop[1]; 1065 | break; 1066 | case 'import': 1067 | list(, $path) = $prop; 1068 | $this->addParsedFile($path); 1069 | $root = $this->createChild($path)->parseTree(); 1070 | 1071 | $root->parent = $block; 1072 | foreach ($root->props as $sub_prop) { 1073 | $this->compileProp($sub_prop, $root, $tags, $_lines, $_blocks); 1074 | } 1075 | 1076 | // inject imported blocks into this block, local will overwrite import 1077 | $block->children = array_merge($root->children, $block->children); 1078 | break; 1079 | case 'charset': 1080 | list(, $value) = $prop; 1081 | $_lines[] = '@charset '.$this->compileValue($this->reduce($value)).';'; 1082 | break; 1083 | default: 1084 | echo "unknown op: {$prop[0]}\n"; 1085 | throw new exception(); 1086 | } 1087 | } 1088 | 1089 | 1090 | /** 1091 | * Compiles a primitive value into a CSS property value. 1092 | * 1093 | * Values in lessphp are typed by being wrapped in arrays, their format is 1094 | * typically: 1095 | * 1096 | * array(type, contents [, additional_contents]*) 1097 | * 1098 | * Will not work on non reduced values (expressions, variables, etc) 1099 | */ 1100 | function compileValue($value) { 1101 | switch ($value[0]) { 1102 | case 'list': 1103 | // [1] - delimiter 1104 | // [2] - array of values 1105 | return implode($value[1], array_map(array($this, 'compileValue'), $value[2])); 1106 | case 'keyword': 1107 | // [1] - the keyword 1108 | case 'number': 1109 | // [1] - the number 1110 | return $value[1]; 1111 | case 'string': 1112 | // [1] - contents of string (includes quotes) 1113 | 1114 | // search for inline variables to replace 1115 | $replace = array(); 1116 | if (preg_match_all('/'.$this->preg_quote($this->vPrefix).'\{([\w-_][0-9\w-_]*)\}/', $value[1], $m)) { 1117 | foreach ($m[1] as $name) { 1118 | if (!isset($replace[$name])) 1119 | $replace[$name] = $this->compileValue($this->reduce(array('variable', $this->vPrefix . $name))); 1120 | } 1121 | } 1122 | 1123 | foreach ($replace as $var=>$val) { 1124 | if ($this->quoted($val)) { 1125 | $val = substr($val, 1, -1); 1126 | } 1127 | $value[1] = str_replace($this->vPrefix. '{'.$var.'}', $val, $value[1]); 1128 | } 1129 | 1130 | return $value[1]; 1131 | case 'color': 1132 | // [1] - red component (either number for a %) 1133 | // [2] - green component 1134 | // [3] - blue component 1135 | // [4] - optional alpha component 1136 | if (count($value) == 5) { // rgba 1137 | return 'rgba('.$value[1].','.$value[2].','.$value[3].','.$value[4].')'; 1138 | } 1139 | return sprintf("#%02x%02x%02x", $value[1], $value[2], $value[3]); 1140 | case 'function': 1141 | // [1] - function name 1142 | // [2] - some value representing arguments 1143 | 1144 | // see if function evaluates to something else 1145 | $value = $this->reduce($value); 1146 | if ($value[0] == 'function') { 1147 | return $value[1].'('.$this->compileValue($value[2]).')'; 1148 | } 1149 | else return $this->compileValue($value); 1150 | default: // assumed to be unit 1151 | return $value[1].$value[0]; 1152 | } 1153 | } 1154 | 1155 | function lib_rgbahex($color) { 1156 | if ($color[0] != 'color') 1157 | throw new exception("color expected for rgbahex"); 1158 | 1159 | return sprintf("#%02x%02x%02x%02x", 1160 | isset($color[4]) ? $color[4]*255 : 0, 1161 | $color[1],$color[2], $color[3]); 1162 | } 1163 | 1164 | // utility func to unquote a string 1165 | function lib_e($arg) { 1166 | switch ($arg[0]) { 1167 | case "list": 1168 | $items = $arg[2]; 1169 | if (isset($items[0])) { 1170 | return $this->lib_e($items[0]); 1171 | } 1172 | return ""; 1173 | case "string": 1174 | $str = $this->compileValue($arg); 1175 | return substr($str, 1, -1); 1176 | default: 1177 | return $this->compileValue($arg); 1178 | } 1179 | } 1180 | 1181 | function lib__sprintf($args) { 1182 | if ($args[0] != "list") return $args; 1183 | $values = $args[2]; 1184 | $source = $this->reduce(array_shift($values)); 1185 | if ($source[0] != "string") { 1186 | return $source; 1187 | } 1188 | 1189 | $str = $source[1]; 1190 | $i = 0; 1191 | if (preg_match_all('/%[dsa]/', $str, $m)) { 1192 | foreach ($m[0] as $match) { 1193 | $val = isset($values[$i]) ? $this->reduce($values[$i]) : array('keyword', ''); 1194 | $i++; 1195 | switch ($match[1]) { 1196 | case "s": 1197 | if ($val[0] == "string") { 1198 | $rep = substr($val[1], 1, -1); 1199 | break; 1200 | } 1201 | default: 1202 | $rep = $this->compileValue($val); 1203 | } 1204 | $str = preg_replace('/'.$this->preg_quote($match).'/', $rep, $str, 1); 1205 | } 1206 | } 1207 | 1208 | return array('string', $str); 1209 | } 1210 | 1211 | function lib_floor($arg) { 1212 | return floor($arg[1]); 1213 | } 1214 | 1215 | function lib_round($arg) { 1216 | return round($arg[1]); 1217 | } 1218 | 1219 | // is a string surrounded in quotes? returns the quoting char if true 1220 | function quoted($s) { 1221 | if (preg_match('/^("|\').*?\1$/', $s, $m)) 1222 | return $m[1]; 1223 | else return false; 1224 | } 1225 | 1226 | /** 1227 | * Helper function to get argurments for color functions 1228 | * accepts invalid input, non colors interpreted to black 1229 | */ 1230 | function colorArgs($args) { 1231 | if ($args[0] != 'list' || count($args[2]) < 2) { 1232 | return array(array('color', 0, 0, 0)); 1233 | } 1234 | list($color, $delta) = $args[2]; 1235 | $color = $this->coerceColor($color); 1236 | if (is_null($color)) 1237 | $color = array('color', 0, 0, 0); 1238 | 1239 | $delta = floatval($delta[1]); 1240 | 1241 | return array($color, $delta); 1242 | } 1243 | 1244 | function lib_darken($args) { 1245 | list($color, $delta) = $this->colorArgs($args); 1246 | 1247 | $hsl = $this->toHSL($color); 1248 | $hsl[3] = $this->clamp($hsl[3] - $delta, 100); 1249 | return $this->toRGB($hsl); 1250 | } 1251 | 1252 | function lib_lighten($args) { 1253 | list($color, $delta) = $this->colorArgs($args); 1254 | 1255 | $hsl = $this->toHSL($color); 1256 | $hsl[3] = $this->clamp($hsl[3] + $delta, 100); 1257 | return $this->toRGB($hsl); 1258 | } 1259 | 1260 | function lib_saturate($args) { 1261 | list($color, $delta) = $this->colorArgs($args); 1262 | 1263 | $hsl = $this->toHSL($color); 1264 | $hsl[2] = $this->clamp($hsl[2] + $delta, 100); 1265 | return $this->toRGB($hsl); 1266 | } 1267 | 1268 | function lib_desaturate($args) { 1269 | list($color, $delta) = $this->colorArgs($args); 1270 | 1271 | $hsl = $this->toHSL($color); 1272 | $hsl[2] = $this->clamp($hsl[2] - $delta, 100); 1273 | return $this->toRGB($hsl); 1274 | } 1275 | 1276 | function lib_spin($args) { 1277 | list($color, $delta) = $this->colorArgs($args); 1278 | 1279 | $hsl = $this->toHSL($color); 1280 | $hsl[1] = $this->clamp($hsl[1] + $delta, 360); 1281 | return $this->toRGB($hsl); 1282 | } 1283 | 1284 | function lib_fadeout($args) { 1285 | list($color, $delta) = $this->colorArgs($args); 1286 | $color[4] = $this->clamp((isset($color[4]) ? $color[4] : 1) - $delta/100); 1287 | return $color; 1288 | } 1289 | 1290 | function lib_fadein($args) { 1291 | list($color, $delta) = $this->colorArgs($args); 1292 | $color[4] = $this->clamp((isset($color[4]) ? $color[4] : 1) + $delta/100); 1293 | return $color; 1294 | } 1295 | 1296 | function lib_hue($color) { 1297 | if ($color[0] != 'color') return 0; 1298 | $hsl = $this->toHSL($color); 1299 | return round($hsl[1]); 1300 | } 1301 | 1302 | function lib_saturation($color) { 1303 | if ($color[0] != 'color') return 0; 1304 | $hsl = $this->toHSL($color); 1305 | return round($hsl[2]); 1306 | } 1307 | 1308 | function lib_lightness($color) { 1309 | if ($color[0] != 'color') return 0; 1310 | $hsl = $this->toHSL($color); 1311 | return round($hsl[3]); 1312 | } 1313 | 1314 | // get the alpha of a color 1315 | // defaults to 1 for non-colors or colors without an alpha 1316 | function lib_alpha($color) { 1317 | if ($color[0] != 'color') return 1; 1318 | return isset($color[4]) ? $color[4] : 1; 1319 | } 1320 | 1321 | // set the alpha of the color 1322 | function lib_fade($args) { 1323 | list($color, $alpha) = $this->colorArgs($args); 1324 | $color[4] = $this->clamp($alpha / 100.0); 1325 | return $color; 1326 | } 1327 | 1328 | function lib_percentage($number) { 1329 | return array('%', $number[1]*100); 1330 | } 1331 | 1332 | // mixes two colors by weight 1333 | // mix(@color1, @color2, @weight); 1334 | // http://sass-lang.com/docs/yardoc/Sass/Script/Functions.html#mix-instance_method 1335 | function lib_mix($args) { 1336 | if ($args[0] != "list") 1337 | throw new exception("mix expects (color1, color2, weight)"); 1338 | 1339 | list($first, $second, $weight) = $args[2]; 1340 | $first = $this->assertColor($first); 1341 | $second = $this->assertColor($second); 1342 | 1343 | $first_a = $this->lib_alpha($first); 1344 | $second_a = $this->lib_alpha($second); 1345 | $weight = $weight[1] / 100.0; 1346 | 1347 | $w = $weight * 2 - 1; 1348 | $a = $first_a - $second_a; 1349 | 1350 | $w1 = (($w * $a == -1 ? $w : ($w + $a)/(1 + $w * $a)) + 1) / 2.0; 1351 | $w2 = 1.0 - $w1; 1352 | 1353 | $new = array('color', 1354 | $w1 * $first[1] + $w2 * $second[1], 1355 | $w1 * $first[2] + $w2 * $second[2], 1356 | $w1 * $first[3] + $w2 * $second[3], 1357 | ); 1358 | 1359 | if ($first_a != 1.0 || $second_a != 1.0) { 1360 | $new[] = $first_a * $p + $second_a * ($p - 1); 1361 | } 1362 | 1363 | return $this->fixColor($new); 1364 | } 1365 | 1366 | function assertColor($value, $error = "expected color value") { 1367 | $color = $this->coerceColor($value); 1368 | if (is_null($color)) throw new exception($error); 1369 | return $color; 1370 | } 1371 | 1372 | function toHSL($color) { 1373 | if ($color[0] == 'hsl') return $color; 1374 | 1375 | $r = $color[1] / 255; 1376 | $g = $color[2] / 255; 1377 | $b = $color[3] / 255; 1378 | 1379 | $min = min($r, $g, $b); 1380 | $max = max($r, $g, $b); 1381 | 1382 | $L = ($min + $max) / 2; 1383 | if ($min == $max) { 1384 | $S = $H = 0; 1385 | } else { 1386 | if ($L < 0.5) 1387 | $S = ($max - $min)/($max + $min); 1388 | else 1389 | $S = ($max - $min)/(2.0 - $max - $min); 1390 | 1391 | if ($r == $max) $H = ($g - $b)/($max - $min); 1392 | elseif ($g == $max) $H = 2.0 + ($b - $r)/($max - $min); 1393 | elseif ($b == $max) $H = 4.0 + ($r - $g)/($max - $min); 1394 | 1395 | } 1396 | 1397 | $out = array('hsl', 1398 | ($H < 0 ? $H + 6 : $H)*60, 1399 | $S*100, 1400 | $L*100, 1401 | ); 1402 | 1403 | if (count($color) > 4) $out[] = $color[4]; // copy alpha 1404 | return $out; 1405 | } 1406 | 1407 | function toRGB_helper($comp, $temp1, $temp2) { 1408 | if ($comp < 0) $comp += 1.0; 1409 | elseif ($comp > 1) $comp -= 1.0; 1410 | 1411 | if (6 * $comp < 1) return $temp1 + ($temp2 - $temp1) * 6 * $comp; 1412 | if (2 * $comp < 1) return $temp2; 1413 | if (3 * $comp < 2) return $temp1 + ($temp2 - $temp1)*((2/3) - $comp) * 6; 1414 | 1415 | return $temp1; 1416 | } 1417 | 1418 | /** 1419 | * Converts an hsl array into a color value in rgb. 1420 | * Expects H to be in range of 0 to 360, S and L in 0 to 100 1421 | */ 1422 | function toRGB($color) { 1423 | if ($color == 'color') return $color; 1424 | 1425 | $H = $color[1] / 360; 1426 | $S = $color[2] / 100; 1427 | $L = $color[3] / 100; 1428 | 1429 | if ($S == 0) { 1430 | $r = $g = $b = $L; 1431 | } else { 1432 | $temp2 = $L < 0.5 ? 1433 | $L*(1.0 + $S) : 1434 | $L + $S - $L * $S; 1435 | 1436 | $temp1 = 2.0 * $L - $temp2; 1437 | 1438 | $r = $this->toRGB_helper($H + 1/3, $temp1, $temp2); 1439 | $g = $this->toRGB_helper($H, $temp1, $temp2); 1440 | $b = $this->toRGB_helper($H - 1/3, $temp1, $temp2); 1441 | } 1442 | 1443 | $out = array('color', round($r*255), round($g*255), round($b*255)); 1444 | if (count($color) > 4) $out[] = $color[4]; // copy alpha 1445 | return $out; 1446 | } 1447 | 1448 | function clamp($v, $max = 1, $min = 0) { 1449 | return min($max, max($min, $v)); 1450 | } 1451 | 1452 | /** 1453 | * Convert the rgb, rgba, hsl color literals of function type 1454 | * as returned by the parser into values of color type. 1455 | */ 1456 | function funcToColor($func) { 1457 | $fname = $func[1]; 1458 | if ($func[2][0] != 'list') return false; // need a list of arguments 1459 | $rawComponents = $func[2][2]; 1460 | 1461 | if ($fname == 'hsl' || $fname == 'hsla') { 1462 | $hsl = array('hsl'); 1463 | $i = 0; 1464 | foreach ($rawComponents as $c) { 1465 | $val = $this->reduce($c); 1466 | $val = isset($val[1]) ? floatval($val[1]) : 0; 1467 | 1468 | if ($i == 0) $clamp = 360; 1469 | elseif ($i < 4) $clamp = 100; 1470 | else $clamp = 1; 1471 | 1472 | $hsl[] = $this->clamp($val, $clamp); 1473 | $i++; 1474 | } 1475 | 1476 | while (count($hsl) < 4) $hsl[] = 0; 1477 | return $this->toRGB($hsl); 1478 | 1479 | } elseif ($fname == 'rgb' || $fname == 'rgba') { 1480 | $components = array(); 1481 | $i = 1; 1482 | foreach ($rawComponents as $c) { 1483 | $c = $this->reduce($c); 1484 | if ($i < 4) { 1485 | if ($c[0] == '%') $components[] = 255 * ($c[1] / 100); 1486 | else $components[] = floatval($c[1]); 1487 | } elseif ($i == 4) { 1488 | if ($c[0] == '%') $components[] = 1.0 * ($c[1] / 100); 1489 | else $components[] = floatval($c[1]); 1490 | } else break; 1491 | 1492 | $i++; 1493 | } 1494 | while (count($components) < 3) $components[] = 0; 1495 | array_unshift($components, 'color'); 1496 | return $this->fixColor($components); 1497 | } 1498 | 1499 | return false; 1500 | } 1501 | 1502 | // reduce a delayed type to its final value 1503 | // dereference variables and solve equations 1504 | function reduce($var, $defaultValue = array('number', 0)) { 1505 | while (in_array($var[0], self::$dtypes)) { 1506 | if ($var[0] == 'list') { 1507 | foreach ($var[2] as &$value) $value = $this->reduce($value); 1508 | break; 1509 | } elseif ($var[0] == 'expression') { 1510 | $var = $this->evaluate($var[1], $var[2], $var[3]); 1511 | } elseif ($var[0] == 'variable') { 1512 | $var = $this->get($var[1]); 1513 | } elseif ($var[0] == 'lookup') { 1514 | // do accessor here.... 1515 | $var = array('number', 0); 1516 | } elseif ($var[0] == 'function') { 1517 | $color = $this->funcToColor($var); 1518 | if ($color) $var = $color; 1519 | else { 1520 | list($_, $name, $args) = $var; 1521 | if ($name == "%") $name = "_sprintf"; 1522 | $f = isset($this->libFunctions[$name]) ? 1523 | $this->libFunctions[$name] : array($this, 'lib_'.$name); 1524 | 1525 | if (is_callable($f)) { 1526 | if ($args[0] == 'list') 1527 | $args = $this->compressList($args[2], $args[1]); 1528 | 1529 | $var = call_user_func($f, $this->reduce($args)); 1530 | 1531 | // convet to a typed value if the result is a php primitive 1532 | if (is_numeric($var)) $var = array('number', $var); 1533 | elseif (!is_array($var)) $var = array('keyword', $var); 1534 | } else { 1535 | // plain function, reduce args 1536 | $var[2] = $this->reduce($var[2]); 1537 | } 1538 | } 1539 | break; // done reducing after a function 1540 | } elseif ($var[0] == 'negative') { 1541 | $value = $this->reduce($var[1]); 1542 | if (is_numeric($value[1])) { 1543 | $value[1] = -1*$value[1]; 1544 | } 1545 | $var = $value; 1546 | } 1547 | } 1548 | 1549 | return $var; 1550 | } 1551 | 1552 | function coerceColor($value) { 1553 | switch($value[0]) { 1554 | case 'color': return $value; 1555 | case 'keyword': 1556 | $name = $value[1]; 1557 | if (isset(self::$cssColors[$name])) { 1558 | list($r, $g, $b) = explode(',', self::$cssColors[$name]); 1559 | return array('color', $r, $g, $b); 1560 | } 1561 | return null; 1562 | } 1563 | } 1564 | 1565 | // evaluate an expression 1566 | function evaluate($op, $left, $right) { 1567 | $left = $this->reduce($left); 1568 | $right = $this->reduce($right); 1569 | 1570 | if ($left_color = $this->coerceColor($left)) { 1571 | $left = $left_color; 1572 | } 1573 | 1574 | if ($right_color = $this->coerceColor($right)) { 1575 | $right = $right_color; 1576 | } 1577 | 1578 | if ($left[0] == 'color' && $right[0] == 'color') { 1579 | $out = $this->op_color_color($op, $left, $right); 1580 | return $out; 1581 | } 1582 | 1583 | if ($left[0] == 'color') { 1584 | return $this->op_color_number($op, $left, $right); 1585 | } 1586 | 1587 | if ($right[0] == 'color') { 1588 | return $this->op_number_color($op, $left, $right); 1589 | } 1590 | 1591 | // concatenate strings 1592 | if ($op == '+' && $left[0] == 'string') { 1593 | $append = $this->compileValue($right); 1594 | if ($this->quoted($append)) $append = substr($append, 1, -1); 1595 | 1596 | $lhs = $this->compileValue($left); 1597 | if ($q = $this->quoted($lhs)) $lhs = substr($lhs, 1, -1); 1598 | if (!$q) $q = ''; 1599 | 1600 | return array('string', $q.$lhs.$append.$q); 1601 | } 1602 | 1603 | if ($left[0] == 'keyword' || $right[0] == 'keyword' || 1604 | $left[0] == 'string' || $right[0] == 'string') 1605 | { 1606 | // look for negative op 1607 | if ($op == '-') $right[1] = '-'.$right[1]; 1608 | return array('keyword', $this->compileValue($left) .' '. $this->compileValue($right)); 1609 | } 1610 | 1611 | // default to number operation 1612 | return $this->op_number_number($op, $left, $right); 1613 | } 1614 | 1615 | // make sure a color's components don't go out of bounds 1616 | function fixColor($c) { 1617 | foreach (range(1, 3) as $i) { 1618 | if ($c[$i] < 0) $c[$i] = 0; 1619 | if ($c[$i] > 255) $c[$i] = 255; 1620 | $c[$i] = floor($c[$i]); 1621 | } 1622 | 1623 | return $c; 1624 | } 1625 | 1626 | function op_number_color($op, $lft, $rgt) { 1627 | if ($op == '+' || $op = '*') { 1628 | return $this->op_color_number($op, $rgt, $lft); 1629 | } 1630 | } 1631 | 1632 | function op_color_number($op, $lft, $rgt) { 1633 | if ($rgt[0] == '%') $rgt[1] /= 100; 1634 | 1635 | return $this->op_color_color($op, $lft, 1636 | array_fill(1, count($lft) - 1, $rgt[1])); 1637 | } 1638 | 1639 | function op_color_color($op, $left, $right) { 1640 | $out = array('color'); 1641 | $max = count($left) > count($right) ? count($left) : count($right); 1642 | foreach (range(1, $max - 1) as $i) { 1643 | $lval = isset($left[$i]) ? $left[$i] : 0; 1644 | $rval = isset($right[$i]) ? $right[$i] : 0; 1645 | switch ($op) { 1646 | case '+': 1647 | $out[] = $lval + $rval; 1648 | break; 1649 | case '-': 1650 | $out[] = $lval - $rval; 1651 | break; 1652 | case '*': 1653 | $out[] = $lval * $rval; 1654 | break; 1655 | case '%': 1656 | $out[] = $lval % $rval; 1657 | break; 1658 | case '/': 1659 | if ($rval == 0) throw new exception("evaluate error: can't divide by zero"); 1660 | $out[] = $lval / $rval; 1661 | break; 1662 | default: 1663 | throw new exception('evaluate error: color op number failed on op '.$op); 1664 | } 1665 | } 1666 | return $this->fixColor($out); 1667 | } 1668 | 1669 | // operator on two numbers 1670 | function op_number_number($op, $left, $right) { 1671 | if ($right[0] == '%') $right[1] /= 100; 1672 | 1673 | // figure out type 1674 | if ($right[0] == 'number' || $right[0] == '%') $type = $left[0]; 1675 | else $type = $right[0]; 1676 | 1677 | $value = 0; 1678 | switch ($op) { 1679 | case '+': 1680 | $value = $left[1] + $right[1]; 1681 | break; 1682 | case '*': 1683 | $value = $left[1] * $right[1]; 1684 | break; 1685 | case '-': 1686 | $value = $left[1] - $right[1]; 1687 | break; 1688 | case '%': 1689 | $value = $left[1] % $right[1]; 1690 | break; 1691 | case '/': 1692 | if ($right[1] == 0) throw new exception('parse error: divide by zero'); 1693 | $value = $left[1] / $right[1]; 1694 | break; 1695 | default: 1696 | throw new exception('parse error: unknown number operator: '.$op); 1697 | } 1698 | 1699 | return array($type, $value); 1700 | } 1701 | 1702 | 1703 | /* environment functions */ 1704 | 1705 | // push a new block on the stack, used for parsing 1706 | function pushBlock($tags) { 1707 | $b = new stdclass; 1708 | $b->parent = $this->env; 1709 | 1710 | $b->id = self::$nextBlockId++; 1711 | $b->tags = $tags; 1712 | $b->props = array(); 1713 | $b->children = array(); 1714 | 1715 | $this->env = $b; 1716 | return $b; 1717 | } 1718 | 1719 | // push a block that doesn't multiply tags 1720 | function pushSpecialBlock($name) { 1721 | $b = $this->pushBlock(array($name)); 1722 | $b->no_multiply = true; 1723 | return $b; 1724 | } 1725 | 1726 | // used for compiliation variable state 1727 | function pushEnv() { 1728 | $e = new stdclass; 1729 | $e->parent = $this->env; 1730 | 1731 | $this->store = array(); 1732 | 1733 | $this->env = $e; 1734 | return $e; 1735 | } 1736 | 1737 | // pop something off the stack 1738 | function pop() { 1739 | $old = $this->env; 1740 | $this->env = $this->env->parent; 1741 | return $old; 1742 | } 1743 | 1744 | // set something in the current env 1745 | function set($name, $value) { 1746 | $this->env->store[$name] = $value; 1747 | } 1748 | 1749 | // append an property 1750 | function append($prop) { 1751 | $this->env->props[] = $prop; 1752 | } 1753 | 1754 | // get the highest occurrence entry for a name 1755 | function get($name) { 1756 | $current = $this->env; 1757 | 1758 | $is_arguments = $name == $this->vPrefix . 'arguments'; 1759 | while ($current) { 1760 | if ($is_arguments && isset($current->arguments)) { 1761 | return array('list', ' ', $current->arguments); 1762 | } 1763 | 1764 | if (isset($current->store[$name])) 1765 | return $current->store[$name]; 1766 | else 1767 | $current = $current->parent; 1768 | } 1769 | 1770 | return null; 1771 | } 1772 | 1773 | /* raw parsing functions */ 1774 | 1775 | function literal($what, $eatWhitespace = true) { 1776 | // this is here mainly prevent notice from { } string accessor 1777 | if ($this->count >= strlen($this->buffer)) return false; 1778 | 1779 | // shortcut on single letter 1780 | if (!$eatWhitespace && strlen($what) == 1) { 1781 | if ($this->buffer{$this->count} == $what) { 1782 | $this->count++; 1783 | return true; 1784 | } 1785 | else return false; 1786 | } 1787 | 1788 | return $this->match($this->preg_quote($what), $m, $eatWhitespace); 1789 | } 1790 | 1791 | function preg_quote($what) { 1792 | return preg_quote($what, '/'); 1793 | } 1794 | 1795 | // advance counter to next occurrence of $what 1796 | // $until - don't include $what in advance 1797 | function to($what, &$out, $until = false, $allowNewline = false) { 1798 | $validChars = $allowNewline ? "." : "[^\n]"; 1799 | if (!$this->match('('.$validChars.'*?)'.$this->preg_quote($what), $m, !$until)) return false; 1800 | if ($until) $this->count -= strlen($what); // give back $what 1801 | $out = $m[1]; 1802 | return true; 1803 | } 1804 | 1805 | // try to match something on head of buffer 1806 | function match($regex, &$out, $eatWhitespace = true) { 1807 | $r = '/'.$regex.($eatWhitespace ? '\s*' : '').'/Ais'; 1808 | if (preg_match($r, $this->buffer, $out, null, $this->count)) { 1809 | $this->count += strlen($out[0]); 1810 | return true; 1811 | } 1812 | return false; 1813 | } 1814 | 1815 | // match something without consuming it 1816 | function peek($regex, &$out = null) { 1817 | $r = '/'.$regex.'/Ais'; 1818 | $result = preg_match($r, $this->buffer, $out, null, $this->count); 1819 | 1820 | return $result; 1821 | } 1822 | 1823 | // seek to a spot in the buffer or return where we are on no argument 1824 | function seek($where = null) { 1825 | if ($where === null) return $this->count; 1826 | else $this->count = $where; 1827 | return true; 1828 | } 1829 | 1830 | /** 1831 | * Initialize state for a fresh parse 1832 | */ 1833 | protected function prepareParser($buff) { 1834 | $this->env = null; 1835 | $this->expandStack = array(); 1836 | $this->indentLevel = 0; 1837 | $this->count = 0; 1838 | $this->line = 1; 1839 | 1840 | $this->buffer = $this->removeComments($buff); 1841 | $this->pushBlock(null); // set up global scope 1842 | 1843 | // trim whitespace on head 1844 | if (preg_match('/^\s+/', $this->buffer, $m)) { 1845 | $this->line += substr_count($m[0], "\n"); 1846 | $this->buffer = ltrim($this->buffer); 1847 | } 1848 | } 1849 | 1850 | // create a child parser (for compiling an import) 1851 | protected function createChild($fname) { 1852 | $less = new lessc($fname); 1853 | $less->importDir = $this->importDir; 1854 | $less->indentChar = $this->indentChar; 1855 | $less->compat = $this->compat; 1856 | return $less; 1857 | } 1858 | 1859 | // parse code and return intermediate tree 1860 | public function parseTree($str = null) { 1861 | $this->prepareParser(is_null($str) ? $this->buffer : $str); 1862 | while (false !== $this->parseChunk()); 1863 | 1864 | if ($this->count != strlen($this->buffer)) 1865 | $this->throwParseError(); 1866 | 1867 | if (!is_null($this->env->parent)) 1868 | throw new exception('parse error: unclosed block'); 1869 | 1870 | $root = $this->env; 1871 | $this->env = null; 1872 | return $root; 1873 | } 1874 | 1875 | // inject array of unparsed strings into environment as variables 1876 | protected function injectVariables($args) { 1877 | $this->pushEnv(); 1878 | $parser = new lessc(); 1879 | foreach ($args as $name => $str_value) { 1880 | if ($name{0} != '@') $name = '@'.$name; 1881 | $parser->count = 0; 1882 | $parser->buffer = (string)$str_value; 1883 | if (!$parser->propertyValue($value)) { 1884 | throw new Exception("failed to parse passed in variable $name: $str_value"); 1885 | } 1886 | 1887 | $this->set($name, $value); 1888 | } 1889 | } 1890 | 1891 | // parse and compile buffer 1892 | function parse($str = null, $initial_variables = null) { 1893 | $locale = setlocale(LC_NUMERIC, 0); 1894 | setlocale(LC_NUMERIC, "C"); 1895 | $root = $this->parseTree($str); 1896 | 1897 | if ($initial_variables) $this->injectVariables($initial_variables); 1898 | $out = $this->compileBlock($root); 1899 | setlocale(LC_NUMERIC, $locale); 1900 | return $out; 1901 | } 1902 | 1903 | function throwParseError($msg = 'parse error') { 1904 | $line = $this->line + substr_count(substr($this->buffer, 0, $this->count), "\n"); 1905 | if (isset($this->fileName)) { 1906 | $loc = $this->fileName.' on line '.$line; 1907 | } else { 1908 | $loc = "line: ".$line; 1909 | } 1910 | 1911 | if ($this->peek("(.*?)(\n|$)", $m)) 1912 | throw new exception($msg.': failed at `'.$m[1].'` '.$loc); 1913 | } 1914 | 1915 | /** 1916 | * Initialize any static state, can initialize parser for a file 1917 | */ 1918 | function __construct($fname = null, $opts = null) { 1919 | if (!self::$operatorString) { 1920 | self::$operatorString = 1921 | '('.implode('|', array_map(array($this, 'preg_quote'), 1922 | array_keys(self::$precedence))).')'; 1923 | } 1924 | 1925 | if ($fname) { 1926 | if (!is_file($fname)) { 1927 | throw new Exception('load error: failed to find '.$fname); 1928 | } 1929 | $pi = pathinfo($fname); 1930 | 1931 | $this->fileName = $fname; 1932 | $this->importDir = $pi['dirname'].'/'; 1933 | $this->buffer = file_get_contents($fname); 1934 | 1935 | $this->addParsedFile($fname); 1936 | } 1937 | } 1938 | 1939 | public function registerFunction($name, $func) { 1940 | $this->libFunctions[$name] = $func; 1941 | } 1942 | 1943 | public function unregisterFunction($name) { 1944 | unset($this->libFunctions[$name]); 1945 | } 1946 | 1947 | // remove comments from $text 1948 | // todo: make it work for all functions, not just url 1949 | function removeComments($text) { 1950 | $look = array( 1951 | 'url(', '//', '/*', '"', "'" 1952 | ); 1953 | 1954 | $out = ''; 1955 | $min = null; 1956 | $done = false; 1957 | while (true) { 1958 | // find the next item 1959 | foreach ($look as $token) { 1960 | $pos = strpos($text, $token); 1961 | if ($pos !== false) { 1962 | if (!isset($min) || $pos < $min[1]) $min = array($token, $pos); 1963 | } 1964 | } 1965 | 1966 | if (is_null($min)) break; 1967 | 1968 | $count = $min[1]; 1969 | $skip = 0; 1970 | $newlines = 0; 1971 | switch ($min[0]) { 1972 | case 'url(': 1973 | if (preg_match('/url\(.*?\)/', $text, $m, 0, $count)) 1974 | $count += strlen($m[0]) - strlen($min[0]); 1975 | break; 1976 | case '"': 1977 | case "'": 1978 | if (preg_match('/'.$min[0].'.*?'.$min[0].'/', $text, $m, 0, $count)) 1979 | $count += strlen($m[0]) - 1; 1980 | break; 1981 | case '//': 1982 | $skip = strpos($text, "\n", $count); 1983 | if ($skip === false) $skip = strlen($text) - $count; 1984 | else $skip -= $count; 1985 | break; 1986 | case '/*': 1987 | if (preg_match('/\/\*.*?\*\//s', $text, $m, 0, $count)) { 1988 | $skip = strlen($m[0]); 1989 | $newlines = substr_count($m[0], "\n"); 1990 | } 1991 | break; 1992 | } 1993 | 1994 | if ($skip == 0) $count += strlen($min[0]); 1995 | 1996 | $out .= substr($text, 0, $count).str_repeat("\n", $newlines); 1997 | $text = substr($text, $count + $skip); 1998 | 1999 | $min = null; 2000 | } 2001 | 2002 | return $out.$text; 2003 | } 2004 | 2005 | public function allParsedFiles() { return $this->allParsedFiles; } 2006 | protected function addParsedFile($file) { 2007 | $this->allParsedFiles[realpath($file)] = filemtime($file); 2008 | } 2009 | 2010 | 2011 | // compile to $in to $out if $in is newer than $out 2012 | // returns true when it compiles, false otherwise 2013 | public static function ccompile($in, $out) { 2014 | if (!is_file($out) || filemtime($in) > filemtime($out)) { 2015 | $less = new lessc($in); 2016 | file_put_contents($out, $less->parse()); 2017 | return true; 2018 | } 2019 | 2020 | return false; 2021 | } 2022 | 2023 | /** 2024 | * Execute lessphp on a .less file or a lessphp cache structure 2025 | * 2026 | * The lessphp cache structure contains information about a specific 2027 | * less file having been parsed. It can be used as a hint for future 2028 | * calls to determine whether or not a rebuild is required. 2029 | * 2030 | * The cache structure contains two important keys that may be used 2031 | * externally: 2032 | * 2033 | * compiled: The final compiled CSS 2034 | * updated: The time (in seconds) the CSS was last compiled 2035 | * 2036 | * The cache structure is a plain-ol' PHP associative array and can 2037 | * be serialized and unserialized without a hitch. 2038 | * 2039 | * @param mixed $in Input 2040 | * @param bool $force Force rebuild? 2041 | * @return array lessphp cache structure 2042 | */ 2043 | public static function cexecute($in, $force = false) { 2044 | 2045 | // assume no root 2046 | $root = null; 2047 | 2048 | if (is_string($in)) { 2049 | $root = $in; 2050 | } elseif (is_array($in) and isset($in['root'])) { 2051 | if ($force or ! isset($in['files'])) { 2052 | // If we are forcing a recompile or if for some reason the 2053 | // structure does not contain any file information we should 2054 | // specify the root to trigger a rebuild. 2055 | $root = $in['root']; 2056 | } elseif (isset($in['files']) and is_array($in['files'])) { 2057 | foreach ($in['files'] as $fname => $ftime ) { 2058 | if (!file_exists($fname) or filemtime($fname) > $ftime) { 2059 | // One of the files we knew about previously has changed 2060 | // so we should look at our incoming root again. 2061 | $root = $in['root']; 2062 | break; 2063 | } 2064 | } 2065 | } 2066 | } else { 2067 | // TODO: Throw an exception? We got neither a string nor something 2068 | // that looks like a compatible lessphp cache structure. 2069 | return null; 2070 | } 2071 | 2072 | if ($root !== null) { 2073 | // If we have a root value which means we should rebuild. 2074 | $less = new lessc($root); 2075 | $out = array(); 2076 | $out['root'] = $root; 2077 | $out['compiled'] = $less->parse(); 2078 | $out['files'] = $less->allParsedFiles(); 2079 | $out['updated'] = time(); 2080 | return $out; 2081 | } else { 2082 | // No changes, pass back the structure 2083 | // we were given initially. 2084 | return $in; 2085 | } 2086 | 2087 | } 2088 | 2089 | static protected $cssColors = array( 2090 | 'aliceblue' => '240,248,255', 2091 | 'antiquewhite' => '250,235,215', 2092 | 'aqua' => '0,255,255', 2093 | 'aquamarine' => '127,255,212', 2094 | 'azure' => '240,255,255', 2095 | 'beige' => '245,245,220', 2096 | 'bisque' => '255,228,196', 2097 | 'black' => '0,0,0', 2098 | 'blanchedalmond' => '255,235,205', 2099 | 'blue' => '0,0,255', 2100 | 'blueviolet' => '138,43,226', 2101 | 'brown' => '165,42,42', 2102 | 'burlywood' => '222,184,135', 2103 | 'cadetblue' => '95,158,160', 2104 | 'chartreuse' => '127,255,0', 2105 | 'chocolate' => '210,105,30', 2106 | 'coral' => '255,127,80', 2107 | 'cornflowerblue' => '100,149,237', 2108 | 'cornsilk' => '255,248,220', 2109 | 'crimson' => '220,20,60', 2110 | 'cyan' => '0,255,255', 2111 | 'darkblue' => '0,0,139', 2112 | 'darkcyan' => '0,139,139', 2113 | 'darkgoldenrod' => '184,134,11', 2114 | 'darkgray' => '169,169,169', 2115 | 'darkgreen' => '0,100,0', 2116 | 'darkgrey' => '169,169,169', 2117 | 'darkkhaki' => '189,183,107', 2118 | 'darkmagenta' => '139,0,139', 2119 | 'darkolivegreen' => '85,107,47', 2120 | 'darkorange' => '255,140,0', 2121 | 'darkorchid' => '153,50,204', 2122 | 'darkred' => '139,0,0', 2123 | 'darksalmon' => '233,150,122', 2124 | 'darkseagreen' => '143,188,143', 2125 | 'darkslateblue' => '72,61,139', 2126 | 'darkslategray' => '47,79,79', 2127 | 'darkslategrey' => '47,79,79', 2128 | 'darkturquoise' => '0,206,209', 2129 | 'darkviolet' => '148,0,211', 2130 | 'deeppink' => '255,20,147', 2131 | 'deepskyblue' => '0,191,255', 2132 | 'dimgray' => '105,105,105', 2133 | 'dimgrey' => '105,105,105', 2134 | 'dodgerblue' => '30,144,255', 2135 | 'firebrick' => '178,34,34', 2136 | 'floralwhite' => '255,250,240', 2137 | 'forestgreen' => '34,139,34', 2138 | 'fuchsia' => '255,0,255', 2139 | 'gainsboro' => '220,220,220', 2140 | 'ghostwhite' => '248,248,255', 2141 | 'gold' => '255,215,0', 2142 | 'goldenrod' => '218,165,32', 2143 | 'gray' => '128,128,128', 2144 | 'green' => '0,128,0', 2145 | 'greenyellow' => '173,255,47', 2146 | 'grey' => '128,128,128', 2147 | 'honeydew' => '240,255,240', 2148 | 'hotpink' => '255,105,180', 2149 | 'indianred' => '205,92,92', 2150 | 'indigo' => '75,0,130', 2151 | 'ivory' => '255,255,240', 2152 | 'khaki' => '240,230,140', 2153 | 'lavender' => '230,230,250', 2154 | 'lavenderblush' => '255,240,245', 2155 | 'lawngreen' => '124,252,0', 2156 | 'lemonchiffon' => '255,250,205', 2157 | 'lightblue' => '173,216,230', 2158 | 'lightcoral' => '240,128,128', 2159 | 'lightcyan' => '224,255,255', 2160 | 'lightgoldenrodyellow' => '250,250,210', 2161 | 'lightgray' => '211,211,211', 2162 | 'lightgreen' => '144,238,144', 2163 | 'lightgrey' => '211,211,211', 2164 | 'lightpink' => '255,182,193', 2165 | 'lightsalmon' => '255,160,122', 2166 | 'lightseagreen' => '32,178,170', 2167 | 'lightskyblue' => '135,206,250', 2168 | 'lightslategray' => '119,136,153', 2169 | 'lightslategrey' => '119,136,153', 2170 | 'lightsteelblue' => '176,196,222', 2171 | 'lightyellow' => '255,255,224', 2172 | 'lime' => '0,255,0', 2173 | 'limegreen' => '50,205,50', 2174 | 'linen' => '250,240,230', 2175 | 'magenta' => '255,0,255', 2176 | 'maroon' => '128,0,0', 2177 | 'mediumaquamarine' => '102,205,170', 2178 | 'mediumblue' => '0,0,205', 2179 | 'mediumorchid' => '186,85,211', 2180 | 'mediumpurple' => '147,112,219', 2181 | 'mediumseagreen' => '60,179,113', 2182 | 'mediumslateblue' => '123,104,238', 2183 | 'mediumspringgreen' => '0,250,154', 2184 | 'mediumturquoise' => '72,209,204', 2185 | 'mediumvioletred' => '199,21,133', 2186 | 'midnightblue' => '25,25,112', 2187 | 'mintcream' => '245,255,250', 2188 | 'mistyrose' => '255,228,225', 2189 | 'moccasin' => '255,228,181', 2190 | 'navajowhite' => '255,222,173', 2191 | 'navy' => '0,0,128', 2192 | 'oldlace' => '253,245,230', 2193 | 'olive' => '128,128,0', 2194 | 'olivedrab' => '107,142,35', 2195 | 'orange' => '255,165,0', 2196 | 'orangered' => '255,69,0', 2197 | 'orchid' => '218,112,214', 2198 | 'palegoldenrod' => '238,232,170', 2199 | 'palegreen' => '152,251,152', 2200 | 'paleturquoise' => '175,238,238', 2201 | 'palevioletred' => '219,112,147', 2202 | 'papayawhip' => '255,239,213', 2203 | 'peachpuff' => '255,218,185', 2204 | 'peru' => '205,133,63', 2205 | 'pink' => '255,192,203', 2206 | 'plum' => '221,160,221', 2207 | 'powderblue' => '176,224,230', 2208 | 'purple' => '128,0,128', 2209 | 'red' => '255,0,0', 2210 | 'rosybrown' => '188,143,143', 2211 | 'royalblue' => '65,105,225', 2212 | 'saddlebrown' => '139,69,19', 2213 | 'salmon' => '250,128,114', 2214 | 'sandybrown' => '244,164,96', 2215 | 'seagreen' => '46,139,87', 2216 | 'seashell' => '255,245,238', 2217 | 'sienna' => '160,82,45', 2218 | 'silver' => '192,192,192', 2219 | 'skyblue' => '135,206,235', 2220 | 'slateblue' => '106,90,205', 2221 | 'slategray' => '112,128,144', 2222 | 'slategrey' => '112,128,144', 2223 | 'snow' => '255,250,250', 2224 | 'springgreen' => '0,255,127', 2225 | 'steelblue' => '70,130,180', 2226 | 'tan' => '210,180,140', 2227 | 'teal' => '0,128,128', 2228 | 'thistle' => '216,191,216', 2229 | 'tomato' => '255,99,71', 2230 | 'turquoise' => '64,224,208', 2231 | 'violet' => '238,130,238', 2232 | 'wheat' => '245,222,179', 2233 | 'white' => '255,255,255', 2234 | 'whitesmoke' => '245,245,245', 2235 | 'yellow' => '255,255,0', 2236 | 'yellowgreen' => '154,205,50' 2237 | ); 2238 | } 2239 | 2240 | -------------------------------------------------------------------------------- /index.php: -------------------------------------------------------------------------------- 1 | parse($lessString); 37 | exit; 38 | } 39 | catch (Exception $e) { 40 | $error = $e->getMessage(); 41 | } 42 | } 43 | ?> 44 | 45 | 46 |
47 | 48 | 49 |Kick-start your Twitter Bootstrap project the way you want. Simply alter the options below and click "Generate" to get your compiled Bootstrap CSS file.
99 |