lessc_parser   F
last analyzed

Complexity

Total Complexity 351

Size/Duplication

Total Lines 1380
Duplicated Lines 15.8 %

Coupling/Cohesion

Components 1
Dependencies 1

Importance

Changes 0
Metric Value
dl 218
loc 1380
rs 0.8
c 0
b 0
f 0
wmc 351
lcom 1
cbo 1

46 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 22 2
B parse() 0 29 6
F parseChunk() 15 151 53
A isDirective() 0 8 1
A fixTags() 0 8 3
A expressionList() 0 12 3
A expression() 0 20 5
C expHelper() 0 46 17
B propertyValue() 0 21 7
B parenValue() 14 23 7
D value() 0 53 19
A import() 0 12 3
A mediaQueryList() 0 7 2
C mediaQuery() 6 32 13
B mediaExpression() 0 19 9
C openString() 7 66 16
B string() 31 50 9
A interpolation() 0 19 5
A unit() 4 13 6
A color() 0 12 3
D argumentDef() 0 97 24
A tags() 10 10 4
A mixinTags() 11 11 3
C tagBracket() 3 68 15
C tag() 0 62 14
C func() 0 41 13
A variable() 0 17 5
A assign() 0 4 3
A keyword() 7 7 2
A end() 9 9 4
A guards() 0 23 5
A guardGroup() 16 16 4
A guard() 0 13 5
B literal() 11 22 7
B genericList() 23 23 7
A to() 11 11 5
B match() 5 11 7
A whitespace() 8 17 5
A peek() 7 7 2
A seek() 0 5 2
A throwError() 10 19 4
A pushBlock() 0 16 1
A pushSpecialBlock() 0 3 1
A append() 0 4 2
A pop() 0 5 1
C removeComments() 10 54 17

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like lessc_parser often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use lessc_parser, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
/**
4
 * lessphp v0.5.0
5
 * https://leafo.net/lessphp
6
 *
7
 * LESS CSS compiler, adapted from http://lesscss.org
8
 *
9
 * Copyright 2013, Leaf Corcoran <[email protected]>
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 instance 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.5.0";
42
43
	static public $TRUE = array("keyword", "true");
44
	static public $FALSE = array("keyword", "false");
45
46
	protected $libFunctions = array();
47
	protected $registeredVars = array();
48
	protected $preserveComments = false;
49
50
	public $vPrefix = '@'; // prefix of abstract properties
51
	public $mPrefix = '$'; // prefix of abstract blocks
52
	public $parentSelector = '&';
53
54
	public $importDisabled = false;
55
	public $importDir = '';
56
57
	protected $numberPrecision = null;
58
59
	protected $allParsedFiles = array();
60
61
	// set to the parser that generated the current line when compiling
62
	// so we know how to create error messages
63
	protected $sourceParser = null;
64
	protected $sourceLoc = null;
65
66
	static protected $nextImportId = 0; // uniquely identify imports
67
68
	// attempts to find the path of an import url, returns null for css files
69
	protected function findImport($url) {
70
		foreach ((array)$this->importDir as $dir) {
71
			$full = $dir.(substr($dir, -1) != '/' ? '/' : '').$url;
72
			if ($this->fileExists($file = $full.'.less') || $this->fileExists($file = $full)) {
73
				return $file;
74
			}
75
		}
76
77
		return null;
78
	}
79
80
	protected function fileExists($name) {
81
		return is_file($name);
82
	}
83
84
	static public function compressList($items, $delim) {
85
		if (!isset($items[1]) && isset($items[0])) return $items[0];
86
		else return array('list', $delim, $items);
87
	}
88
89
	static public function preg_quote($what) {
90
		return preg_quote($what, '/');
91
	}
92
93
	protected function tryImport($importPath, $parentBlock, $out) {
94
		if ($importPath[0] == "function" && $importPath[1] == "url") {
95
			$importPath = $this->flattenList($importPath[2]);
96
		}
97
98
		$str = $this->coerceString($importPath);
99
		if ($str === null) return false;
100
101
		$url = $this->compileValue($this->lib_e($str));
102
103
		// don't import if it ends in css
104
		if (substr_compare($url, '.css', -4, 4) === 0) return false;
105
106
		$realPath = $this->findImport($url);
107
108
		if ($realPath === null) return false;
109
110
		if ($this->importDisabled) {
111
			return array(false, "/* import disabled */");
112
		}
113
114
		if (isset($this->allParsedFiles[realpath($realPath)])) {
115
			return array(false, null);
116
		}
117
118
		$this->addParsedFile($realPath);
119
		$parser = $this->makeParser($realPath);
120
		$root = $parser->parse(file_get_contents($realPath));
121
122
		// set the parents of all the block props
123
		foreach ($root->props as $prop) {
124
			if ($prop[0] == "block") {
125
				$prop[1]->parent = $parentBlock;
126
			}
127
		}
128
129
		// copy mixins into scope, set their parents
130
		// bring blocks from import into current block
131
		// TODO: need to mark the source parser	these came from this file
132
		foreach ($root->children as $childName => $child) {
133
			if (isset($parentBlock->children[$childName])) {
134
				$parentBlock->children[$childName] = array_merge(
135
					$parentBlock->children[$childName],
136
					$child);
137
			} else {
138
				$parentBlock->children[$childName] = $child;
139
			}
140
		}
141
142
		$pi = pathinfo($realPath);
143
		$dir = $pi["dirname"];
144
145
		list($top, $bottom) = $this->sortProps($root->props, true);
146
		$this->compileImportedProps($top, $parentBlock, $out, $parser, $dir);
147
148
		return array(true, $bottom, $parser, $dir);
149
	}
150
151
	protected function compileImportedProps($props, $block, $out, $sourceParser, $importDir) {
152
		$oldSourceParser = $this->sourceParser;
153
154
		$oldImport = $this->importDir;
155
156
		// TODO: this is because the importDir api is stupid
157
		$this->importDir = (array)$this->importDir;
0 ignored issues
show
Documentation Bug introduced by
It seems like (array) $this->importDir of type array is incompatible with the declared type string of property $importDir.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
158
		array_unshift($this->importDir, $importDir);
159
160
		foreach ($props as $prop) {
161
			$this->compileProp($prop, $block, $out);
162
		}
163
164
		$this->importDir = $oldImport;
165
		$this->sourceParser = $oldSourceParser;
166
	}
167
168
	/**
169
	 * Recursively compiles a block.
170
	 *
171
	 * A block is analogous to a CSS block in most cases. A single LESS document
172
	 * is encapsulated in a block when parsed, but it does not have parent tags
173
	 * so all of it's children appear on the root level when compiled.
174
	 *
175
	 * Blocks are made up of props and children.
176
	 *
177
	 * Props are property instructions, array tuples which describe an action
178
	 * to be taken, eg. write a property, set a variable, mixin a block.
179
	 *
180
	 * The children of a block are just all the blocks that are defined within.
181
	 * This is used to look up mixins when performing a mixin.
182
	 *
183
	 * Compiling the block involves pushing a fresh environment on the stack,
184
	 * and iterating through the props, compiling each one.
185
	 *
186
	 * See lessc::compileProp()
187
	 *
188
	 */
189
	protected function compileBlock($block) {
190
		switch ($block->type) {
191
		case "root":
192
			$this->compileRoot($block);
193
			break;
194
		case null:
195
			$this->compileCSSBlock($block);
196
			break;
197
		case "media":
198
			$this->compileMedia($block);
199
			break;
200 View Code Duplication
		case "directive":
201
			$name = "@" . $block->name;
202
			if (!empty($block->value)) {
203
				$name .= " " . $this->compileValue($this->reduce($block->value));
204
			}
205
206
			$this->compileNestedBlock($block, array($name));
207
			break;
208
		default:
209
			$this->throwError("unknown block type: $block->type\n");
210
		}
211
	}
212
213
	protected function compileCSSBlock($block) {
214
		$env = $this->pushEnv();
215
216
		$selectors = $this->compileSelectors($block->tags);
217
		$env->selectors = $this->multiplySelectors($selectors);
218
		$out = $this->makeOutputBlock(null, $env->selectors);
219
220
		$this->scope->children[] = $out;
0 ignored issues
show
Bug introduced by
The property scope does not exist. Did you maybe forget to declare it?

In PHP it is possible to write to properties without declaring them. For example, the following is perfectly valid PHP code:

class MyClass { }

$x = new MyClass();
$x->foo = true;

Generally, it is a good practice to explictly declare properties to avoid accidental typos and provide IDE auto-completion:

class MyClass {
    public $foo;
}

$x = new MyClass();
$x->foo = true;
Loading history...
221
		$this->compileProps($block, $out);
222
223
		$block->scope = $env; // mixins carry scope with them!
224
		$this->popEnv();
225
	}
226
227
	protected function compileMedia($media) {
228
		$env = $this->pushEnv($media);
229
		$parentScope = $this->mediaParent($this->scope);
230
231
		$query = $this->compileMediaQuery($this->multiplyMedia($env));
232
233
		$this->scope = $this->makeOutputBlock($media->type, array($query));
234
		$parentScope->children[] = $this->scope;
235
236
		$this->compileProps($media, $this->scope);
237
238
		if (count($this->scope->lines) > 0) {
239
			$orphanSelelectors = $this->findClosestSelectors();
240
			if (!is_null($orphanSelelectors)) {
241
				$orphan = $this->makeOutputBlock(null, $orphanSelelectors);
242
				$orphan->lines = $this->scope->lines;
243
				array_unshift($this->scope->children, $orphan);
244
				$this->scope->lines = array();
245
			}
246
		}
247
248
		$this->scope = $this->scope->parent;
249
		$this->popEnv();
250
	}
251
252 View Code Duplication
	protected function mediaParent($scope) {
253
		while (!empty($scope->parent)) {
254
			if (!empty($scope->type) && $scope->type != "media") {
255
				break;
256
			}
257
			$scope = $scope->parent;
258
		}
259
260
		return $scope;
261
	}
262
263 View Code Duplication
	protected function compileNestedBlock($block, $selectors) {
264
		$this->pushEnv($block);
265
		$this->scope = $this->makeOutputBlock($block->type, $selectors);
266
		$this->scope->parent->children[] = $this->scope;
267
268
		$this->compileProps($block, $this->scope);
269
270
		$this->scope = $this->scope->parent;
271
		$this->popEnv();
272
	}
273
274
	protected function compileRoot($root) {
275
		$this->pushEnv();
276
		$this->scope = $this->makeOutputBlock($root->type);
277
		$this->compileProps($root, $this->scope);
278
		$this->popEnv();
279
	}
280
281
	protected function compileProps($block, $out) {
282
		foreach ($this->sortProps($block->props) as $prop) {
283
			$this->compileProp($prop, $block, $out);
284
		}
285
		$out->lines = $this->deduplicate($out->lines);
286
	}
287
288
	/**
289
	 * Deduplicate lines in a block. Comments are not deduplicated. If a
290
	 * duplicate rule is detected, the comments immediately preceding each
291
	 * occurence are consolidated.
292
	 */
293
	protected function deduplicate($lines) {
294
		$unique = array();
295
		$comments = array();
296
297
		foreach($lines as $line) {
298
			if (strpos($line, '/*') === 0) {
299
				$comments[] = $line;
300
				continue;
301
			}
302
			if (!in_array($line, $unique)) {
303
				$unique[] = $line;
304
			}
305
			array_splice($unique, array_search($line, $unique), 0, $comments);
306
			$comments = array();
307
		}
308
		return array_merge($unique, $comments);
309
	}
310
311
	protected function sortProps($props, $split = false) {
312
		$vars = array();
313
		$imports = array();
314
		$other = array();
315
		$stack = array();
316
317
		foreach ($props as $prop) {
318
			switch ($prop[0]) {
319
			case "comment":
320
				$stack[] = $prop;
321
				break;
322
			case "assign":
323
				$stack[] = $prop;
324
				if (isset($prop[1][0]) && $prop[1][0] == $this->vPrefix) {
325
					$vars = array_merge($vars, $stack);
326
				} else {
327
					$other = array_merge($other, $stack);
328
				}
329
				$stack = array();
330
				break;
331
			case "import":
332
				$id = self::$nextImportId++;
333
				$prop[] = $id;
334
				$stack[] = $prop;
335
				$imports = array_merge($imports, $stack);
336
				$other[] = array("import_mixin", $id);
337
				$stack = array();
338
				break;
339
			default:
340
				$stack[] = $prop;
341
				$other = array_merge($other, $stack);
342
				$stack = array();
343
				break;
344
			}
345
		}
346
		$other = array_merge($other, $stack);
347
348
		if ($split) {
349
			return array(array_merge($imports, $vars), $other);
350
		} else {
351
			return array_merge($imports, $vars, $other);
352
		}
353
	}
354
355
	protected function compileMediaQuery($queries) {
356
		$compiledQueries = array();
357
		foreach ($queries as $query) {
358
			$parts = array();
359
			foreach ($query as $q) {
360
				switch ($q[0]) {
361
				case "mediaType":
362
					$parts[] = implode(" ", array_slice($q, 1));
363
					break;
364
				case "mediaExp":
365
					if (isset($q[2])) {
366
						$parts[] = "($q[1]: " .
367
							$this->compileValue($this->reduce($q[2])) . ")";
368
					} else {
369
						$parts[] = "($q[1])";
370
					}
371
					break;
372
				case "variable":
373
					$parts[] = $this->compileValue($this->reduce($q));
374
				break;
375
				}
376
			}
377
378
			if (count($parts) > 0) {
379
				$compiledQueries[] =  implode(" and ", $parts);
380
			}
381
		}
382
383
		$out = "@media";
384
		if (!empty($parts)) {
385
			$out .= " " .
386
				implode($this->formatter->selectorSeparator, $compiledQueries);
0 ignored issues
show
Bug introduced by
The property formatter does not exist. Did you maybe forget to declare it?

In PHP it is possible to write to properties without declaring them. For example, the following is perfectly valid PHP code:

class MyClass { }

$x = new MyClass();
$x->foo = true;

Generally, it is a good practice to explictly declare properties to avoid accidental typos and provide IDE auto-completion:

class MyClass {
    public $foo;
}

$x = new MyClass();
$x->foo = true;
Loading history...
387
		}
388
		return $out;
389
	}
390
391 View Code Duplication
	protected function multiplyMedia($env, $childQueries = null) {
392
		if (is_null($env) ||
393
			!empty($env->block->type) && $env->block->type != "media")
394
		{
395
			return $childQueries;
396
		}
397
398
		// plain old block, skip
399
		if (empty($env->block->type)) {
400
			return $this->multiplyMedia($env->parent, $childQueries);
401
		}
402
403
		$out = array();
404
		$queries = $env->block->queries;
405
		if (is_null($childQueries)) {
406
			$out = $queries;
407
		} else {
408
			foreach ($queries as $parent) {
409
				foreach ($childQueries as $child) {
410
					$out[] = array_merge($parent, $child);
411
				}
412
			}
413
		}
414
415
		return $this->multiplyMedia($env->parent, $out);
416
	}
417
418
	protected function expandParentSelectors(&$tag, $replace) {
419
		$parts = explode("$&$", $tag);
420
		$count = 0;
421
		foreach ($parts as &$part) {
422
			$part = str_replace($this->parentSelector, $replace, $part, $c);
423
			$count += $c;
424
		}
425
		$tag = implode($this->parentSelector, $parts);
426
		return $count;
427
	}
428
429
	protected function findClosestSelectors() {
430
		$env = $this->env;
0 ignored issues
show
Bug introduced by
The property env does not exist. Did you maybe forget to declare it?

In PHP it is possible to write to properties without declaring them. For example, the following is perfectly valid PHP code:

class MyClass { }

$x = new MyClass();
$x->foo = true;

Generally, it is a good practice to explictly declare properties to avoid accidental typos and provide IDE auto-completion:

class MyClass {
    public $foo;
}

$x = new MyClass();
$x->foo = true;
Loading history...
431
		$selectors = null;
432
		while ($env !== null) {
433
			if (isset($env->selectors)) {
434
				$selectors = $env->selectors;
435
				break;
436
			}
437
			$env = $env->parent;
438
		}
439
440
		return $selectors;
441
	}
442
443
444
	// multiply $selectors against the nearest selectors in env
445
	protected function multiplySelectors($selectors) {
446
		// find parent selectors
447
448
		$parentSelectors = $this->findClosestSelectors();
449
		if (is_null($parentSelectors)) {
450
			// kill parent reference in top level selector
451
			foreach ($selectors as &$s) {
452
				$this->expandParentSelectors($s, "");
453
			}
454
455
			return $selectors;
456
		}
457
458
		$out = array();
459
		foreach ($parentSelectors as $parent) {
460
			foreach ($selectors as $child) {
461
				$count = $this->expandParentSelectors($child, $parent);
462
463
				// don't prepend the parent tag if & was used
464
				if ($count > 0) {
465
					$out[] = trim($child);
466
				} else {
467
					$out[] = trim($parent . ' ' . $child);
468
				}
469
			}
470
		}
471
472
		return $out;
473
	}
474
475
	// reduces selector expressions
476
	protected function compileSelectors($selectors) {
477
		$out = array();
478
479
		foreach ($selectors as $s) {
480
			if (is_array($s)) {
481
				list(, $value) = $s;
482
				$out[] = trim($this->compileValue($this->reduce($value)));
483
			} else {
484
				$out[] = $s;
485
			}
486
		}
487
488
		return $out;
489
	}
