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