Subversion Repositories SmartDukaan

Rev

Details | Last modification | View Log | RSS feed

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