490
491
	protected function eq($left, $right) {
492
		return $left == $right;
493
	}
494
495
	protected function patternMatch($block, $orderedArgs, $keywordArgs) {
496
		// match the guards if it has them
497
		// any one of the groups must have all its guards pass for a match
498
		if (!empty($block->guards)) {
499
			$groupPassed = false;
500
			foreach ($block->guards as $guardGroup) {
501
				foreach ($guardGroup as $guard) {
502
					$this->pushEnv();
503
					$this->zipSetArgs($block->args, $orderedArgs, $keywordArgs);
504
505
					$negate = false;
506
					if ($guard[0] == "negate") {
507
						$guard = $guard[1];
508
						$negate = true;
509
					}
510
511
					$passed = $this->reduce($guard) == self::$TRUE;
512
					if ($negate) $passed = !$passed;
513
514
					$this->popEnv();
515
516
					if ($passed) {
517
						$groupPassed = true;
518
					} else {
519
						$groupPassed = false;
520
						break;
521
					}
522
				}
523
524
				if ($groupPassed) break;
525
			}
526
527
			if (!$groupPassed) {
528
				return false;
529
			}
530
		}
531
532
		if (empty($block->args)) {
533
			return $block->isVararg || empty($orderedArgs) && empty($keywordArgs);
534
		}
535
536
		$remainingArgs = $block->args;
537
		if ($keywordArgs) {
538
			$remainingArgs = array();
539
			foreach ($block->args as $arg) {
540
				if ($arg[0] == "arg" && isset($keywordArgs[$arg[1]])) {
541
					continue;
542
				}
543
544
				$remainingArgs[] = $arg;
545
			}
546
		}
547
548
		$i = -1; // no args
549
		// try to match by arity or by argument literal
550
		foreach ($remainingArgs as $i => $arg) {
551
			switch ($arg[0]) {
552
			case "lit":
553
				if (empty($orderedArgs[$i]) || !$this->eq($arg[1], $orderedArgs[$i])) {
554
					return false;
555
				}
556
				break;
557
			case "arg":
558
				// no arg and no default value
559
				if (!isset($orderedArgs[$i]) && !isset($arg[2])) {
560
					return false;
561
				}
562
				break;
563
			case "rest":
564
				$i--; // rest can be empty
565
				break 2;
566
			}
567
		}
568
569
		if ($block->isVararg) {
570
			return true; // not having enough is handled above
571
		} else {
572
			$numMatched = $i + 1;
573
			// greater than becuase default values always match
574
			return $numMatched >= count($orderedArgs);
575
		}
576
	}
577
578
	protected function patternMatchAll($blocks, $orderedArgs, $keywordArgs, $skip=array()) {
579
		$matches = null;
580
		foreach ($blocks as $block) {
581
			// skip seen blocks that don't have arguments
582
			if (isset($skip[$block->id]) && !isset($block->args)) {
583
				continue;
584
			}
585
586
			if ($this->patternMatch($block, $orderedArgs, $keywordArgs)) {
587
				$matches[] = $block;
588
			}
589
		}
590
591
		return $matches;
592
	}
593
594
	// attempt to find blocks matched by path and args
595
	protected function findBlocks($searchIn, $path, $orderedArgs, $keywordArgs, $seen=array()) {
596
		if ($searchIn == null) return null;
597
		if (isset($seen[$searchIn->id])) return null;
598
		$seen[$searchIn->id] = true;
599
600
		$name = $path[0];
601
602
		if (isset($searchIn->children[$name])) {
603
			$blocks = $searchIn->children[$name];
604
			if (count($path) == 1) {
605
				$matches = $this->patternMatchAll($blocks, $orderedArgs, $keywordArgs, $seen);
0 ignored issues
show
Bug introduced by
Are you sure the assignment to $matches is correct as $this->patternMatchAll($...s, $keywordArgs, $seen) (which targets lessc::patternMatchAll()) seems to always return null.

This check looks for function or method calls that always return null and whose return value is assigned to a variable.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
$object = $a->getObject();

The method getObject() can return nothing but null, so it makes no sense to assign that value to a variable.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
606
				if (!empty($matches)) {
607
					// This will return all blocks that match in the closest
608
					// scope that has any matching block, like lessjs
609
					return $matches;
610
				}
611
			} else {
612
				$matches = array();
613
				foreach ($blocks as $subBlock) {
614
					$subMatches = $this->findBlocks($subBlock,
615
						array_slice($path, 1), $orderedArgs, $keywordArgs, $seen);
616
617
					if (!is_null($subMatches)) {
618
						foreach ($subMatches as $sm) {
619
							$matches[] = $sm;
620
						}
621
					}
622
				}
623
624
				return count($matches) > 0 ? $matches : null;
625
			}
626
		}
627
		if ($searchIn->parent === $searchIn) return null;
628
		return $this->findBlocks($searchIn->parent, $path, $orderedArgs, $keywordArgs, $seen);
629
	}
630
631
	// sets all argument names in $args to either the default value
632
	// or the one passed in through $values
633
	protected function zipSetArgs($args, $orderedValues, $keywordValues) {
634
		$assignedValues = array();
635
636
		$i = 0;
637
		foreach ($args as  $a) {
638
			if ($a[0] == "arg") {
639
				if (isset($keywordValues[$a[1]])) {
640
					// has keyword arg
641
					$value = $keywordValues[$a[1]];
642
				} elseif (isset($orderedValues[$i])) {
643
					// has ordered arg
644
					$value = $orderedValues[$i];
645
					$i++;
646
				} elseif (isset($a[2])) {
647
					// has default value
648
					$value = $a[2];
649
				} else {
650
					$this->throwError("Failed to assign arg " . $a[1]);
651
					$value = null; // :(
652
				}
653
654
				$value = $this->reduce($value);
655
				$this->set($a[1], $value);
656
				$assignedValues[] = $value;
657
			} else {
658
				// a lit
659
				$i++;
660
			}
661
		}
662
663
		// check for a rest
664
		$last = end($args);
665
		if ($last[0] == "rest") {
666
			$rest = array_slice($orderedValues, count($args) - 1);
667
			$this->set($last[1], $this->reduce(array("list", " ", $rest)));
668
		}
669
670
		// wow is this the only true use of PHP's + operator for arrays?
671
		$this->env->arguments = $assignedValues + $orderedValues;
672
	}
673
674
	// compile a prop and update $lines or $blocks appropriately
675
	protected function compileProp($prop, $block, $out) {
676
		// set error position context
677
		$this->sourceLoc = isset($prop[-1]) ? $prop[-1] : -1;
678
679
		switch ($prop[0]) {
680
		case 'assign':
681
			list(, $name, $value) = $prop;
682
			if ($name[0] == $this->vPrefix) {
683
				$this->set($name, $value);
684
			} else {
685
				$out->lines[] = $this->formatter->property($name,
686
						$this->compileValue($this->reduce($value)));
687
			}
688
			break;
689
		case 'block':
690
			list(, $child) = $prop;
691
			$this->compileBlock($child);
692
			break;
693
		case 'mixin':
694
			list(, $path, $args, $suffix) = $prop;
695
696
			$orderedArgs = array();
697
			$keywordArgs = array();
698
			foreach ((array)$args as $arg) {
699
				$argval = null;
0 ignored issues
show
Unused Code introduced by
$argval is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
700
				switch ($arg[0]) {
701
				case "arg":
702
					if (!isset($arg[2])) {
703
						$orderedArgs[] = $this->reduce(array("variable", $arg[1]));
704
					} else {
705
						$keywordArgs[$arg[1]] = $this->reduce($arg[2]);
706
					}
707
					break;
708
709
				case "lit":
710
					$orderedArgs[] = $this->reduce($arg[1]);
711
					break;
712
				default:
713
					$this->throwError("Unknown arg type: " . $arg[0]);
714
				}
715
			}
716
717
			$mixins = $this->findBlocks($block, $path, $orderedArgs, $keywordArgs);
718
719
			if ($mixins === null) {
720
				$this->throwError("{$prop[1][0]} is undefined");
721
			}
722
723
			foreach ($mixins as $mixin) {
724
				if ($mixin === $block && !$orderedArgs) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $orderedArgs of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
725
					continue;
726
				}
727
728
				$haveScope = false;
729
				if (isset($mixin->parent->scope)) {
730
					$haveScope = true;
731
					$mixinParentEnv = $this->pushEnv();
732
					$mixinParentEnv->storeParent = $mixin->parent->scope;
733
				}
734
735
				$haveArgs = false;
736
				if (isset($mixin->args)) {
737
					$haveArgs = true;
738
					$this->pushEnv();
739
					$this->zipSetArgs($mixin->args, $orderedArgs, $keywordArgs);
740
				}
741
742
				$oldParent = $mixin->parent;
743
				if ($mixin != $block) $mixin->parent = $block;
744
745
				foreach ($this->sortProps($mixin->props) as $subProp) {
746
					if ($suffix !== null &&
747
						$subProp[0] == "assign" &&
748
						is_string($subProp[1]) &&
749
						$subProp[1][0] != $this->vPrefix)
750
					{
751
						$subProp[2] = array(
752
							'list', ' ',
753
							array($subProp[2], array('keyword', $suffix))
754
						);
755
					}
756
757
					$this->compileProp($subProp, $mixin, $out);
758
				}
759
760
				$mixin->parent = $oldParent;
761
762
				if ($haveArgs) $this->popEnv();
763
				if ($haveScope) $this->popEnv();
764
			}
765
766
			break;
767
		case 'raw':
768
			$out->lines[] = $prop[1];
769
			break;
770 View Code Duplication
		case "directive":
771
			list(, $name, $value) = $prop;
772
			$out->lines[] = "@$name " . $this->compileValue($this->reduce($value)).';';
773
			break;
774
		case "comment":
775
			$out->lines[] = $prop[1];
776
			break;
777
		case "import";
778
			list(, $importPath, $importId) = $prop;
779
			$importPath = $this->reduce($importPath);
780
781
			if (!isset($this->env->imports)) {
782
				$this->env->imports = array();
783
			}
784
785
			$result = $this->tryImport($importPath, $block, $out);
786
787
			$this->env->imports[$importId] = $result === false ?
788
				array(false, "@import " . $this->compileValue($importPath).";") :
789
				$result;
790
791
			break;
792
		case "import_mixin":
793
			list(,$importId) = $prop;
794
			$import = $this->env->imports[$importId];
795
			if ($import[0] === false) {
796
				if (isset($import[1])) {
797
					$out->lines[] = $import[1];
798
				}
799
			} else {
800
				list(, $bottom, $parser, $importDir) = $import;
801
				$this->compileImportedProps($bottom, $block, $out, $parser, $importDir);
802
			}
803
804
			break;
805
		default:
806
			$this->throwError("unknown op: {$prop[0]}\n");
807
		}
808
	}
809
810
811
	/**
812
	 * Compiles a primitive value into a CSS property value.
813
	 *
814
	 * Values in lessphp are typed by being wrapped in arrays, their format is
815
	 * typically:
816
	 *
817
	 *     array(type, contents [, additional_contents]*)
818
	 *
819
	 * The input is expected to be reduced. This function will not work on
820
	 * things like expressions and variables.
821
	 */
822
	public function compileValue($value) {
823
		switch ($value[0]) {
824
		case 'list':
825
			// [1] - delimiter
826
			// [2] - array of values
827
			return implode($value[1], array_map(array($this, 'compileValue'), $value[2]));
828
		case 'raw_color':
829
			if (!empty($this->formatter->compressColors)) {
830
				return $this->compileValue($this->coerceColor($value));
831
			}
832
			return $value[1];
833
		case 'keyword':
834
			// [1] - the keyword
835
			return $value[1];
836
		case 'number':
837
			list(, $num, $unit) = $value;
838
			// [1] - the number
839
			// [2] - the unit
840
			if ($this->numberPrecision !== null) {
841
				$num = round($num, $this->numberPrecision);
842
			}
843
			return $num . $unit;
844
		case 'string':
845
			// [1] - contents of string (includes quotes)
846
			list(, $delim, $content) = $value;
847
			foreach ($content as &$part) {
848
				if (is_array($part)) {
849
					$part = $this->compileValue($part);
850
				}
851
			}
852
			return $delim . implode($content) . $delim;
853
		case 'color':
854
			// [1] - red component (either number or a %)
855
			// [2] - green component
856
			// [3] - blue component
857
			// [4] - optional alpha component
858
			list(, $r, $g, $b) = $value;
859
			$r = round($r);
860
			$g = round($g);
861
			$b = round($b);
862
863 View Code Duplication
			if (count($value) == 5 && $value[4] != 1) { // rgba
864
				return 'rgba('.$r.','.$g.','.$b.','.$value[4].')';
865
			}
866
867
			$h = sprintf("#%02x%02x%02x", $r, $g, $b);
868
869 View Code Duplication
			if (!empty($this->formatter->compressColors)) {
870
				// Converting hex color to short notation (e.g. #003399 to #039)
871
				if ($h[1] === $h[2] && $h[3] === $h[4] && $h[5] === $h[6]) {
872
					$h = '#' . $h[1] . $h[3] . $h[5];
873
				}
874
			}
875
876
			return $h;
877
878
		case 'function':
879
			list(, $name, $args) = $value;
880
			return $name.'('.$this->compileValue($args).')';
881
		default: // assumed to be unit
882
			$this->throwError("unknown value type: $value[0]");
883
		}
884
	}
885
886
	protected function lib_pow($args) {
887
		list($base, $exp) = $this->assertArgs($args, 2, "pow");
888
		return pow($this->assertNumber($base), $this->assertNumber($exp));
889
	}
890
891
	protected function lib_pi() {
892
		return pi();
893
	}
894
895
	protected function lib_mod($args) {
896
		list($a, $b) = $this->assertArgs($args, 2, "mod");
897
		return $this->assertNumber($a) % $this->assertNumber($b);
898
	}
899
900
	protected function lib_tan($num) {
901
		return tan($this->assertNumber($num));
902
	}
903
904
	protected function lib_sin($num) {
905
		return sin($this->assertNumber($num));
906
	}
907
908
	protected function lib_cos($num) {
909
		return cos($this->assertNumber($num));
910
	}
911
912
	protected function lib_atan($num) {
913
		$num = atan($this->assertNumber($num));
914
		return array("number", $num, "rad");
915
	}
916
917
	protected function lib_asin($num) {
918
		$num = asin($this->assertNumber($num));
919
		return array("number", $num, "rad");
920
	}
921
922
	protected function lib_acos($num) {
923
		$num = acos($this->assertNumber($num));
924
		return array("number", $num, "rad");
925
	}
926
927
	protected function lib_sqrt($num) {
928
		return sqrt($this->assertNumber($num));
929
	}
930
931
	protected function lib_extract($value) {
932
		list($list, $idx) = $this->assertArgs($value, 2, "extract");
933
		$idx = $this->assertNumber($idx);
934
		// 1 indexed
935
		if ($list[0] == "list" && isset($list[2][$idx - 1])) {
936
			return $list[2][$idx - 1];
937
		}
938
	}
939
940
	protected function lib_isnumber($value) {
941
		return $this->toBool($value[0] == "number");
942
	}
943
944
	protected function lib_isstring($value) {
945
		return $this->toBool($value[0] == "string");
946
	}
947
948
	protected function lib_iscolor($value) {
949
		return $this->toBool($this->coerceColor($value));
950
	}
951
952
	protected function lib_iskeyword($value) {
953
		return $this->toBool($value[0] == "keyword");
954
	}
955
956
	protected function lib_ispixel($value) {
957
		return $this->toBool($value[0] == "number" && $value[2] == "px");
958
	}
959
960
	protected function lib_ispercentage($value) {
961
		return $this->toBool($value[0] == "number" && $value[2] == "%");
962
	}
963
964
	protected function lib_isem($value) {
965
		return $this->toBool($value[0] == "number" && $value[2] == "em");
966
	}
967
968
	protected function lib_isrem($value) {
969
		return $this->toBool($value[0] == "number" && $value[2] == "rem");
970
	}
971
972
	protected function lib_rgbahex($color) {
973
		$color = $this->coerceColor($color);
974
		if (is_null($color))
975
			$this->throwError("color expected for rgbahex");
976
977
		return sprintf("#%02x%02x%02x%02x",
978
			isset($color[4]) ? $color[4]*255 : 255,
979
			$color[1],$color[2], $color[3]);
980
	}
981
982
	protected function lib_argb($color){
983
		return $this->lib_rgbahex($color);
984
	}
985
986
	/**
987
	 * Given an url, decide whether to output a regular link or the base64-encoded contents of the file
988
	 *
989
	 * @param  array  $value either an argument list (two strings) or a single string
990
	 * @return string        formatted url(), either as a link or base64-encoded
991
	 */
992
	protected function lib_data_uri($value) {
993
		$mime = ($value[0] === 'list') ? $value[2][0][2] : null;
994
		$url = ($value[0] === 'list') ? $value[2][1][2][0] : $value[2][0];
995
996
		$fullpath = $this->findImport($url);
997
998
		if($fullpath && ($fsize = filesize($fullpath)) !== false) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $fullpath of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
999
			// IE8 can't handle data uris larger than 32KB
1000
			if($fsize/1024 < 32) {
1001
				if(is_null($mime)) {
1002
					$mime = jetpack_mime_content_type( $fullpath );
1003
				}
1004
1005
				if(!is_null($mime)) // fallback if the mime type is still unknown
1006
					$url = sprintf('data:%s;base64,%s', $mime, base64_encode(file_get_contents($fullpath)));
1007
			}
1008
		}
1009
1010
		return 'url("'.$url.'")';
1011
	}
1012
1013
	// utility func to unquote a string
1014
	protected function lib_e($arg) {
1015
		switch ($arg[0]) {
1016
			case "list":
1017
				$items = $arg[2];
1018
				if (isset($items[0])) {
1019
					return $this->lib_e($items[0]);
1020
				}
1021
				$this->throwError("unrecognised input");
1022
			case "string":
1023
				$arg[1] = "";
1024
				return $arg;
1025
			case "keyword":
1026
				return $arg;
1027
			default:
1028
				return array("keyword", $this->compileValue($arg));
1029
		}
1030
	}
1031
1032
	protected function lib__sprintf($args) {
1033
		if ($args[0] != "list") return $args;
1034
		$values = $args[2];
1035
		$string = array_shift($values);
1036
		$template = $this->compileValue($this->lib_e($string));
1037
1038
		$i = 0;
1039
		if (preg_match_all('/%[dsa]/', $template, $m)) {
1040
			foreach ($m[0] as $match) {
1041
				$val = isset($values[$i]) ?
1042
					$this->reduce($values[$i]) : array('keyword', '');
1043
1044
				// lessjs compat, renders fully expanded color, not raw color
1045
				if ($color = $this->coerceColor($val)) {
1046
					$val = $color;
1047
				}
1048
1049
				$i++;
1050
				$rep = $this->compileValue($this->lib_e($val));
1051
				$template = preg_replace('/'.self::preg_quote($match).'/',
1052
					$rep, $template, 1);
1053
			}
1054
		}
1055
1056
		$d = $string[0] == "string" ? $string[1] : '"';
1057
		return array("string", $d, array($template));
1058
	}
1059
1060
	protected function lib_floor($arg) {
1061
		$value = $this->assertNumber($arg);
1062
		return array("number", floor($value), $arg[2]);
1063
	}
1064
1065
	protected function lib_ceil($arg) {
1066
		$value = $this->assertNumber($arg);
1067
		return array("number", ceil($value), $arg[2]);
1068
	}
1069
1070
	protected function lib_round($arg) {
1071
		if($arg[0] != "list") {
1072
			$value = $this->assertNumber($arg);
1073
			return array("number", round($value), $arg[2]);
1074
		} else {
1075
			$value = $this->assertNumber($arg[2][0]);
1076
			$precision = $this->assertNumber($arg[2][1]);
1077
			return array("number", round($value, $precision), $arg[2][0][2]);
1078
		}
1079
	}
1080
1081
	protected function lib_unit($arg) {
1082
		if ($arg[0] == "list") {
1083
			list($number, $newUnit) = $arg[2];
1084
			return array("number", $this->assertNumber($number),
1085
				$this->compileValue($this->lib_e($newUnit)));
1086
		} else {
1087
			return array("number", $this->assertNumber($arg), "");
1088
		}
1089
	}
1090
1091
	/**
1092
	 * Helper function to get arguments for color manipulation functions.
1093
	 * takes a list that contains a color like thing and a percentage
1094
	 */
1095
	public function colorArgs($args) {
1096
		if ($args[0] != 'list' || count($args[2]) < 2) {
1097
			return array(array('color', 0, 0, 0), 0);
1098
		}
1099
		list($color, $delta) = $args[2];
1100
		$color = $this->assertColor($color);
1101
		$delta = (float) $delta[1];
1102
1103
		return array($color, $delta);
1104
	}
1105
1106 View Code Duplication
	protected function lib_darken($args) {
1107
		list($color, $delta) = $this->colorArgs($args);
1108
1109
		$hsl = $this->toHSL($color);
1110
		$hsl[3] = $this->clamp($hsl[3] - $delta, 100);
1111
		return $this->toRGB($hsl);
1112
	}
1113
1114 View Code Duplication
	protected function lib_lighten($args) {
1115
		list($color, $delta) = $this->colorArgs($args);
1116
1117
		$hsl = $this->toHSL($color);
1118
		$hsl[3] = $this->clamp($hsl[3] + $delta, 100);
1119
		return $this->toRGB($hsl);
1120
	}
1121
1122 View Code Duplication
	protected function lib_saturate($args) {
1123
		list($color, $delta) = $this->colorArgs($args);
1124
1125
		$hsl = $this->toHSL($color);
1126
		$hsl[2] = $this->clamp($hsl[2] + $delta, 100);
1127
		return $this->toRGB($hsl);
1128
	}
1129
1130 View Code Duplication
	protected function lib_desaturate($args) {
1131
		list($color, $delta) = $this->colorArgs($args);
1132
1133
		$hsl = $this->toHSL($color);
1134
		$hsl[2] = $this->clamp($hsl[2] - $delta, 100);
1135
		return $this->toRGB($hsl);
1136
	}
1137
1138
	protected function lib_spin($args) {
1139
		list($color, $delta) = $this->colorArgs($args);
1140
1141
		$hsl = $this->toHSL($color);
1142
1143
		$hsl[1] = $hsl[1] + $delta % 360;
1144
		if ($hsl[1] < 0) $hsl[1] += 360;
1145
1146
		return $this->toRGB($hsl);
1147
	}
1148
1149
	protected function lib_fadeout($args) {
1150
		list($color, $delta) = $this->colorArgs($args);
1151
		$color[4] = $this->clamp((isset($color[4]) ? $color[4] : 1) - $delta/100);
1152
		return $color;
1153
	}
1154
1155
	protected function lib_fadein($args) {
1156
		list($color, $delta) = $this->colorArgs($args);
1157
		$color[4] = $this->clamp((isset($color[4]) ? $color[4] : 1) + $delta/100);
1158
		return $color;
1159
	}
1160
1161
	protected function lib_hue($color) {
1162
		$hsl = $this->toHSL($this->assertColor($color));
1163
		return round($hsl[1]);
1164
	}
1165
1166
	protected function lib_saturation($color) {
1167
		$hsl = $this->toHSL($this->assertColor($color));
1168
		return round($hsl[2]);
1169
	}
1170
1171
	protected function lib_lightness($color) {
1172
		$hsl = $this->toHSL($this->assertColor($color));
1173
		return round($hsl[3]);
1174
	}
1175
1176
	// get the alpha of a color
1177
	// defaults to 1 for non-colors or colors without an alpha
1178
	protected function lib_alpha($value) {
1179
		if (!is_null($color = $this->coerceColor($value))) {
1180
			return isset($color[4]) ? $color[4] : 1;
1181
		}
1182
	}
1183
1184
	// set the alpha of the color
1185
	protected function lib_fade($args) {
1186
		list($color, $alpha) = $this->colorArgs($args);
1187
		$color[4] = $this->clamp($alpha / 100.0);
1188
		return $color;
1189
	}
1190
1191
	protected function lib_percentage($arg) {
1192
		$num = $this->assertNumber($arg);
1193
		return array("number", $num*100, "%");
1194
	}
1195
1196
	// mixes two colors by weight
1197
	// mix(@color1, @color2, [@weight: 50%]);
1198
	// https://sass-lang.com/documentation/functions/color#mix
1199
	protected function lib_mix($args) {
1200
		if ($args[0] != "list" || count($args[2]) < 2)
1201
			$this->throwError("mix expects (color1, color2, weight)");
1202
1203
		list($first, $second) = $args[2];
1204
		$first = $this->assertColor($first);
1205
		$second = $this->assertColor($second);
1206
1207
		$first_a = $this->lib_alpha($first);
1208
		$second_a = $this->lib_alpha($second);
1209
1210
		if (isset($args[2][2])) {
1211
			$weight = $args[2][2][1] / 100.0;
1212
		} else {
1213
			$weight = 0.5;
1214
		}
1215
1216
		$w = $weight * 2 - 1;
1217
		$a = $first_a - $second_a;
1218
1219
		$w1 = (($w * $a == -1 ? $w : ($w + $a)/(1 + $w * $a)) + 1) / 2.0;
1220
		$w2 = 1.0 - $w1;
1221
1222
		$new = array('color',
1223
			$w1 * $first[1] + $w2 * $second[1],
1224
			$w1 * $first[2] + $w2 * $second[2],
1225
			$w1 * $first[3] + $w2 * $second[3],
1226
		);
1227
1228
		if ($first_a != 1.0 || $second_a != 1.0) {
1229
			$new[] = $first_a * $weight + $second_a * ($weight - 1);
1230
		}
1231
1232
		return $this->fixColor($new);
1233
	}
1234
1235
	protected function lib_contrast($args) {
1236
	    $darkColor  = array('color', 0, 0, 0);
1237
	    $lightColor = array('color', 255, 255, 255);
1238
	    $threshold  = 0.43;
1239
1240
	    if ( $args[0] == 'list' ) {
1241
	        $inputColor = ( isset($args[2][0]) ) ? $this->assertColor($args[2][0])  : $lightColor;
1242
	        $darkColor  = ( isset($args[2][1]) ) ? $this->assertColor($args[2][1])  : $darkColor;
1243
	        $lightColor = ( isset($args[2][2]) ) ? $this->assertColor($args[2][2])  : $lightColor;
1244
	        $threshold  = ( isset($args[2][3]) ) ? $this->assertNumber($args[2][3]) : $threshold;
1245
	    }
1246
	    else {
1247
	        $inputColor  = $this->assertColor($args);
1248
	    }
1249
1250
	    $inputColor = $this->coerceColor($inputColor);
1251
	    $darkColor  = $this->coerceColor($darkColor);
1252
	    $lightColor = $this->coerceColor($lightColor);
1253
1254
	    //Figure out which is actually light and dark!
1255
	    if ( $this->lib_luma($darkColor) > $this->lib_luma($lightColor) ) {
1256
	        $t  = $lightColor;
1257
	        $lightColor = $darkColor;
1258
	        $darkColor  = $t;
1259
	    }
1260
1261
	    $inputColor_alpha = $this->lib_alpha($inputColor);
1262
	    if ( ( $this->lib_luma($inputColor) * $inputColor_alpha) < $threshold) {
1263
	        return $lightColor;
1264
	    }
1265
	    return $darkColor;
1266
	}
1267
1268
	protected function lib_luma($color) {
1269
	    $color = $this->coerceColor($color);
1270
	    return (0.2126 * $color[0] / 255) + (0.7152 * $color[1] / 255) + (0.0722 * $color[2] / 255);
1271
	}
1272
1273
1274
	public function assertColor($value, $error = "expected color value") {
1275
		$color = $this->coerceColor($value);
1276
		if (is_null($color)) $this->throwError($error);
1277
		return $color;
1278
	}
1279
1280
	public function assertNumber($value, $error = "expecting number") {
1281
		if ($value[0] == "number") return $value[1];
1282
		$this->throwError($error);
1283
	}
1284
1285
	public function assertArgs($value, $expectedArgs, $name="") {
1286
		if ($expectedArgs == 1) {
1287
			return $value;
1288
		} else {
1289
			if ($value[0] !== "list" || $value[1] != ",") $this->throwError("expecting list");
1290
			$values = $value[2];
1291
			$numValues = count($values);
1292
			if ($expectedArgs != $numValues) {
1293
				if ($name) {
1294
					$name = $name . ": ";
1295
				}
1296
1297
				$this->throwError("${name}expecting $expectedArgs arguments, got $numValues");
1298
			}
1299
1300
			return $values;
1301
		}
1302
	}
1303
1304
	protected function toHSL($color) {
1305
		if ($color[0] == 'hsl') return $color;
1306
1307
		$r = $color[1] / 255;
1308
		$g = $color[2] / 255;
1309
		$b = $color[3] / 255;
1310
1311
		$min = min($r, $g, $b);
1312
		$max = max($r, $g, $b);
1313
1314
		$L = ($min + $max) / 2;
1315
		if ($min == $max) {
1316
			$S = $H = 0;
1317
		} else {
1318
			if ($L < 0.5)
1319
				$S = ($max - $min)/($max + $min);
1320
			else
1321
				$S = ($max - $min)/(2.0 - $max - $min);
1322
1323
			if ($r == $max) $H = ($g - $b)/($max - $min);
1324
			elseif ($g == $max) $H = 2.0 + ($b - $r)/($max - $min);
1325
			elseif ($b == $max) $H = 4.0 + ($r - $g)/($max - $min);
1326
1327
		}
1328
1329
		$out = array('hsl',
1330
			($H < 0 ? $H + 6 : $H)*60,
0 ignored issues
show
Bug introduced by
The variable $H does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
1331
			$S*100,
1332
			$L*100,
1333
		);
1334
1335
		if (count($color) > 4) $out[] = $color[4]; // copy alpha
1336
		return $out;
1337
	}
1338
1339
	protected function toRGB_helper($comp, $temp1, $temp2) {
1340
		if ($comp < 0) $comp += 1.0;
1341
		elseif ($comp > 1) $comp -= 1.0;
1342
1343 View Code Duplication
		if (6 * $comp < 1) return $temp1 + ($temp2 - $temp1) * 6 * $comp;
1344
		if (2 * $comp < 1) return $temp2;
1345 View Code Duplication
		if (3 * $comp < 2) return $temp1 + ($temp2 - $temp1)*((2/3) - $comp) * 6;
1346
1347
		return $temp1;
1348
	}
1349
1350
	/**
1351
	 * Converts a hsl array into a color value in rgb.
1352
	 * Expects H to be in range of 0 to 360, S and L in 0 to 100
1353
	 */
1354
	protected function toRGB($color) {
1355
		if ($color[0] == 'color') return $color;
1356
1357
		$H = $color[1] / 360;
1358
		$S = $color[2] / 100;
1359
		$L = $color[3] / 100;
1360
1361
		if ($S == 0) {
1362
			$r = $g = $b = $L;
1363
		} else {
1364
			$temp2 = $L < 0.5 ?
1365
				$L*(1.0 + $S) :
1366
				$L + $S - $L * $S;
1367
1368
			$temp1 = 2.0 * $L - $temp2;
1369
1370
			$r = $this->toRGB_helper($H + 1/3, $temp1, $temp2);
1371
			$g = $this->toRGB_helper($H, $temp1, $temp2);
1372
			$b = $this->toRGB_helper($H - 1/3, $temp1, $temp2);
1373
		}
1374
1375
		// $out = array('color', round($r*255), round($g*255), round($b*255));
1376
		$out = array('color', $r*255, $g*255, $b*255);
1377
		if (count($color) > 4) $out[] = $color[4]; // copy alpha
1378
		return $out;
1379
	}
1380
1381
	protected function clamp($v, $max = 1, $min = 0) {
1382
		return min($max, max($min, $v));
1383
	}
1384
1385
	/**
1386
	 * Convert the rgb, rgba, hsl color literals of function type
1387
	 * as returned by the parser into values of color type.
1388
	 */
1389
	protected function funcToColor($func) {
1390
		$fname = $func[1];
1391
		if ($func[2][0] != 'list') return false; // need a list of arguments
1392
		$rawComponents = $func[2][2];
1393
1394
		if ($fname == 'hsl' || $fname == 'hsla') {
1395
			$hsl = array('hsl');
1396
			$i = 0;
1397
			foreach ($rawComponents as $c) {
1398
				$val = $this->reduce($c);
1399
				$val = isset( $val[1] ) ? (float) $val[1] : 0;
1400
1401
				if ($i == 0) $clamp = 360;
1402
				elseif ($i < 3) $clamp = 100;
1403
				else $clamp = 1;
1404
1405
				$hsl[] = $this->clamp($val, $clamp);
1406
				$i++;
1407
			}
1408
1409
			while (count($hsl) < 4) $hsl[] = 0;
1410
			return $this->toRGB($hsl);
1411
1412
		} elseif ($fname == 'rgb' || $fname == 'rgba') {
1413
			$components = array();
1414
			$i = 1;
1415
			foreach	($rawComponents as $c) {
1416
				$c = $this->reduce($c);
1417
				if ($i < 4) {
1418 View Code Duplication
					if ($c[0] == "number" && $c[2] == "%") {
1419
						$components[] = 255 * ($c[1] / 100);
1420
					} else {
1421
						$components[] = (float) $c[1];
1422
					}
1423 View Code Duplication
				} elseif ($i == 4) {
1424
					if ($c[0] == "number" && $c[2] == "%") {
1425
						$components[] = 1.0 * ($c[1] / 100);
1426
					} else {
1427
						$components[] = (float) $c[1];
1428
					}
1429
				} else break;
1430
1431
				$i++;
1432
			}
1433
			while (count($components) < 3) $components[] = 0;
1434
			array_unshift($components, 'color');
1435
			return $this->fixColor($components);
1436
		}
1437
1438
		return false;
1439
	}
1440
1441
	protected function reduce($value, $forExpression = false) {
1442
		switch ($value[0]) {
1443
		case "interpolate":
1444
			$reduced = $this->reduce($value[1]);
1445
			$var = $this->compileValue($reduced);
1446
			$res = $this->reduce(array("variable", $this->vPrefix . $var));
1447
1448
			if ($res[0] == "raw_color") {
1449
				$res = $this->coerceColor($res);
1450
			}
1451
1452
			if (empty($value[2])) $res = $this->lib_e($res);
1453
1454
			return $res;
1455
		case "variable":
1456
			$key = $value[1];
1457
			if (is_array($key)) {
1458
				$key = $this->reduce($key);
1459
				$key = $this->vPrefix . $this->compileValue($this->lib_e($key));
1460
			}
1461
1462
			$seen =& $this->env->seenNames;
1463
1464
			if (!empty($seen[$key])) {
1465
				$this->throwError("infinite loop detected: $key");
1466
			}
1467
1468
			$seen[$key] = true;
1469
			$out = $this->reduce($this->get($key));
1470
			$seen[$key] = false;
1471
			return $out;
1472
		case "list":
1473
			foreach ($value[2] as &$item) {
1474
				$item = $this->reduce($item, $forExpression);
1475
			}
1476
			return $value;
1477
		case "expression":
1478
			return $this->evaluate($value);
1479
		case "string":
1480
			foreach ($value[2] as &$part) {
1481
				if (is_array($part)) {
1482
					$strip = $part[0] == "variable";
1483
					$part = $this->reduce($part);
1484
					if ($strip) $part = $this->lib_e($part);
1485
				}
1486
			}
1487
			return $value;
1488
		case "escape":
1489
			list(,$inner) = $value;
1490
			return $this->lib_e($this->reduce($inner));
1491
		case "function":
1492
			$color = $this->funcToColor($value);
1493
			if ($color) return $color;
1494
1495
			list(, $name, $args) = $value;
1496
			if ($name == "%") $name = "_sprintf";
1497
1498
			$f = isset($this->libFunctions[$name]) ?
1499
				$this->libFunctions[$name] : array($this, 'lib_'.str_replace('-', '_', $name));
1500
1501
			if (is_callable($f)) {
1502
				if ($args[0] == 'list')
1503
					$args = self::compressList($args[2], $args[1]);
1504
1505
				$ret = call_user_func($f, $this->reduce($args, true), $this);
1506
1507
				if (is_null($ret)) {
1508
					return array("string", "", array(
1509
						$name, "(", $args, ")"
1510
					));
1511
				}
1512
1513
				// convert to a typed value if the result is a php primitive
1514
				if (is_numeric($ret)) $ret = array('number', $ret, "");
1515
				elseif (!is_array($ret)) $ret = array('keyword', $ret);
1516
1517
				return $ret;
1518
			}
1519
1520
			// plain function, reduce args
1521
			$value[2] = $this->reduce($value[2]);
1522
			return $value;
1523
		case "unary":
1524
			list(, $op, $exp) = $value;
1525
			$exp = $this->reduce($exp);
1526
1527 View Code Duplication
			if ($exp[0] == "number") {
1528
				switch ($op) {
1529
				case "+":
1530
					return $exp;
1531
				case "-":
1532
					$exp[1] *= -1;
1533
					return $exp;
1534
				}
1535
			}
1536
			return array("string", "", array($op, $exp));
1537
		}
1538
1539
		if ($forExpression) {
1540
			switch ($value[0]) {
1541
			case "keyword":
1542
				if ($color = $this->coerceColor($value)) {
1543
					return $color;
1544
				}
1545
				break;
1546
			case "raw_color":
1547
				return $this->coerceColor($value);
1548
			}
1549
		}
1550
1551
		return $value;
1552
	}
1553
1554
1555
	// coerce a value for use in color operation
1556
	protected function coerceColor($value) {
1557
		switch($value[0]) {
1558
			case 'color': return $value;
1559
			case 'raw_color':
1560
				$c = array("color", 0, 0, 0);
1561
				$colorStr = substr($value[1], 1);
1562
				$num = hexdec($colorStr);
1563
				$width = strlen($colorStr) == 3 ? 16 : 256;
1564
1565
				for ($i = 3; $i > 0; $i--) { // 3 2 1
1566
					$t = $num % $width;
1567
					$num /= $width;
1568
1569
					$c[$i] = $t * (256/$width) + $t * floor(16/$width);
1570
				}
1571
1572
				return $c;
1573
			case 'keyword':
1574
				$name = $value[1];
1575
				if (isset(self::$cssColors[$name])) {
1576
					$rgba = explode(',', self::$cssColors[$name]);
1577
1578
					if(isset($rgba[3]))
1579
						return array('color', $rgba[0], $rgba[1], $rgba[2], $rgba[3]);
1580
1581
					return array('color', $rgba[0], $rgba[1], $rgba[2]);
1582
				}
1583
				return null;
1584
		}
1585
	}
1586
1587
	// make something string like into a string
1588 View Code Duplication
	protected function coerceString($value) {
1589
		switch ($value[0]) {
1590
		case "string":
1591
			return $value;
1592
		case "keyword":
1593
			return array("string", "", array($value[1]));
1594
		}
1595
		return null;
1596
	}
1597
1598
	// turn list of length 1 into value type
1599
	protected function flattenList($value) {
1600 View Code Duplication
		if ($value[0] == "list" && count($value[2]) == 1) {
1601
			return $this->flattenList($value[2][0]);
1602
		}
1603
		return $value;
1604
	}
1605
1606
	public function toBool($a) {
1607
		if ($a) return self::$TRUE;
1608
		else return self::$FALSE;
1609
	}
1610
1611
	// evaluate an expression
1612
	protected function evaluate($exp) {
1613
		list(, $op, $left, $right, $whiteBefore, $whiteAfter) = $exp;
1614
1615
		$left = $this->reduce($left, true);
1616
		$right = $this->reduce($right, true);
1617
1618
		if ($leftColor = $this->coerceColor($left)) {
1619
			$left = $leftColor;
1620
		}
1621
1622
		if ($rightColor = $this->coerceColor($right)) {
1623
			$right = $rightColor;
1624
		}
1625
1626
		$ltype = $left[0];
1627
		$rtype = $right[0];
1628
1629
		// operators that work on all types
1630
		if ($op == "and") {
1631
			return $this->toBool($left == self::$TRUE && $right == self::$TRUE);
1632
		}
1633
1634
		if ($op == "=") {
1635
			return $this->toBool($this->eq($left, $right) );
1636
		}
1637
1638
		if ($op == "+" && !is_null($str = $this->stringConcatenate($left, $right))) {
1639
			return $str;
1640
		}
1641
1642
		// type based operators
1643
		$fname = "op_${ltype}_${rtype}";
1644
		if (is_callable(array($this, $fname))) {
1645
			$out = $this->$fname($op, $left, $right);
1646
			if (!is_null($out)) return $out;
1647
		}
1648
1649
		// make the expression look it did before being parsed
1650
		$paddedOp = $op;
1651
		if ($whiteBefore) $paddedOp = " " . $paddedOp;
1652
		if ($whiteAfter) $paddedOp .= " ";
1653
1654
		return array("string", "", array($left, $paddedOp, $right));
1655
	}
1656
1657
	protected function stringConcatenate($left, $right) {
1658 View Code Duplication
		if ($strLeft = $this->coerceString($left)) {
1659
			if ($right[0] == "string") {
1660
				$right[1] = "";
1661
			}
1662
			$strLeft[2][] = $right;
1663
			return $strLeft;
1664
		}
1665
1666
		if ($strRight = $this->coerceString($right)) {
1667
			array_unshift($strRight[2], $left);
1668
			return $strRight;
1669
		}
1670
	}
1671
1672
1673
	// make sure a color's components don't go out of bounds
1674 View Code Duplication
	protected function fixColor($c) {
1675
		foreach (range(1, 3) as $i) {
1676
			if ($c[$i] < 0) $c[$i] = 0;
1677
			if ($c[$i] > 255) $c[$i] = 255;
1678
		}
1679
1680
		return $c;
1681
	}
1682
1683
	protected function op_number_color($op, $lft, $rgt) {
1684
		if ($op == '+' || $op == '*') {
1685
			return $this->op_color_number($op, $rgt, $lft);
1686
		}
1687
	}
1688
1689
	protected function op_color_number($op, $lft, $rgt) {
1690
		if ($rgt[0] == '%') $rgt[1] /= 100;
1691
1692
		return $this->op_color_color($op, $lft,
1693
			array_fill(1, count($lft) - 1, $rgt[1]));
1694
	}
1695
1696
	protected function op_color_color($op, $left, $right) {
1697
		$out = array('color');
1698
		$max = count($left) > count($right) ? count($left) : count($right);
1699
		foreach (range(1, $max - 1) as $i) {
1700
			$lval = isset($left[$i]) ? $left[$i] : 0;
1701
			$rval = isset($right[$i]) ? $right[$i] : 0;
1702
			switch ($op) {
1703
			case '+':
1704
				$out[] = $lval + $rval;
1705
				break;
1706
			case '-':
1707
				$out[] = $lval - $rval;
1708
				break;
1709
			case '*':
1710
				$out[] = $lval * $rval;
1711
				break;
1712
			case '%':
1713
				$out[] = $lval % $rval;
1714
				break;
1715 View Code Duplication
			case '/':
1716
				if ($rval == 0) $this->throwError("evaluate error: can't divide by zero");
1717
				$out[] = $lval / $rval;
1718
				break;
1719
			default:
1720
				$this->throwError('evaluate error: color op number failed on op '.$op);
1721
			}
1722
		}
1723
		return $this->fixColor($out);
1724
	}
1725
1726 View Code Duplication
	function lib_red($color){
1727
		$color = $this->coerceColor($color);
1728
		if (is_null($color)) {
1729
			$this->throwError('color expected for red()');
1730
		}
1731
1732
		return $color[1];
1733
	}
1734
1735 View Code Duplication
	function lib_green($color){
1736
		$color = $this->coerceColor($color);
1737
		if (is_null($color)) {
1738
			$this->throwError('color expected for green()');
1739
		}
1740
1741
		return $color[2];
1742
	}
1743
1744 View Code Duplication
	function lib_blue($color){
1745
		$color = $this->coerceColor($color);
1746
		if (is_null($color)) {
1747
			$this->throwError('color expected for blue()');
1748
		}
1749
1750
		return $color[3];
1751
	}
1752
1753
1754
	// operator on two numbers
1755
	protected function op_number_number($op, $left, $right) {
1756
		$unit = empty($left[2]) ? $right[2] : $left[2];
1757
1758
		$value = 0;
1759
		switch ($op) {
1760
		case '+':
1761
			$value = $left[1] + $right[1];
1762
			break;
1763
		case '*':
1764
			$value = $left[1] * $right[1];
1765
			break;
1766
		case '-':
1767
			$value = $left[1] - $right[1];
1768
			break;
1769
		case '%':
1770
			$value = $left[1] % $right[1];
1771
			break;
1772
		case '/':
1773
			if ($right[1] == 0) $this->throwError('parse error: divide by zero');
1774
			$value = $left[1] / $right[1];
1775
			break;
1776
		case '<':
1777
			return $this->toBool($left[1] < $right[1]);
1778
		case '>':
1779
			return $this->toBool($left[1] > $right[1]);
1780
		case '>=':
1781
			return $this->toBool($left[1] >= $right[1]);
1782
		case '=<':
1783
			return $this->toBool($left[1] <= $right[1]);
1784
		default:
1785
			$this->throwError('parse error: unknown number operator: '.$op);
1786
		}
1787
1788
		return array("number", $value, $unit);
1789
	}
1790
1791
1792
	/* environment functions */
1793
1794
	protected function makeOutputBlock($type, $selectors = null) {
1795
		$b = new stdclass;
1796
		$b->lines = array();
1797
		$b->children = array();
1798
		$b->selectors = $selectors;
1799
		$b->type = $type;
1800
		$b->parent = $this->scope;
1801
		return $b;
1802
	}
1803
1804
	// the state of execution
1805
	protected function pushEnv($block = null) {
1806
		$e = new stdclass;
1807
		$e->parent = $this->env;
1808
		$e->store = array();
1809
		$e->block = $block;
1810
1811
		$this->env = $e;
1812
		return $e;
1813
	}
1814
1815
	// pop something off the stack
1816
	protected function popEnv() {
1817
		$old = $this->env;
1818
		$this->env = $this->env->parent;
1819
		return $old;
1820
	}
1821
1822
	// set something in the current env
1823
	protected function set($name, $value) {
1824
		$this->env->store[$name] = $value;
1825
	}
1826
1827
1828
	// get the highest occurrence entry for a name
1829
	protected function get($name) {
1830
		$current = $this->env;
1831
1832
		$isArguments = $name == $this->vPrefix . 'arguments';
1833
		while ($current) {
1834
			if ($isArguments && isset($current->arguments)) {
1835
				return array('list', ' ', $current->arguments);
1836
			}
1837
1838 View Code Duplication
			if (isset($current->store[$name]))
1839
				return $current->store[$name];
1840
			else {
1841
				$current = isset($current->storeParent) ?
1842
					$current->storeParent : $current->parent;
1843
			}
1844
		}
1845
1846
		$this->throwError("variable $name is undefined");
1847
	}
1848
1849
	// inject array of unparsed strings into environment as variables
1850
	protected function injectVariables($args) {
1851
		$this->pushEnv();
1852
		$parser = new lessc_parser($this, __METHOD__);
1853
		foreach ($args as $name => $strValue) {
1854
			if ($name[0] != '@') $name = '@'.$name;
1855
			$parser->count = 0;
0 ignored issues
show
Bug introduced by
The property count does not seem to exist in lessc_parser.

An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.

If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.

Loading history...
1856
			$parser->buffer = (string)$strValue;
0 ignored issues
show
Bug introduced by
The property buffer does not seem to exist in lessc_parser.

An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.

If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.

Loading history...
1857
			if (!$parser->propertyValue($value)) {
1858
				throw new Exception("failed to parse passed in variable $name: $strValue");
1859
			}
1860
1861
			$this->set($name, $value);
1862
		}
1863
	}
1864
1865
	/**
1866
	 * Initialize any static state, can initialize parser for a file
1867
	 * $opts isn't used yet
1868
	 */
1869
	public function __construct($fname = null) {
1870
		if ($fname !== null) {
1871
			// used for deprecated parse method
1872
			$this->_parseFile = $fname;
0 ignored issues
show
Bug introduced by
The property _parseFile does not exist. Did you maybe forget to declare it?

In PHP it is possible to write to properties without declaring them. For example, the following is perfectly valid PHP code:

class MyClass { }

$x = new MyClass();
$x->foo = true;

Generally, it is a good practice to explictly declare properties to avoid accidental typos and provide IDE auto-completion:

class MyClass {
    public $foo;
}

$x = new MyClass();
$x->foo = true;
Loading history...
1873
		}
1874
	}
1875
1876
	public function compile($string, $name = null) {
1877
		$locale = setlocale(LC_NUMERIC, 0);
1878
		setlocale(LC_NUMERIC, "C");
1879
1880
		$this->parser = $this->makeParser($name);
0 ignored issues
show
Bug introduced by
The property parser does not seem to exist. Did you mean sourceParser?

An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.

If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.

Loading history...
1881
		$root = $this->parser->parse($string);
0 ignored issues
show
Bug introduced by
The property parser does not seem to exist. Did you mean sourceParser?

An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.

If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.

Loading history...
1882
1883
		$this->env = null;
1884
		$this->scope = null;
1885
1886
		$this->formatter = $this->newFormatter();
1887
1888
		if (!empty($this->registeredVars)) {
1889
			$this->injectVariables($this->registeredVars);
1890
		}
1891
1892
		$this->sourceParser = $this->parser; // used for error messages
0 ignored issues
show
Bug introduced by
The property parser does not seem to exist. Did you mean sourceParser?

An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.

If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.

Loading history...
1893
		$this->compileBlock($root);
1894
1895
		ob_start();
1896
		$this->formatter->block($this->scope);
1897
		$out = ob_get_clean();
1898
		setlocale(LC_NUMERIC, $locale);
1899
		return $out;
1900
	}
1901
1902
	public function compileFile($fname, $outFname = null) {
1903
		if (!is_readable($fname)) {
1904
			throw new Exception('load error: failed to find '.$fname);
1905
		}
1906
1907
		$pi = pathinfo($fname);
1908
1909
		$oldImport = $this->importDir;
1910
1911
		$this->importDir = (array)$this->importDir;
0 ignored issues
show
Documentation Bug introduced by
It seems like (array) $this->importDir of type array is incompatible with the declared type string of property $importDir.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
1912
		$this->importDir[] = $pi['dirname'].'/';
1913
1914
		$this->addParsedFile($fname);
1915
1916
		$out = $this->compile(file_get_contents($fname), $fname);
1917
1918
		$this->importDir = $oldImport;
1919
1920
		if ($outFname !== null) {
1921
			return file_put_contents($outFname, $out);
1922
		}
1923
1924
		return $out;
1925
	}
1926
1927
	// compile only if changed input has changed or output doesn't exist
1928
	public function checkedCompile($in, $out) {
1929
		if (!is_file($out) || filemtime($in) > filemtime($out)) {
1930
			$this->compileFile($in, $out);
1931
			return true;
1932
		}
1933
		return false;
1934
	}
1935
1936
	/**
1937
	 * Execute lessphp on a .less file or a lessphp cache structure
1938
	 *
1939
	 * The lessphp cache structure contains information about a specific
1940
	 * less file having been parsed. It can be used as a hint for future
1941
	 * calls to determine whether or not a rebuild is required.
1942
	 *
1943
	 * The cache structure contains two important keys that may be used
1944
	 * externally:
1945
	 *
1946
	 * compiled: The final compiled CSS
1947
	 * updated: The time (in seconds) the CSS was last compiled
1948
	 *
1949
	 * The cache structure is a plain-ol' PHP associative array and can
1950
	 * be serialized and unserialized without a hitch.
1951
	 *
1952
	 * @param mixed $in Input
1953
	 * @param bool $force Force rebuild?
1954
	 * @return array lessphp cache structure
1955
	 */
1956
	public function cachedCompile($in, $force = false) {
1957
		// assume no root
1958
		$root = null;
1959
1960
		if (is_string($in)) {
1961
			$root = $in;
1962
		} elseif (is_array($in) and isset($in['root'])) {
1963
			if ($force or ! isset($in['files'])) {
1964
				// If we are forcing a recompile or if for some reason the
1965
				// structure does not contain any file information we should
1966
				// specify the root to trigger a rebuild.
1967
				$root = $in['root'];
1968
			} elseif (isset($in['files']) and is_array($in['files'])) {
1969
				foreach ($in['files'] as $fname => $ftime ) {
1970
					if (!file_exists($fname) or filemtime($fname) > $ftime) {
1971
						// One of the files we knew about previously has changed
1972
						// so we should look at our incoming root again.
1973
						$root = $in['root'];
1974
						break;
1975
					}
1976
				}
1977
			}
1978
		} else {
1979
			// TODO: Throw an exception? We got neither a string nor something
1980
			// that looks like a compatible lessphp cache structure.
1981
			return null;
1982
		}
1983
1984
		if ($root !== null) {
1985
			// If we have a root value which means we should rebuild.
1986
			$out = array();
1987
			$out['root'] = $root;
1988
			$out['compiled'] = $this->compileFile($root);
1989
			$out['files'] = $this->allParsedFiles();
1990
			$out['updated'] = time();
1991
			return $out;
1992
		} else {
1993
			// No changes, pass back the structure
1994
			// we were given initially.
1995
			return $in;
1996
		}
1997
1998
	}
1999
2000
	// parse and compile buffer
2001
	// This is deprecated
2002
	public function parse($str = null, $initialVariables = null) {
2003
		if (is_array($str)) {
2004
			$initialVariables = $str;
2005
			$str = null;
2006
		}
2007
2008
		$oldVars = $this->registeredVars;
2009
		if ($initialVariables !== null) {
2010
			$this->setVariables($initialVariables);
2011
		}
2012
2013
		if ($str == null) {
2014
			if (empty($this->_parseFile)) {
2015
				throw new exception("nothing to parse");
2016
			}
2017
2018
			$out = $this->compileFile($this->_parseFile);
2019
		} else {
2020
			$out = $this->compile($str);
2021
		}
2022
2023
		$this->registeredVars = $oldVars;
2024
		return $out;
2025
	}
2026
2027
	protected function makeParser($name) {
2028
		$parser = new lessc_parser($this, $name);
2029
		$parser->writeComments = $this->preserveComments;
0 ignored issues
show
Bug introduced by
The property writeComments does not seem to exist in lessc_parser.

An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.

If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.

Loading history...
2030
2031
		return $parser;
2032
	}
2033
2034
	public function setFormatter($name) {
2035
		$this->formatterName = $name;
0 ignored issues
show
Bug introduced by
The property formatterName does not seem to exist. Did you mean formatter?

An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.

If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.

Loading history...
2036
	}
2037
2038
	protected function newFormatter() {
2039
		$className = "lessc_formatter_lessjs";
2040
		if (!empty($this->formatterName)) {
0 ignored issues
show
Bug introduced by
The property formatterName does not seem to exist. Did you mean formatter?

An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.

If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.

Loading history...
2041
			if (!is_string($this->formatterName))
0 ignored issues
show
Bug introduced by
The property formatterName does not seem to exist. Did you mean formatter?

An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.

If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.

Loading history...
2042
				return $this->formatterName;
0 ignored issues
show
Bug introduced by
The property formatterName does not seem to exist. Did you mean formatter?

An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.

If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.

Loading history...
2043
			$className = "lessc_formatter_$this->formatterName";
0 ignored issues
show
Bug introduced by
The property formatterName does not seem to exist. Did you mean formatter?

An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.

If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.

Loading history...
2044
		}
2045
2046
		return new $className;
2047
	}
2048
2049
	public function setPreserveComments($preserve) {
2050
		$this->preserveComments = $preserve;
2051
	}
2052
2053
	public function registerFunction($name, $func) {
2054
		$this->libFunctions[$name] = $func;
2055
	}
2056
2057
	public function unregisterFunction($name) {
2058
		unset($this->libFunctions[$name]);
2059
	}
2060
2061
	public function setVariables($variables) {
2062
		$this->registeredVars = array_merge($this->registeredVars, $variables);
2063
	}
2064
2065
	public function unsetVariable($name) {
2066
		unset($this->registeredVars[$name]);
2067
	}
2068
2069
	public function setImportDir($dirs) {
2070
		$this->importDir = (array)$dirs;
0 ignored issues
show
Documentation Bug introduced by
It seems like (array) $dirs of type array is incompatible with the declared type string of property $importDir.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
2071
	}
2072
2073
	public function addImportDir($dir) {
2074
		$this->importDir = (array)$this->importDir;
0 ignored issues
show
Documentation Bug introduced by
It seems like (array) $this->importDir of type array is incompatible with the declared type string of property $importDir.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
2075
		$this->importDir[] = $dir;
2076
	}
2077
2078
	public function allParsedFiles() {
2079
		return $this->allParsedFiles;
2080
	}
2081
2082
	public function addParsedFile($file) {
2083
		$this->allParsedFiles[realpath($file)] = filemtime($file);
2084
	}
2085
2086
	/**
2087
	 * Uses the current value of $this->count to show line and line number
2088
	 */
2089
	public function throwError($msg = null) {
2090
		if ($this->sourceLoc >= 0) {
2091
			$this->sourceParser->throwError($msg, $this->sourceLoc);
2092
		}
2093
		throw new exception($msg);
2094
	}
2095
2096
	// compile file $in to file $out if $in is newer than $out
2097
	// returns true when it compiles, false otherwise
2098
	public static function ccompile($in, $out, $less = null) {
2099
		if ($less === null) {
2100
			$less = new self;
2101
		}
2102
		return $less->checkedCompile($in, $out);
2103
	}
2104
2105
	public static function cexecute($in, $force = false, $less = null) {
2106
		if ($less === null) {
2107
			$less = new self;
2108
		}
2109
		return $less->cachedCompile($in, $force);
2110
	}
2111
2112
	static protected $cssColors = array(
2113
		'aliceblue' => '240,248,255',
2114
		'antiquewhite' => '250,235,215',
2115
		'aqua' => '0,255,255',
2116
		'aquamarine' => '127,255,212',
2117
		'azure' => '240,255,255',
2118
		'beige' => '245,245,220',
2119
		'bisque' => '255,228,196',
2120
		'black' => '0,0,0',
2121
		'blanchedalmond' => '255,235,205',
2122
		'blue' => '0,0,255',
2123
		'blueviolet' => '138,43,226',
2124
		'brown' => '165,42,42',
2125
		'burlywood' => '222,184,135',
2126
		'cadetblue' => '95,158,160',
2127
		'chartreuse' => '127,255,0',
2128
		'chocolate' => '210,105,30',
2129
		'coral' => '255,127,80',
2130
		'cornflowerblue' => '100,149,237',
2131
		'cornsilk' => '255,248,220',
2132
		'crimson' => '220,20,60',
2133
		'cyan' => '0,255,255',
2134
		'darkblue' => '0,0,139',
2135
		'darkcyan' => '0,139,139',
2136
		'darkgoldenrod' => '184,134,11',
2137
		'darkgray' => '169,169,169',
2138
		'darkgreen' => '0,100,0',
2139
		'darkgrey' => '169,169,169',
2140
		'darkkhaki' => '189,183,107',
2141
		'darkmagenta' => '139,0,139',
2142
		'darkolivegreen' => '85,107,47',
2143
		'darkorange' => '255,140,0',
2144
		'darkorchid' => '153,50,204',
2145
		'darkred' => '139,0,0',
2146
		'darksalmon' => '233,150,122',
2147
		'darkseagreen' => '143,188,143',
2148
		'darkslateblue' => '72,61,139',
2149
		'darkslategray' => '47,79,79',
2150
		'darkslategrey' => '47,79,79',
2151
		'darkturquoise' => '0,206,209',
2152
		'darkviolet' => '148,0,211',
2153
		'deeppink' => '255,20,147',
2154
		'deepskyblue' => '0,191,255',
2155
		'dimgray' => '105,105,105',
2156
		'dimgrey' => '105,105,105',
2157
		'dodgerblue' => '30,144,255',
2158
		'firebrick' => '178,34,34',
2159
		'floralwhite' => '255,250,240',
2160
		'forestgreen' => '34,139,34',
2161
		'fuchsia' => '255,0,255',
2162
		'gainsboro' => '220,220,220',
2163
		'ghostwhite' => '248,248,255',
2164
		'gold' => '255,215,0',
2165
		'goldenrod' => '218,165,32',
2166
		'gray' => '128,128,128',
2167
		'green' => '0,128,0',
2168
		'greenyellow' => '173,255,47',
2169
		'grey' => '128,128,128',
2170
		'honeydew' => '240,255,240',
2171
		'hotpink' => '255,105,180',
2172
		'indianred' => '205,92,92',
2173
		'indigo' => '75,0,130',
2174
		'ivory' => '255,255,240',
2175
		'khaki' => '240,230,140',
2176
		'lavender' => '230,230,250',
2177
		'lavenderblush' => '255,240,245',
2178
		'lawngreen' => '124,252,0',
2179
		'lemonchiffon' => '255,250,205',
2180
		'lightblue' => '173,216,230',
2181
		'lightcoral' => '240,128,128',
2182
		'lightcyan' => '224,255,255',
2183
		'lightgoldenrodyellow' => '250,250,210',
2184
		'lightgray' => '211,211,211',
2185
		'lightgreen' => '144,238,144',
2186
		'lightgrey' => '211,211,211',
2187
		'lightpink' => '255,182,193',
2188
		'lightsalmon' => '255,160,122',
2189
		'lightseagreen' => '32,178,170',
2190
		'lightskyblue' => '135,206,250',
2191
		'lightslategray' => '119,136,153',
2192
		'lightslategrey' => '119,136,153',
2193
		'lightsteelblue' => '176,196,222',
2194
		'lightyellow' => '255,255,224',
2195
		'lime' => '0,255,0',
2196
		'limegreen' => '50,205,50',
2197
		'linen' => '250,240,230',
2198
		'magenta' => '255,0,255',
2199
		'maroon' => '128,0,0',
2200
		'mediumaquamarine' => '102,205,170',
2201
		'mediumblue' => '0,0,205',
2202
		'mediumorchid' => '186,85,211',
2203
		'mediumpurple' => '147,112,219',
2204
		'mediumseagreen' => '60,179,113',
2205
		'mediumslateblue' => '123,104,238',
2206
		'mediumspringgreen' => '0,250,154',
2207
		'mediumturquoise' => '72,209,204',
2208
		'mediumvioletred' => '199,21,133',
2209
		'midnightblue' => '25,25,112',
2210
		'mintcream' => '245,255,250',
2211
		'mistyrose' => '255,228,225',
2212
		'moccasin' => '255,228,181',
2213
		'navajowhite' => '255,222,173',
2214
		'navy' => '0,0,128',
2215
		'oldlace' => '253,245,230',
2216
		'olive' => '128,128,0',
2217
		'olivedrab' => '107,142,35',
2218
		'orange' => '255,165,0',
2219
		'orangered' => '255,69,0',
2220
		'orchid' => '218,112,214',
2221
		'palegoldenrod' => '238,232,170',
2222
		'palegreen' => '152,251,152',
2223
		'paleturquoise' => '175,238,238',
2224
		'palevioletred' => '219,112,147',
2225
		'papayawhip' => '255,239,213',
2226
		'peachpuff' => '255,218,185',
2227
		'peru' => '205,133,63',
2228
		'pink' => '255,192,203',
2229
		'plum' => '221,160,221',
2230
		'powderblue' => '176,224,230',
2231
		'purple' => '128,0,128',
2232
		'rebeccapurple' => '102,51,153',
2233
		'red' => '255,0,0',
2234
		'rosybrown' => '188,143,143',
2235
		'royalblue' => '65,105,225',
2236
		'saddlebrown' => '139,69,19',
2237
		'salmon' => '250,128,114',
2238
		'sandybrown' => '244,164,96',
2239
		'seagreen' => '46,139,87',
2240
		'seashell' => '255,245,238',
2241
		'sienna' => '160,82,45',
2242
		'silver' => '192,192,192',
2243
		'skyblue' => '135,206,235',
2244
		'slateblue' => '106,90,205',
2245
		'slategray' => '112,128,144',
2246
		'slategrey' => '112,128,144',
2247
		'snow' => '255,250,250',
2248
		'springgreen' => '0,255,127',
2249
		'steelblue' => '70,130,180',
2250
		'tan' => '210,180,140',
2251
		'teal' => '0,128,128',
2252
		'thistle' => '216,191,216',
2253
		'tomato' => '255,99,71',
2254
		'transparent' => '0,0,0,0',
2255
		'turquoise' => '64,224,208',
2256
		'violet' => '238,130,238',
2257
		'wheat' => '245,222,179',
2258
		'white' => '255,255,255',
2259
		'whitesmoke' => '245,245,245',
2260
		'yellow' => '255,255,0',
2261
		'yellowgreen' => '154,205,50'
2262
	);
2263
}
2264
2265
// responsible for taking a string of LESS code and converting it into a
2266
// syntax tree
2267
class lessc_parser {
2268
	static protected $nextBlockId = 0; // used to uniquely identify blocks
2269
2270
	static protected $precedence = array(
2271
		'=<' => 0,
2272
		'>=' => 0,
2273
		'=' => 0,
2274
		'<' => 0,
2275
		'>' => 0,
2276
2277
		'+' => 1,
2278
		'-' => 1,
2279
		'*' => 2,
2280
		'/' => 2,
2281
		'%' => 2,
2282
	);
2283
2284
	static protected $whitePattern;
2285
	static protected $commentMulti;
2286
2287
	static protected $commentSingle = "//";
2288
	static protected $commentMultiLeft = "/*";
2289
	static protected $commentMultiRight = "*/";
2290
2291
	// regex string to match any of the operators
2292
	static protected $operatorString;
2293
2294
	// these properties will supress division unless it's inside parenthases
2295
	static protected $supressDivisionProps =
2296
		array('/border-radius$/i', '/^font$/i');
2297
2298
	protected $blockDirectives = array("font-face", "keyframes", "page", "-moz-document", "viewport", "-moz-viewport", "-o-viewport", "-ms-viewport");
2299
	protected $lineDirectives = array("charset");
2300
2301
	/**
2302
	 * if we are in parens we can be more liberal with whitespace around
2303
	 * operators because it must evaluate to a single value and thus is less
2304
	 * ambiguous.
2305
	 *
2306
	 * Consider:
2307
	 *     property1: 10 -5; // is two numbers, 10 and -5
2308
	 *     property2: (10 -5); // should evaluate to 5
2309
	 */
2310
	protected $inParens = false;
2311
2312
	// caches preg escaped literals
2313
	static protected $literalCache = array();
2314
2315
	public function __construct($lessc, $sourceName = null) {
2316
		$this->eatWhiteDefault = true;
0 ignored issues
show
Bug introduced by
The property eatWhiteDefault does not exist. Did you maybe forget to declare it?

In PHP it is possible to write to properties without declaring them. For example, the following is perfectly valid PHP code:

class MyClass { }

$x = new MyClass();
$x->foo = true;

Generally, it is a good practice to explictly declare properties to avoid accidental typos and provide IDE auto-completion:

class MyClass {
    public $foo;
}

$x = new MyClass();
$x->foo = true;
Loading history...
2317
		// reference to less needed for vPrefix, mPrefix, and parentSelector
2318
		$this->lessc = $lessc;
0 ignored issues
show
Bug introduced by
The property lessc does not exist. Did you maybe forget to declare it?

In PHP it is possible to write to properties without declaring them. For example, the following is perfectly valid PHP code:

class MyClass { }

$x = new MyClass();
$x->foo = true;

Generally, it is a good practice to explictly declare properties to avoid accidental typos and provide IDE auto-completion:

class MyClass {
    public $foo;
}

$x = new MyClass();
$x->foo = true;
Loading history...
2319
2320
		$this->sourceName = $sourceName; // name used for error messages
0 ignored issues
show
Bug introduced by
The property sourceName does not exist. Did you maybe forget to declare it?

In PHP it is possible to write to properties without declaring them. For example, the following is perfectly valid PHP code:

class MyClass { }

$x = new MyClass();
$x->foo = true;

Generally, it is a good practice to explictly declare properties to avoid accidental typos and provide IDE auto-completion:

class MyClass {
    public $foo;
}

$x = new MyClass();
$x->foo = true;
Loading history...
2321
2322
		$this->writeComments = false;
0 ignored issues
show
Bug introduced by
The property writeComments does not exist. Did you maybe forget to declare it?

In PHP it is possible to write to properties without declaring them. For example, the following is perfectly valid PHP code:

class MyClass { }

$x = new MyClass();
$x->foo = true;

Generally, it is a good practice to explictly declare properties to avoid accidental typos and provide IDE auto-completion:

class MyClass {
    public $foo;
}

$x = new MyClass();
$x->foo = true;
Loading history...
2323
2324
		if (!self::$operatorString) {
2325
			self::$operatorString =
2326
				'('.implode('|', array_map(array('lessc', 'preg_quote'),
2327
					array_keys(self::$precedence))).')';
2328
2329
			$commentSingle = lessc::preg_quote(self::$commentSingle);
2330
			$commentMultiLeft = lessc::preg_quote(self::$commentMultiLeft);
2331
			$commentMultiRight = lessc::preg_quote(self::$commentMultiRight);
2332
2333
			self::$commentMulti = $commentMultiLeft.'.*?'.$commentMultiRight;
2334
			self::$whitePattern = '/'.$commentSingle.'[^\n]*\s*|('.self::$commentMulti.')\s*|\s+/Ais';
2335
		}
2336
	}
2337
2338
	public function parse($buffer) {
2339
		$this->count = 0;
0 ignored issues
show
Bug introduced by
The property count does not exist. Did you maybe forget to declare it?

In PHP it is possible to write to properties without declaring them. For example, the following is perfectly valid PHP code:

class MyClass { }

$x = new MyClass();
$x->foo = true;

Generally, it is a good practice to explictly declare properties to avoid accidental typos and provide IDE auto-completion:

class MyClass {
    public $foo;
}

$x = new MyClass();
$x->foo = true;
Loading history...
2340
		$this->line = 1;
0 ignored issues
show
Bug introduced by
The property line does not exist. Did you maybe forget to declare it?

In PHP it is possible to write to properties without declaring them. For example, the following is perfectly valid PHP code:

class MyClass { }

$x = new MyClass();
$x->foo = true;

Generally, it is a good practice to explictly declare properties to avoid accidental typos and provide IDE auto-completion:

class MyClass {
    public $foo;
}

$x = new MyClass();
$x->foo = true;
Loading history...
2341
2342
		$this->env = null; // block stack
0 ignored issues
show
Bug introduced by
The property env does not exist. Did you maybe forget to declare it?

In PHP it is possible to write to properties without declaring them. For example, the following is perfectly valid PHP code:

class MyClass { }

$x = new MyClass();
$x->foo = true;

Generally, it is a good practice to explictly declare properties to avoid accidental typos and provide IDE auto-completion:

class MyClass {
    public $foo;
}

$x = new MyClass();
$x->foo = true;
Loading history...
2343
		$this->buffer = $this->writeComments ? $buffer : $this->removeComments($buffer);
0 ignored issues
show
Bug introduced by
The property buffer does not exist. Did you maybe forget to declare it?

In PHP it is possible to write to properties without declaring them. For example, the following is perfectly valid PHP code:

class MyClass { }

$x = new MyClass();
$x->foo = true;

Generally, it is a good practice to explictly declare properties to avoid accidental typos and provide IDE auto-completion:

class MyClass {
    public $foo;
}

$x = new MyClass();
$x->foo = true;
Loading history...
2344
		$this->pushSpecialBlock("root");
2345
		$this->eatWhiteDefault = true;
2346
		$this->seenComments = array();
0 ignored issues
show
Bug introduced by
The property seenComments does not exist. Did you maybe forget to declare it?

In PHP it is possible to write to properties without declaring them. For example, the following is perfectly valid PHP code:

class MyClass { }

$x = new MyClass();
$x->foo = true;

Generally, it is a good practice to explictly declare properties to avoid accidental typos and provide IDE auto-completion:

class MyClass {
    public $foo;
}

$x = new MyClass();
$x->foo = true;
Loading history...
2347
2348
		// trim whitespace on head
2349
		// if (preg_match('/^\s+/', $this->buffer, $m)) {
2350
		// 	$this->line += substr_count($m[0], "\n");
2351
		// 	$this->buffer = ltrim($this->buffer);
2352
		// }
2353
		$this->whitespace();
2354
2355
		// parse the entire file
2356
		while (false !== $this->parseChunk());
2357
2358
		if ($this->count != strlen($this->buffer))
2359
			$this->throwError();
2360
2361
		// TODO report where the block was opened
2362
		if ( !property_exists($this->env, 'parent') || !is_null($this->env->parent) )
2363
			throw new exception('parse error: unclosed block');
2364
2365
		return $this->env;
2366
	}
2367
2368
	/**
2369
	 * Parse a single chunk off the head of the buffer and append it to the
2370
	 * current parse environment.
2371
	 * Returns false when the buffer is empty, or when there is an error.
2372
	 *
2373
	 * This function is called repeatedly until the entire document is
2374
	 * parsed.
2375
	 *
2376
	 * This parser is most similar to a recursive descent parser. Single
2377
	 * functions represent discrete grammatical rules for the language, and
2378
	 * they are able to capture the text that represents those rules.
2379
	 *
2380
	 * Consider the function lessc::keyword(). (all parse functions are
2381
	 * structured the same)
2382
	 *
2383
	 * The function takes a single reference argument. When calling the
2384
	 * function it will attempt to match a keyword on the head of the buffer.
2385
	 * If it is successful, it will place the keyword in the referenced
2386
	 * argument, advance the position in the buffer, and return true. If it
2387
	 * fails then it won't advance the buffer and it will return false.
2388
	 *
2389
	 * All of these parse functions are powered by lessc::match(), which behaves
2390
	 * the same way, but takes a literal regular expression. Sometimes it is
2391
	 * more convenient to use match instead of creating a new function.
2392
	 *
2393
	 * Because of the format of the functions, to parse an entire string of
2394
	 * grammatical rules, you can chain them together using &&.
2395
	 *
2396
	 * But, if some of the rules in the chain succeed before one fails, then
2397
	 * the buffer position will be left at an invalid state. In order to
2398
	 * avoid this, lessc::seek() is used to remember and set buffer positions.
2399
	 *
2400
	 * Before parsing a chain, use $s = $this->seek() to remember the current
2401
	 * position into $s. Then if a chain fails, use $this->seek($s) to
2402
	 * go back where we started.
2403
	 */
2404
	protected function parseChunk() {
2405
		if (empty($this->buffer)) return false;
2406
		$s = $this->seek();
2407
2408
		if ($this->whitespace()) {
2409
			return true;
2410
		}
2411
2412
		// setting a property
2413
		if ($this->keyword($key) && $this->assign() &&
2414
			$this->propertyValue($value, $key) && $this->end())
2415
		{
2416
			$this->append(array('assign', $key, $value), $s);
2417
			return true;
2418
		} else {
2419
			$this->seek($s);
2420
		}
2421
2422
2423
		// look for special css blocks
2424
		if ($this->literal('@', false)) {
2425
			$this->count--;
2426
2427
			// media
2428
			if ($this->literal('@media')) {
2429
				if (($this->mediaQueryList($mediaQueries) || true)
2430
					&& $this->literal('{'))
2431
				{
2432
					$media = $this->pushSpecialBlock("media");
2433
					$media->queries = is_null($mediaQueries) ? array() : $mediaQueries;
2434
					return true;
2435
				} else {
2436
					$this->seek($s);
2437
					return false;
2438
				}
2439
			}
2440
2441
			if ($this->literal("@", false) && $this->keyword($dirName)) {
2442
				if ($this->isDirective($dirName, $this->blockDirectives)) {
2443
					if (($this->openString("{", $dirValue, null, array(";")) || true) &&
2444
						$this->literal("{"))
2445
					{
2446
						$dir = $this->pushSpecialBlock("directive");
2447
						$dir->name = $dirName;
2448
						if (isset($dirValue)) $dir->value = $dirValue;
2449
						return true;
2450
					}
2451
				} elseif ($this->isDirective($dirName, $this->lineDirectives)) {
2452
					if ($this->propertyValue($dirValue) && $this->end()) {
2453
						$this->append(array("directive", $dirName, $dirValue));
2454
						return true;
2455
					}
2456
				}
2457
			}
2458
2459
			$this->seek($s);
2460
		}
2461
2462
		// setting a variable
2463 View Code Duplication
		if ($this->variable($var) && $this->assign() &&
2464
			$this->propertyValue($value) && $this->end())
2465
		{
2466
			$this->append(array('assign', $var, $value), $s);
2467
			return true;
2468
		} else {
2469
			$this->seek($s);
2470
		}
2471
2472
		if ($this->import($importValue)) {
2473
			$this->append($importValue, $s);
2474
			return true;
2475
		}
2476
2477
		// opening parametric mixin
2478
		if ($this->tag($tag, true) && $this->argumentDef($args, $isVararg) &&
2479
			($this->guards($guards) || true) &&
2480
			$this->literal('{'))
2481
		{
2482
			$block = $this->pushBlock($this->fixTags(array($tag)));
2483
			$block->args = $args;
2484
			$block->isVararg = $isVararg;
2485
			if (!empty($guards)) $block->guards = $guards;
2486
			return true;
2487
		} else {
2488
			$this->seek($s);
2489
		}
2490
2491
		// opening a simple block
2492 View Code Duplication
		if ($this->tags($tags) && $this->literal('{', false)) {
2493
			$tags = $this->fixTags($tags);
2494
			$this->pushBlock($tags);
2495
			return true;
2496
		} else {
2497
			$this->seek($s);
2498
		}
2499
2500
		// closing a block
2501
		if ($this->literal('}', false)) {
2502
			try {
2503
				$block = $this->pop();
2504
			} catch (exception $e) {
2505
				$this->seek($s);
2506
				$this->throwError($e->getMessage());
2507
			}
2508
2509
			$hidden = false;
2510
			if (is_null($block->type)) {
2511
				$hidden = true;
2512
				if (!isset($block->args)) {
2513
					foreach ($block->tags as $tag) {
2514
						if (!is_string($tag) || $tag[0] != $this->lessc->mPrefix) {
2515
							$hidden = false;
2516
							break;
2517
						}
2518
					}
2519
				}
2520
2521
				foreach ($block->tags as $tag) {
2522
					if (is_string($tag)) {
2523
						$this->env->children[$tag][] = $block;
2524
					}
2525
				}
2526
			}
2527
2528
			if (!$hidden) {
2529
				$this->append(array('block', $block), $s);
2530
			}
2531
2532
			// this is done here so comments aren't bundled into he block that
2533
			// was just closed
2534
			$this->whitespace();
2535
			return true;
2536
		}
2537
2538
		// mixin
2539
		if ($this->mixinTags($tags) &&
2540
			($this->argumentDef($argv, $isVararg) || true) &&
2541
			($this->keyword($suffix) || true) && $this->end())
2542
		{
2543
			$tags = $this->fixTags($tags);
2544
			$this->append(array('mixin', $tags, $argv, $suffix), $s);
2545
			return true;
2546
		} else {
2547
			$this->seek($s);
2548
		}
2549
2550
		// spare ;
2551
		if ($this->literal(';')) return true;
2552
2553
		return false; // got nothing, throw error
2554
	}
2555
2556
	protected function isDirective($dirname, $directives) {
2557
		// TODO: cache pattern in parser
2558
		$pattern = implode("|",
2559
			array_map(array("lessc", "preg_quote"), $directives));
2560
		$pattern = '/^(-[a-z-]+-)?(' . $pattern . ')$/i';
2561
2562
		return preg_match($pattern, $dirname);
2563
	}
2564
2565
	protected function fixTags($tags) {
2566
		// move @ tags out of variable namespace
2567
		foreach ($tags as &$tag) {
2568
			if ($tag[0] == $this->lessc->vPrefix)
2569
				$tag[0] = $this->lessc->mPrefix;
2570
		}
2571
		return $tags;
2572
	}
2573
2574
	// a list of expressions
2575
	protected function expressionList(&$exps) {
2576
		$values = array();
2577
2578
		while ($this->expression($exp)) {
2579
			$values[] = $exp;
2580
		}
2581
2582
		if (count($values) == 0) return false;
2583
2584
		$exps = lessc::compressList($values, ' ');
2585
		return true;
2586
	}
2587
2588
	/**
2589
	 * Attempt to consume an expression.
2590
	 * @link https://en.wikipedia.org/wiki/Operator-precedence_parser#Pseudo-code
2591
	 */
2592
	protected function expression(&$out) {
2593
		if ($this->value($lhs)) {
2594
			$out = $this->expHelper($lhs, 0);
2595
2596
			// look for / shorthand
2597
			if (!empty($this->env->supressedDivision)) {
2598
				unset($this->env->supressedDivision);
2599
				$s = $this->seek();
2600
				if ($this->literal("/") && $this->value($rhs)) {
2601
					$out = array("list", "",
2602
						array($out, array("keyword", "/"), $rhs));
2603
				} else {
2604
					$this->seek($s);
2605
				}
2606
			}
2607
2608
			return true;
2609
		}
2610
		return false;
2611
	}
2612
2613
	/**
2614
	 * recursively parse infix equation with $lhs at precedence $minP
2615
	 */
2616
	protected function expHelper($lhs, $minP) {
2617
		$this->inExp = true;
0 ignored issues
show
Bug introduced by
The property inExp does not exist. Did you maybe forget to declare it?

In PHP it is possible to write to properties without declaring them. For example, the following is perfectly valid PHP code:

class MyClass { }

$x = new MyClass();
$x->foo = true;

Generally, it is a good practice to explictly declare properties to avoid accidental typos and provide IDE auto-completion:

class MyClass {
    public $foo;
}

$x = new MyClass();
$x->foo = true;
Loading history...
2618
		$ss = $this->seek();
2619
2620
		while (true) {
2621
			$whiteBefore = isset($this->buffer[$this->count - 1]) &&
2622
				ctype_space($this->buffer[$this->count - 1]);
2623
2624
			// If there is whitespace before the operator, then we require
2625
			// whitespace after the operator for it to be an expression
2626
			$needWhite = $whiteBefore && !$this->inParens;
2627
2628
			if ($this->match(self::$operatorString.($needWhite ? '\s' : ''), $m) && self::$precedence[$m[1]] >= $minP) {
2629
				if (!$this->inParens && isset($this->env->currentProperty) && $m[1] == "/" && empty($this->env->supressedDivision)) {
2630
					foreach (self::$supressDivisionProps as $pattern) {
2631
						if (preg_match($pattern, $this->env->currentProperty)) {
2632
							$this->env->supressedDivision = true;
2633
							break 2;
2634
						}
2635
					}
2636
				}
2637
2638
2639
				$whiteAfter = isset($this->buffer[$this->count - 1]) &&
2640
					ctype_space($this->buffer[$this->count - 1]);
2641
2642
				if (!$this->value($rhs)) break;
2643
2644
				// peek for next operator to see what to do with rhs
2645
				if ($this->peek(self::$operatorString, $next) && self::$precedence[$next[1]] > self::$precedence[$m[1]]) {
2646
					$rhs = $this->expHelper($rhs, self::$precedence[$next[1]]);
2647
				}
2648
2649
				$lhs = array('expression', $m[1], $lhs, $rhs, $whiteBefore, $whiteAfter);
2650
				$ss = $this->seek();
2651
2652
				continue;
2653
			}
2654
2655
			break;
2656
		}
2657
2658
		$this->seek($ss);
2659
2660
		return $lhs;
2661
	}
2662
2663
	// consume a list of values for a property
2664
	public function propertyValue(&$value, $keyName = null) {
2665
		$values = array();
2666
2667
		if ($keyName !== null) $this->env->currentProperty = $keyName;
2668
2669
		$s = null;
2670
		while ($this->expressionList($v)) {
2671
			$values[] = $v;
2672
			$s = $this->seek();
2673
			if (!$this->literal(',')) break;
2674
		}
2675
2676
		if ($s) $this->seek($s);
2677
2678
		if ($keyName !== null) unset($this->env->currentProperty);
2679
2680
		if (count($values) == 0) return false;
2681
2682
		$value = lessc::compressList($values, ', ');
2683
		return true;
2684
	}
2685
2686
	protected function parenValue(&$out) {
2687
		$s = $this->seek();
2688
2689
		// speed shortcut
2690 View Code Duplication
		if (isset($this->buffer[$this->count]) && $this->buffer[$this->count] != "(") {
2691
			return false;
2692
		}
2693
2694
		$inParens = $this->inParens;
2695 View Code Duplication
		if ($this->literal("(") &&
2696
			($this->inParens = true) && $this->expression($exp) &&
2697
			$this->literal(")"))
2698
		{
2699
			$out = $exp;
2700
			$this->inParens = $inParens;
2701
			return true;
2702
		} else {
2703
			$this->inParens = $inParens;
2704
			$this->seek($s);
2705
		}
2706
2707
		return false;
2708
	}
2709
2710
	// a single value
2711
	protected function value(&$value) {
2712
		$s = $this->seek();
2713
2714
		// speed shortcut
2715
		if (isset($this->buffer[$this->count]) && $this->buffer[$this->count] == "-") {
2716
			// negation
2717
			if ($this->literal("-", false) &&
2718
				(($this->variable($inner) && $inner = array("variable", $inner)) ||
2719
				$this->unit($inner) ||
2720
				$this->parenValue($inner)))
2721
			{
2722
				$value = array("unary", "-", $inner);
2723
				return true;
2724
			} else {
2725
				$this->seek($s);
2726
			}
2727
		}
2728
2729
		if ($this->parenValue($value)) return true;
2730
		if ($this->unit($value)) return true;
2731
		if ($this->color($value)) return true;
2732
		if ($this->func($value)) return true;
2733
		if ($this->string($value)) return true;
2734
2735
		if ($this->keyword($word)) {
2736
			$value = array('keyword', $word);
2737
			return true;
2738
		}
2739
2740
		// try a variable
2741
		if ($this->variable($var)) {
2742
			$value = array('variable', $var);
2743
			return true;
2744
		}
2745
2746
		// unquote string (should this work on any type?
2747
		if ($this->literal("~") && $this->string($str)) {
2748
			$value = array("escape", $str);
2749
			return true;
2750
		} else {
2751
			$this->seek($s);
2752
		}
2753
2754
		// css hack: \0
2755
		if ($this->literal('\\') && $this->match('([0-9]+)', $m)) {
2756
			$value = array('keyword', '\\'.$m[1]);
2757
			return true;
2758
		} else {
2759
			$this->seek($s);
2760
		}
2761
2762
		return false;
2763
	}
2764
2765
	// an import statement
2766
	protected function import(&$out) {
2767
		if (!$this->literal('@import')) return false;
2768
2769
		// @import "something.css" media;
2770
		// @import url("something.css") media;
2771
		// @import url(something.css) media;
2772
2773
		if ($this->propertyValue($value)) {
2774
			$out = array("import", $value);
2775
			return true;
2776
		}
2777
	}
2778
2779
	protected function mediaQueryList(&$out) {
2780
		if ($this->genericList($list, "mediaQuery", ",", false)) {
2781
			$out = $list[2];
2782
			return true;
2783
		}
2784
		return false;
2785
	}
2786
2787
	protected function mediaQuery(&$out) {
2788
		$s = $this->seek();
2789
2790
		$expressions = null;
2791
		$parts = array();
2792
2793
		if (($this->literal("only") && ($only = true) || $this->literal("not") && ($not = true) || true) && $this->keyword($mediaType)) {
2794
			$prop = array("mediaType");
2795
			if (isset($only)) $prop[] = "only";
2796
			if (isset($not)) $prop[] = "not";
2797
			$prop[] = $mediaType;
2798
			$parts[] = $prop;
2799
		} else {
2800
			$this->seek($s);
2801
		}
2802
2803
2804 View Code Duplication
		if (!empty($mediaType) && !$this->literal("and")) {
2805
			// ~
2806
		} else {
2807
			$this->genericList($expressions, "mediaExpression", "and", false);
2808
			if (is_array($expressions)) $parts = array_merge($parts, $expressions[2]);
2809
		}
2810
2811
		if (count($parts) == 0) {
2812
			$this->seek($s);
2813
			return false;
2814
		}
2815
2816
		$out = $parts;
2817
		return true;
2818
	}
2819
2820
	protected function mediaExpression(&$out) {
2821
		$s = $this->seek();
2822
		$value = null;
2823
		if ($this->literal("(") &&
2824
			$this->keyword($feature) &&
2825
			($this->literal(":") && $this->expression($value) || true) &&
2826
			$this->literal(")"))
2827
		{
2828
			$out = array("mediaExp", $feature);
2829
			if ($value) $out[] = $value;
2830
			return true;
2831
		} elseif ($this->variable($variable)) {
2832
			$out = array('variable', $variable);
2833
			return true;
2834
		}
2835
2836
		$this->seek($s);
2837
		return false;
2838
	}
2839
2840
	// an unbounded string stopped by $end
2841
	protected function openString($end, &$out, $nestingOpen=null, $rejectStrs = null) {
2842
		$oldWhite = $this->eatWhiteDefault;
2843
		$this->eatWhiteDefault = false;
2844
2845
		$stop = array("'", '"', "@{", $end);
2846
		$stop = array_map(array("lessc", "preg_quote"), $stop);
2847
		// $stop[] = self::$commentMulti;
2848
2849
		if (!is_null($rejectStrs)) {
2850
			$stop = array_merge($stop, $rejectStrs);
2851
		}
2852
2853
		$patt = '(.*?)('.implode("|", $stop).')';
2854
2855
		$nestingLevel = 0;
2856
2857
		$content = array();
2858
		while ($this->match($patt, $m, false)) {
2859
			if (!empty($m[1])) {
2860
				$content[] = $m[1];
2861
				if ($nestingOpen) {
2862
					$nestingLevel += substr_count($m[1], $nestingOpen);
2863
				}
2864
			}
2865
2866
			$tok = $m[2];
2867
2868
			$this->count-= strlen($tok);
2869
			if ($tok == $end) {
2870
				if ($nestingLevel == 0) {
2871
					break;
2872
				} else {
2873
					$nestingLevel--;
2874
				}
2875
			}
2876
2877 View Code Duplication
			if (($tok == "'" || $tok == '"') && $this->string($str)) {
2878
				$content[] = $str;
2879
				continue;
2880
			}
2881
2882
			if ($tok == "@{" && $this->interpolation($inter)) {
2883
				$content[] = $inter;
2884
				continue;
2885
			}
2886
2887
			if (!empty($rejectStrs) && in_array($tok, $rejectStrs)) {
2888
				break;
2889
			}
2890
2891
			$content[] = $tok;
2892
			$this->count+= strlen($tok);
2893
		}
2894
2895
		$this->eatWhiteDefault = $oldWhite;
2896
2897
		if (count($content) == 0) return false;
2898
2899
		// trim the end
2900 View Code Duplication
		if (is_string(end($content))) {
2901
			$content[count($content) - 1] = rtrim(end($content));
2902
		}
2903
2904
		$out = array("string", "", $content);
2905
		return true;
2906
	}
2907
2908
	protected function string(&$out) {
2909
		$s = $this->seek();
2910 View Code Duplication
		if ($this->literal('"', false)) {
2911
			$delim = '"';
2912
		} elseif ($this->literal("'", false)) {
2913
			$delim = "'";
2914
		} else {
2915
			return false;
2916
		}
2917
2918
		$content = array();
2919
2920
		// look for either ending delim , escape, or string interpolation
2921
		$patt = '([^\n]*?)(@\{|\\\\|' .
2922
			lessc::preg_quote($delim).')';
2923
2924
		$oldWhite = $this->eatWhiteDefault;
2925
		$this->eatWhiteDefault = false;
2926
2927 View Code Duplication
		while ($this->match($patt, $m, false)) {
2928
			$content[] = $m[1];
2929
			if ($m[2] == "@{") {
2930
				$this->count -= strlen($m[2]);
2931
				if ($this->interpolation($inter, false)) {
0 ignored issues
show
Unused Code introduced by
The call to lessc_parser::interpolation() has too many arguments starting with false.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
2932
					$content[] = $inter;
2933
				} else {
2934
					$this->count += strlen($m[2]);
2935
					$content[] = "@{"; // ignore it
2936
				}
2937
			} elseif ($m[2] == '\\') {
2938
				$content[] = $m[2];
2939
				if ($this->literal($delim, false)) {
2940
					$content[] = $delim;
2941
				}
2942
			} else {
2943
				$this->count -= strlen($delim);
2944
				break; // delim
2945
			}
2946
		}
2947
2948
		$this->eatWhiteDefault = $oldWhite;
2949
2950 View Code Duplication
		if ($this->literal($delim)) {
2951
			$out = array("string", $delim, $content);
2952
			return true;
2953
		}
2954
2955
		$this->seek($s);
2956
		return false;
2957
	}
2958
2959
	protected function interpolation(&$out) {
2960
		$oldWhite = $this->eatWhiteDefault;
2961
		$this->eatWhiteDefault = true;
2962
2963
		$s = $this->seek();
2964
		if ($this->literal("@{") &&
2965
			$this->openString("}", $interp, null, array("'", '"', ";")) &&
2966
			$this->literal("}", false))
2967
		{
2968
			$out = array("interpolate", $interp);
2969
			$this->eatWhiteDefault = $oldWhite;
2970
			if ($this->eatWhiteDefault) $this->whitespace();
2971
			return true;
2972
		}
2973
2974
		$this->eatWhiteDefault = $oldWhite;
2975
		$this->seek($s);
2976
		return false;
2977
	}
2978
2979
	protected function unit(&$unit) {
2980
		// speed shortcut
2981
		if (isset($this->buffer[$this->count])) {
2982
			$char = $this->buffer[$this->count];
2983
			if (!ctype_digit($char) && $char != ".") return false;
2984
		}
2985
2986 View Code Duplication
		if ($this->match('([0-9]+(?:\.[0-9]*)?|\.[0-9]+)([%a-zA-Z]+)?', $m)) {
2987
			$unit = array("number", $m[1], empty($m[2]) ? "" : $m[2]);
2988
			return true;
2989
		}
2990
		return false;
2991
	}
2992
2993
	// a # color
2994
	protected function color(&$out) {
2995
		if ($this->match('(#(?:[0-9a-f]{8}|[0-9a-f]{6}|[0-9a-f]{3}))', $m)) {
2996
			if (strlen($m[1]) > 7) {
2997
				$out = array("string", "", array($m[1]));
2998
			} else {
2999
				$out = array("raw_color", $m[1]);
3000
			}
3001
			return true;
3002
		}
3003
3004
		return false;
3005
	}
3006
3007
	// consume an argument definition list surrounded by ()
3008
	// each argument is a variable name with optional value
3009
	// or at the end a ... or a variable named followed by ...
3010
	// arguments are separated by , unless a ; is in the list, then ; is the
3011
	// delimiter.
3012
	protected function argumentDef(&$args, &$isVararg) {
3013
		$s = $this->seek();
3014
		if (!$this->literal('(')) return false;
3015
3016
		$values = array();
3017
		$delim = ",";
3018
		$method = "expressionList";
3019
3020
		$isVararg = false;
3021
		while (true) {
3022
			if ($this->literal("...")) {
3023
				$isVararg = true;
3024
				break;
3025
			}
3026
3027
			if ($this->$method($value)) {
0 ignored issues
show
Bug introduced by
The variable $value does not exist. Did you mean $values?

This check looks for variables that are accessed but have not been defined. It raises an issue if it finds another variable that has a similar name.

The variable may have been renamed without also renaming all references.

Loading history...
3028
				if ($value[0] == "variable") {
0 ignored issues
show
Bug introduced by
The variable $value does not exist. Did you mean $values?

This check looks for variables that are accessed but have not been defined. It raises an issue if it finds another variable that has a similar name.

The variable may have been renamed without also renaming all references.

Loading history...
3029
					$arg = array("arg", $value[1]);
0 ignored issues
show
Bug introduced by
The variable $value does not exist. Did you mean $values?

This check looks for variables that are accessed but have not been defined. It raises an issue if it finds another variable that has a similar name.

The variable may have been renamed without also renaming all references.

Loading history...
3030
					$ss = $this->seek();
3031
3032
					if ($this->assign() && $this->$method($rhs)) {
3033
						$arg[] = $rhs;
0 ignored issues
show
Bug introduced by
The variable $rhs does not exist. Did you forget to declare it?

This check marks access to variables or properties that have not been declared yet. While PHP has no explicit notion of declaring a variable, accessing it before a value is assigned to it is most likely a bug.

Loading history...
3034
					} else {
3035
						$this->seek($ss);
3036
						if ($this->literal("...")) {
3037
							$arg[0] = "rest";
3038
							$isVararg = true;
3039
						}
3040
					}
3041
3042
					$values[] = $arg;
3043
					if ($isVararg) break;
3044
					continue;
3045
				} else {
3046
					$values[] = array("lit", $value);
0 ignored issues
show
Bug introduced by
The variable $value does not exist. Did you mean $values?

This check looks for variables that are accessed but have not been defined. It raises an issue if it finds another variable that has a similar name.

The variable may have been renamed without also renaming all references.

Loading history...
3047
				}
3048
			}
3049
3050
3051
			if (!$this->literal($delim)) {
3052
				if ($delim == "," && $this->literal(";")) {
3053
					// found new delim, convert existing args
3054
					$delim = ";";
3055
					$method = "propertyValue";
3056
3057
					// transform arg list
3058
					if (isset($values[1])) { // 2 items
3059
						$newList = array();
3060
						foreach ($values as $i => $arg) {
3061
							switch($arg[0]) {
3062
							case "arg":
3063
								if ($i) {
3064
									$this->throwError("Cannot mix ; and , as delimiter types");
3065
								}
3066
								$newList[] = $arg[2];
3067
								break;
3068
							case "lit":
3069
								$newList[] = $arg[1];
3070
								break;
3071
							case "rest":
3072
								$this->throwError("Unexpected rest before semicolon");
3073
							}
3074
						}
3075
3076
						$newList = array("list", ", ", $newList);
3077
3078
						switch ($values[0][0]) {
3079
						case "arg":
3080
							$newArg = array("arg", $values[0][1], $newList);
3081
							break;
3082
						case "lit":
3083
							$newArg = array("lit", $newList);
3084
							break;
3085
						}
3086
3087
					} elseif ($values) { // 1 item
0 ignored issues
show
Bug Best Practice introduced by
The expression $values of type null[] is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
3088
						$newArg = $values[0];
3089
					}
3090
3091
					if ($newArg) {
3092
						$values = array($newArg);
0 ignored issues
show
Bug introduced by
The variable $newArg does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
3093
					}
3094
				} else {
3095
					break;
3096
				}
3097
			}
3098
		}
3099
3100
		if (!$this->literal(')')) {
3101
			$this->seek($s);
3102
			return false;
3103
		}
3104
3105
		$args = $values;
3106
3107
		return true;
3108
	}
3109
3110
	// consume a list of tags
3111
	// this accepts a hanging delimiter
3112 View Code Duplication
	protected function tags(&$tags, $simple = false, $delim = ',') {
3113
		$tags = array();
3114
		while ($this->tag($tt, $simple)) {
3115
			$tags[] = $tt;
3116
			if (!$this->literal($delim)) break;
3117
		}
3118
		if (count($tags) == 0) return false;
3119
3120
		return true;
3121
	}
3122
3123
	// list of tags of specifying mixin path
3124
	// optionally separated by > (lazy, accepts extra >)
3125 View Code Duplication
	protected function mixinTags(&$tags) {
3126
		$tags = array();
3127
		while ($this->tag($tt, true)) {
3128
			$tags[] = $tt;
3129
			$this->literal(">");
3130
		}
3131
3132
		if (count($tags) == 0) return false;
3133
3134
		return true;
3135
	}
3136
3137
	// a bracketed value (contained within in a tag definition)
3138
	protected function tagBracket(&$parts, &$hasExpression) {
3139
		// speed shortcut
3140 View Code Duplication
		if (isset($this->buffer[$this->count]) && $this->buffer[$this->count] != "[") {
3141
			return false;
3142
		}
3143
3144
		$s = $this->seek();
3145
3146
		$hasInterpolation = false;
3147
3148
		if ($this->literal("[", false)) {
3149
			$attrParts = array("[");
3150
			// keyword, string, operator
3151
			while (true) {
3152
				if ($this->literal("]", false)) {
3153
					$this->count--;
3154
					break; // get out early
3155
				}
3156
3157
				if ($this->match('\s+', $m)) {
3158
					$attrParts[] = " ";
3159
					continue;
3160
				}
3161
				if ($this->string($str)) {
3162
					// escape parent selector, (yuck)
3163
					foreach ($str[2] as &$chunk) {
3164
						$chunk = str_replace($this->lessc->parentSelector, "$&$", $chunk);
3165
					}
3166
3167
					$attrParts[] = $str;
3168
					$hasInterpolation = true;
3169
					continue;
3170
				}
3171
3172
				if ($this->keyword($word)) {
3173
					$attrParts[] = $word;
3174
					continue;
3175
				}
3176
3177
				if ($this->interpolation($inter, false)) {
0 ignored issues
show
Unused Code introduced by
The call to lessc_parser::interpolation() has too many arguments starting with false.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
3178
					$attrParts[] = $inter;
3179
					$hasInterpolation = true;
3180
					continue;
3181
				}
3182
3183
				// operator, handles attr namespace too
3184
				if ($this->match('[|-~\$\*\^=]+', $m)) {
3185
					$attrParts[] = $m[0];
3186
					continue;
3187
				}
3188
3189
				break;
3190
			}
3191
3192
			if ($this->literal("]", false)) {
3193
				$attrParts[] = "]";
3194
				foreach ($attrParts as $part) {
3195
					$parts[] = $part;
3196
				}
3197
				$hasExpression = $hasExpression || $hasInterpolation;
3198
				return true;
3199
			}
3200
			$this->seek($s);
3201
		}
3202
3203
		$this->seek($s);
3204
		return false;
3205
	}
3206
3207
	// a space separated list of selectors
3208
	protected function tag(&$tag, $simple = false) {
3209
		if ($simple)
3210
			$chars = '^@,:;{}\][>\(\) "\'';
3211
		else
3212
			$chars = '^@,;{}["\'';
3213
3214
		$s = $this->seek();
3215
3216
		$hasExpression = false;
3217
		$parts = array();
3218
		while ($this->tagBracket($parts, $hasExpression));
3219
3220
		$oldWhite = $this->eatWhiteDefault;
3221
		$this->eatWhiteDefault = false;
3222
3223
		while (true) {
3224
			if ($this->match('(['.$chars.'0-9]['.$chars.']*)', $m)) {
3225
				$parts[] = $m[1];
3226
				if ($simple) break;
3227
3228
				while ($this->tagBracket($parts, $hasExpression));
3229
				continue;
3230
			}
3231
3232
			if (isset($this->buffer[$this->count]) && $this->buffer[$this->count] == "@") {
3233
				if ($this->interpolation($interp)) {
3234
					$hasExpression = true;
3235
					$interp[2] = true; // don't unescape
3236
					$parts[] = $interp;
3237
					continue;
3238
				}
3239
3240
				if ($this->literal("@")) {
3241
					$parts[] = "@";
3242
					continue;
3243
				}
3244
			}
3245
3246
			if ($this->unit($unit)) { // for keyframes
3247
				$parts[] = $unit[1];
3248
				$parts[] = $unit[2];
3249
				continue;
3250
			}
3251
3252
			break;
3253
		}
3254
3255
		$this->eatWhiteDefault = $oldWhite;
3256
		if (!$parts) {
3257
			$this->seek($s);
3258
			return false;
3259
		}
3260
3261
		if ($hasExpression) {
3262
			$tag = array("exp", array("string", "", $parts));
3263
		} else {
3264
			$tag = trim(implode($parts));
3265
		}
3266
3267
		$this->whitespace();
3268
		return true;
3269
	}
3270
3271
	// a css function
3272
	protected function func(&$func) {
3273
		$s = $this->seek();
3274
3275
		if ($this->match('(%|[\w\-_][\w\-_:\.]+|[\w_])', $m) && $this->literal('(')) {
3276
			$fname = $m[1];
3277
3278
			$sPreArgs = $this->seek();
3279
3280
			$args = array();
3281
			while (true) {
3282
				$ss = $this->seek();
3283
				// this ugly nonsense is for ie filter properties
3284
				if ($this->keyword($name) && $this->literal('=') && $this->expressionList($value)) {
3285
					$args[] = array("string", "", array($name, "=", $value));
3286
				} else {
3287
					$this->seek($ss);
3288
					if ($this->expressionList($value)) {
3289
						$args[] = $value;
3290
					}
3291
				}
3292
3293
				if (!$this->literal(',')) break;
3294
			}
3295
			$args = array('list', ',', $args);
3296
3297
			if ($this->literal(')')) {
3298
				$func = array('function', $fname, $args);
3299
				return true;
3300
			} elseif ($fname == 'url') {
3301
				// couldn't parse and in url? treat as string
3302
				$this->seek($sPreArgs);
3303
				if ($this->openString(")", $string) && $this->literal(")")) {
3304
					$func = array('function', $fname, $string);
3305
					return true;
3306
				}
3307
			}
3308
		}
3309
3310
		$this->seek($s);
3311
		return false;
3312
	}
3313
3314
	// consume a less variable
3315
	protected function variable(&$name) {
3316
		$s = $this->seek();
3317
		if ($this->literal($this->lessc->vPrefix, false) &&
3318
			($this->variable($sub) || $this->keyword($name)))
3319
		{
3320
			if (!empty($sub)) {
3321
				$name = array('variable', $sub);
3322
			} else {
3323
				$name = $this->lessc->vPrefix.$name;
3324
			}
3325
			return true;
3326
		}
3327
3328
		$name = null;
3329
		$this->seek($s);
3330
		return false;
3331
	}
3332
3333
	/**
3334
	 * Consume an assignment operator
3335
	 * Can optionally take a name that will be set to the current property name
3336
	 */
3337
	protected function assign($name = null) {
3338
		if ($name) $this->currentProperty = $name;
0 ignored issues
show
Bug introduced by
The property currentProperty does not exist. Did you maybe forget to declare it?

In PHP it is possible to write to properties without declaring them. For example, the following is perfectly valid PHP code:

class MyClass { }

$x = new MyClass();
$x->foo = true;

Generally, it is a good practice to explictly declare properties to avoid accidental typos and provide IDE auto-completion:

class MyClass {
    public $foo;
}

$x = new MyClass();
$x->foo = true;
Loading history...
3339
		return $this->literal(':') || $this->literal('=');
3340
	}
3341
3342
	// consume a keyword
3343 View Code Duplication
	protected function keyword(&$word) {
3344
		if ($this->match('([\w_\-\*!"][\w\-_"]*)', $m)) {
3345
			$word = $m[1];
3346
			return true;
3347
		}
3348
		return false;
3349
	}
3350
3351
	// consume an end of statement delimiter
3352 View Code Duplication
	protected function end() {
3353
		if ($this->literal(';', false)) {
3354
			return true;
3355
		} elseif ($this->count == strlen($this->buffer) || $this->buffer[$this->count] == '}') {
3356
			// if there is end of file or a closing block next then we don't need a ;
3357
			return true;
3358
		}
3359
		return false;
3360
	}
3361
3362
	protected function guards(&$guards) {
3363
		$s = $this->seek();
3364
3365
		if (!$this->literal("when")) {
3366
			$this->seek($s);
3367
			return false;
3368
		}
3369
3370
		$guards = array();
3371
3372
		while ($this->guardGroup($g)) {
3373
			$guards[] = $g;
3374
			if (!$this->literal(",")) break;
3375
		}
3376
3377
		if (count($guards) == 0) {
3378
			$guards = null;
3379
			$this->seek($s);
3380
			return false;
3381
		}
3382
3383
		return true;
3384
	}
3385
3386
	// a bunch of guards that are and'd together
3387
	// TODO rename to guardGroup
3388 View Code Duplication
	protected function guardGroup(&$guardGroup) {
3389
		$s = $this->seek();
3390
		$guardGroup = array();
3391
		while ($this->guard($guard)) {
3392
			$guardGroup[] = $guard;
3393
			if (!$this->literal("and")) break;
3394
		}
3395
3396
		if (count($guardGroup) == 0) {
3397
			$guardGroup = null;
3398
			$this->seek($s);
3399
			return false;
3400
		}
3401
3402
		return true;
3403
	}
3404
3405
	protected function guard(&$guard) {
3406
		$s = $this->seek();
3407
		$negate = $this->literal("not");
3408
3409
		if ($this->literal("(") && $this->expression($exp) && $this->literal(")")) {
3410
			$guard = $exp;
3411
			if ($negate) $guard = array("negate", $guard);
3412
			return true;
3413
		}
3414
3415
		$this->seek($s);
3416
		return false;
3417
	}
3418
3419
	/* raw parsing functions */
3420
3421
	protected function literal($what, $eatWhitespace = null) {
3422
		if ($eatWhitespace === null) $eatWhitespace = $this->eatWhiteDefault;
3423
3424
		// shortcut on single letter
3425 View Code Duplication
		if (!isset($what[1]) && isset($this->buffer[$this->count])) {
3426
			if ($this->buffer[$this->count] == $what) {
3427
				if (!$eatWhitespace) {
3428
					$this->count++;
3429
					return true;
3430
				}
3431
				// goes below...
3432
			} else {
3433
				return false;
3434
			}
3435
		}
3436
3437
		if (!isset(self::$literalCache[$what])) {
3438
			self::$literalCache[$what] = lessc::preg_quote($what);
3439
		}
3440
3441
		return $this->match(self::$literalCache[$what], $m, $eatWhitespace);
3442
	}
3443
3444 View Code Duplication
	protected function genericList(&$out, $parseItem, $delim="", $flatten=true) {
3445
		$s = $this->seek();
3446
		$items = array();
3447
		while ($this->$parseItem($value)) {
0 ignored issues
show
Bug introduced by
The variable $value does not exist. Did you forget to declare it?

This check marks access to variables or properties that have not been declared yet. While PHP has no explicit notion of declaring a variable, accessing it before a value is assigned to it is most likely a bug.

Loading history...
3448
			$items[] = $value;
3449
			if ($delim) {
3450
				if (!$this->literal($delim)) break;
3451
			}
3452
		}
3453
3454
		if (count($items) == 0) {
3455
			$this->seek($s);
3456
			return false;
3457
		}
3458
3459
		if ($flatten && count($items) == 1) {
3460
			$out = $items[0];
3461
		} else {
3462
			$out = array("list", $delim, $items);
3463
		}
3464
3465
		return true;
3466
	}
3467
3468
3469
	// advance counter to next occurrence of $what
3470
	// $until - don't include $what in advance
3471
	// $allowNewline, if string, will be used as valid char set
3472 View Code Duplication
	protected function to($what, &$out, $until = false, $allowNewline = false) {
3473
		if (is_string($allowNewline)) {
3474
			$validChars = $allowNewline;
3475
		} else {
3476
			$validChars = $allowNewline ? "." : "[^\n]";
3477
		}
3478
		if (!$this->match('('.$validChars.'*?)'.lessc::preg_quote($what), $m, !$until)) return false;
3479
		if ($until) $this->count -= strlen($what); // give back $what
3480
		$out = $m[1];
3481
		return true;
3482
	}
3483
3484
	// try to match something on head of buffer
3485
	protected function match($regex, &$out, $eatWhitespace = null) {
3486
		if ($eatWhitespace === null) $eatWhitespace = $this->eatWhiteDefault;
3487
3488
		$r = '/'.$regex.($eatWhitespace && !$this->writeComments ? '\s*' : '').'/Ais';
3489 View Code Duplication
		if (preg_match($r, $this->buffer, $out, null, $this->count)) {
3490
			$this->count += strlen($out[0]);
3491
			if ($eatWhitespace && $this->writeComments) $this->whitespace();
3492
			return true;
3493
		}
3494
		return false;
3495
	}
3496
3497
	// match some whitespace
3498
	protected function whitespace() {
3499
		if ($this->writeComments) {
3500
			$gotWhite = false;
3501 View Code Duplication
			while (preg_match(self::$whitePattern, $this->buffer, $m, null, $this->count)) {
3502
				if (isset($m[1]) && empty($this->seenComments[$this->count])) {
3503
					$this->append(array("comment", $m[1]));
3504
					$this->seenComments[$this->count] = true;
3505
				}
3506
				$this->count += strlen($m[0]);
3507
				$gotWhite = true;
3508
			}
3509
			return $gotWhite;
3510
		} else {
3511
			$this->match("", $m);
3512
			return strlen($m[0]) > 0;
3513
		}
3514
	}
3515
3516
	// match something without consuming it
3517 View Code Duplication
	protected function peek($regex, &$out = null, $from=null) {
3518
		if (is_null($from)) $from = $this->count;
3519
		$r = '/'.$regex.'/Ais';
3520
		$result = preg_match($r, $this->buffer, $out, null, $from);
3521
3522
		return $result;
3523
	}
3524
3525
	// seek to a spot in the buffer or return where we are on no argument
3526
	protected function seek($where = null) {
3527
		if ($where === null) return $this->count;
3528
		else $this->count = $where;
3529
		return true;
3530
	}
3531
3532
	/* misc functions */
3533
3534
	public function throwError($msg = "parse error", $count = null) {
3535
		$count = is_null($count) ? $this->count : $count;
3536
3537
		$line = $this->line +
3538
			substr_count(substr($this->buffer, 0, $count), "\n");
3539
3540 View Code Duplication
		if (!empty($this->sourceName)) {
3541
			$loc = "$this->sourceName on line $line";
3542
		} else {
3543
			$loc = "line: $line";
3544
		}
3545
3546
		// TODO this depends on $this->count
3547 View Code Duplication
		if ($this->peek("(.*?)(\n|$)", $m, $count)) {
3548
			throw new exception("$msg: failed at `$m[1]` $loc");
3549
		} else {
3550
			throw new exception("$msg: $loc");
3551
		}
3552
	}
3553
3554
	protected function pushBlock($selectors=null, $type=null) {
3555
		$b = new stdclass;
3556
		$b->parent = $this->env;
3557
3558
		$b->type = $type;
3559
		$b->id = self::$nextBlockId++;
3560
3561
		$b->isVararg = false; // TODO: kill me from here
3562
		$b->tags = $selectors;
3563
3564
		$b->props = array();
3565
		$b->children = array();
3566
3567
		$this->env = $b;
3568
		return $b;
3569
	}
3570
3571
	// push a block that doesn't multiply tags
3572
	protected function pushSpecialBlock($type) {
3573
		return $this->pushBlock(null, $type);
3574
	}
3575
3576
	// append a property to the current block
3577
	protected function append($prop, $pos = null) {
3578
		if ($pos !== null) $prop[-1] = $pos;
3579
		$this->env->props[] = $prop;
3580
	}
3581
3582
	// pop something off the stack
3583
	protected function pop() {
3584
		$old = $this->env;
3585
		$this->env = $this->env->parent;
3586
		return $old;
3587
	}
3588
3589
	// remove comments from $text
3590
	// todo: make it work for all functions, not just url
3591
	protected function removeComments($text) {
3592
		$look = array(
3593
			'url(', '//', '/*', '"', "'"
3594
		);
3595
3596
		$out = '';
3597
		$min = null;
3598
		while (true) {
3599
			// find the next item
3600
			foreach ($look as $token) {
3601
				$pos = strpos($text, $token);
3602
				if ($pos !== false) {
3603
					if (!isset($min) || $pos < $min[1]) $min = array($token, $pos);
3604
				}
3605
			}
3606
3607
			if (is_null($min)) break;
3608
3609
			$count = $min[1];
3610
			$skip = 0;
3611
			$newlines = 0;
3612
			switch ($min[0]) {
3613 View Code Duplication
			case 'url(':
3614
				if (preg_match('/url\(.*?\)/', $text, $m, 0, $count))
3615
					$count += strlen($m[0]) - strlen($min[0]);
3616
				break;
3617
			case '"':
3618
			case "'":
3619
				if (preg_match('/'.$min[0].'.*?(?<!\\\\)'.$min[0].'/', $text, $m, 0, $count))
3620
					$count += strlen($m[0]) - 1;
3621
				break;
3622
			case '//':
3623
				$skip = strpos($text, "\n", $count);
3624
				if ($skip === false) $skip = strlen($text) - $count;
3625
				else $skip -= $count;
3626
				break;
3627 View Code Duplication
			case '/*':
3628
				if (preg_match('/\/\*.*?\*\//s', $text, $m, 0, $count)) {
3629
					$skip = strlen($m[0]);
3630
					$newlines = substr_count($m[0], "\n");
3631
				}
3632
				break;
3633
			}
3634
3635
			if ($skip == 0) $count += strlen($min[0]);
3636
3637
			$out .= substr($text, 0, $count).str_repeat("\n", $newlines);
3638
			$text = substr($text, $count + $skip);
3639
3640
			$min = null;
3641
		}
3642
3643
		return $out.$text;
3644
	}
3645
3646
}
3647
3648
class lessc_formatter_classic {
3649
	public $indentChar = "  ";
3650
3651
	public $break = "\n";
3652
	public $open = " {";
3653
	public $close = "}";
3654
	public $selectorSeparator = ", ";
3655
	public $assignSeparator = ":";
3656
3657
	public $openSingle = " { ";
3658
	public $closeSingle = " }";
3659
3660
	public $disableSingle = false;
3661
	public $breakSelectors = false;
3662
3663
	public $compressColors = false;
3664
3665
	public function __construct() {
3666
		$this->indentLevel = 0;
0 ignored issues
show
Bug introduced by
The property indentLevel does not exist. Did you maybe forget to declare it?

In PHP it is possible to write to properties without declaring them. For example, the following is perfectly valid PHP code:

class MyClass { }

$x = new MyClass();
$x->foo = true;

Generally, it is a good practice to explictly declare properties to avoid accidental typos and provide IDE auto-completion:

class MyClass {
    public $foo;
}

$x = new MyClass();
$x->foo = true;
Loading history...
3667
	}
3668
3669
	public function indentStr($n = 0) {
3670
		return str_repeat($this->indentChar, max($this->indentLevel + $n, 0));
3671
	}
3672
3673
	public function property($name, $value) {
3674
		return $name . $this->assignSeparator . $value . ";";
3675
	}
3676
3677
	protected function isEmpty($block) {
3678
		if (empty($block->lines)) {
3679
			foreach ($block->children as $child) {
3680
				if (!$this->isEmpty($child)) return false;
3681
			}
3682
3683
			return true;
3684
		}
3685
		return false;
3686
	}
3687
3688
	public function block($block) {
3689
		if ($this->isEmpty($block)) return;
3690
3691
		$inner = $pre = $this->indentStr();
3692
3693
		$isSingle = !$this->disableSingle &&
3694
			is_null($block->type) && count($block->lines) == 1;
3695
3696
		if (!empty($block->selectors)) {
3697
			$this->indentLevel++;
3698
3699
			if ($this->breakSelectors) {
3700
				$selectorSeparator = $this->selectorSeparator . $this->break . $pre;
3701
			} else {
3702
				$selectorSeparator = $this->selectorSeparator;
3703
			}
3704
3705
			echo $pre .
3706
				implode($selectorSeparator, $block->selectors);
3707
			if ($isSingle) {
3708
				echo $this->openSingle;
3709
				$inner = "";
3710
			} else {
3711
				echo $this->open . $this->break;
3712
				$inner = $this->indentStr();
3713
			}
3714
3715
		}
3716
3717 View Code Duplication
		if (!empty($block->lines)) {
3718
			$glue = $this->break.$inner;
3719
			echo $inner . implode($glue, $block->lines);
3720
			if (!$isSingle && !empty($block->children)) {
3721
				echo $this->break;
3722
			}
3723
		}
3724
3725
		foreach ($block->children as $child) {
3726
			$this->block($child);
3727
		}
3728
3729
		if (!empty($block->selectors)) {
3730
			if (!$isSingle && empty($block->children)) echo $this->break;
3731
3732
			if ($isSingle) {
3733
				echo $this->closeSingle . $this->break;
3734
			} else {
3735
				echo $pre . $this->close . $this->break;
3736
			}
3737
3738
			$this->indentLevel--;
3739
		}
3740
	}
3741
}
3742
3743
class lessc_formatter_compressed extends lessc_formatter_classic {
3744
	public $disableSingle = true;
3745
	public $open = "{";
3746
	public $selectorSeparator = ",";
3747
	public $assignSeparator = ":";
3748
	public $break = "";
3749
	public $compressColors = true;
3750
3751
	public function indentStr($n = 0) {
3752
		return "";
3753
	}
3754
}
3755
3756
class lessc_formatter_lessjs extends lessc_formatter_classic {
3757
	public $disableSingle = true;
3758
	public $breakSelectors = true;
3759
	public $assignSeparator = ": ";
3760
	public $selectorSeparator = ",";
3761
}
3762