scssc::reduce()   F
last analyzed

Complexity

Conditions 64
Paths > 20000

Size

Total Lines 176

Duplication

Lines 9
Ratio 5.11 %

Importance

Changes 0
Metric Value
cc 64
nc 115374
nop 2
dl 9
loc 176
rs 0
c 0
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
/**
3
 * SCSS compiler written in PHP
4
 *
5
 * @copyright 2012-2013 Leaf Corcoran
6
 *
7
 * @license https://opensource.org/licenses/gpl-license GPL-3.0
8
 * @license https://opensource.org/licenses/MIT MIT
9
 *
10
 * @link https://leafo.net/scssphp
11
 */
12
13
/**
14
 * The scss compiler and parser.
15
 *
16
 * Converting SCSS to CSS is a three stage process. The incoming file is parsed
17
 * by `scssc_parser` into a syntax tree, then it is compiled into another tree
18
 * representing the CSS structure by `scssc`. The CSS tree is fed into a
19
 * formatter, like `scssc_formatter` which then outputs CSS as a string.
20
 *
21
 * During the first compile, all values are *reduced*, which means that their
22
 * types are brought to the lowest form before being dump as strings. This
23
 * handles math equations, variable dereferences, and the like.
24
 *
25
 * The `parse` function of `scssc` is the entry point.
26
 *
27
 * In summary:
28
 *
29
 * The `scssc` class creates an instance of the parser, feeds it SCSS code,
30
 * then transforms the resulting tree to a CSS tree. This class also holds the
31
 * evaluation context, such as all available mixins and variables at any given
32
 * time.
33
 *
34
 * The `scssc_parser` class is only concerned with parsing its input.
35
 *
36
 * The `scssc_formatter` takes a CSS tree, and dumps it to a formatted string,
37
 * handling things like indentation.
38
 */
39
40
/**
41
 * SCSS compiler
42
 *
43
 * @author Leaf Corcoran <[email protected]>
44
 */
45
class scssc {
46
	static public $VERSION = "v0.0.9";
47
48
	static protected $operatorNames = array(
49
		'+' => "add",
50
		'-' => "sub",
51
		'*' => "mul",
52
		'/' => "div",
53
		'%' => "mod",
54
55
		'==' => "eq",
56
		'!=' => "neq",
57
		'<' => "lt",
58
		'>' => "gt",
59
60
		'<=' => "lte",
61
		'>=' => "gte",
62
	);
63
64
	static protected $namespaces = array(
65
		"special" => "%",
66
		"mixin" => "@",
67
		"function" => "^",
68
	);
69
70
	static protected $unitTable = array(
71
		"in" => array(
72
			"in" => 1,
73
			"pt" => 72,
74
			"pc" => 6,
75
			"cm" => 2.54,
76
			"mm" => 25.4,
77
			"px" => 96,
78
		)
79
	);
80
81
	static public $true = array("keyword", "true");
82
	static public $false = array("keyword", "false");
83
	static public $null = array("null");
84
85
	static public $defaultValue = array("keyword", "");
86
	static public $selfSelector = array("self");
87
88
	protected $importPaths = array("");
89
	protected $importCache = array();
90
91
	protected $userFunctions = array();
92
93
	protected $numberPrecision = 5;
94
95
	protected $formatter = "scss_formatter_nested";
96
97
	public function compile($code, $name=null) {
98
		$this->indentLevel = -1;
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...
99
		$this->commentsSeen = array();
0 ignored issues
show
Bug introduced by
The property commentsSeen 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...
100
		$this->extends = array();
0 ignored issues
show
Bug introduced by
The property extends 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...
101
		$this->extendsMap = array();
0 ignored issues
show
Bug introduced by
The property extendsMap does not seem to exist. Did you mean extends?

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...
102
103
		$locale = setlocale(LC_NUMERIC, 0);
104
		setlocale(LC_NUMERIC, "C");
105
106
		$this->parsedFiles = array();
0 ignored issues
show
Bug introduced by
The property parsedFiles 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...
107
		$this->parser = new scss_parser($name);
0 ignored issues
show
Bug introduced by
The property parser 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...
108
		$tree = $this->parser->parse($code);
109
110
		$this->formatter = new $this->formatter();
0 ignored issues
show
Documentation Bug introduced by
It seems like new $this->formatter() of type object is incompatible with the declared type string of property $formatter.

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...
111
112
		$this->env = null;
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...
113
		$this->scope = null;
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...
114
115
		$this->compileRoot($tree);
116
117
		$out = $this->formatter->format($this->scope);
118
119
		setlocale(LC_NUMERIC, $locale);
120
		return $out;
121
	}
122
123
	protected function isSelfExtend($target, $origin) {
124
		foreach ($origin as $sel) {
125
			if (in_array($target, $sel)) {
126
				return true;
127
			}
128
		}
129
130
		return false;
131
	}
132
133
	protected function pushExtends($target, $origin) {
134
		if ($this->isSelfExtend($target, $origin)) {
135
			return;
136
		}
137
138
		$i = count($this->extends);
139
		$this->extends[] = array($target, $origin);
140
141
		foreach ($target as $part) {
142
			if (isset($this->extendsMap[$part])) {
0 ignored issues
show
Bug introduced by
The property extendsMap does not seem to exist. Did you mean extends?

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...
143
				$this->extendsMap[$part][] = $i;
0 ignored issues
show
Bug introduced by
The property extendsMap does not seem to exist. Did you mean extends?

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...
144
			} else {
145
				$this->extendsMap[$part] = array($i);
0 ignored issues
show
Bug introduced by
The property extendsMap does not seem to exist. Did you mean extends?

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...
146
			}
147
		}
148
	}
149
150
	protected function makeOutputBlock($type, $selectors = null) {
151
		$out = new stdClass;
152
		$out->type = $type;
153
		$out->lines = array();
154
		$out->children = array();
155
		$out->parent = $this->scope;
156
		$out->selectors = $selectors;
157
		$out->depth = $this->env->depth;
158
159
		return $out;
160
	}
161
162
	protected function matchExtendsSingle($single, &$outOrigin) {
163
		$counts = array();
164
		foreach ($single as $part) {
165
			if (!is_string($part)) return false; // hmm
166
167
			if (isset($this->extendsMap[$part])) {
0 ignored issues
show
Bug introduced by
The property extendsMap does not seem to exist. Did you mean extends?

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...
168
				foreach ($this->extendsMap[$part] as $idx) {
0 ignored issues
show
Bug introduced by
The property extendsMap does not seem to exist. Did you mean extends?

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...
169
					$counts[$idx] =
170
						isset($counts[$idx]) ? $counts[$idx] + 1 : 1;
171
				}
172
			}
173
		}
174
175
		$outOrigin = array();
176
		$found = false;
177
178
		foreach ($counts as $idx => $count) {
179
			list($target, $origin) = $this->extends[$idx];
180
181
			// check count
182
			if ($count != count($target)) continue;
183
184
			// check if target is subset of single
185
			if (array_diff(array_intersect($single, $target), $target)) continue;
186
187
			$rem = array_diff($single, $target);
188
189
			foreach ($origin as $j => $new) {
190
				// prevent infinite loop when target extends itself
191
				foreach ($new as $new_selector) {
192
					if (!array_diff($single, $new_selector)) {
193
						continue 2;
194
					}
195
				}
196
197
				$origin[$j][count($origin[$j]) - 1] = $this->combineSelectorSingle(end($new), $rem);
198
			}
199
200
			$outOrigin = array_merge($outOrigin, $origin);
201
202
			$found = true;
203
		}
204
205
		return $found;
206
	}
207
208
	protected function combineSelectorSingle($base, $other) {
209
		$tag = null;
210
		$out = array();
211
212
		foreach (array($base, $other) as $single) {
213
			foreach ($single as $part) {
214
				if (preg_match('/^[^\[.#:]/', $part)) {
215
					$tag = $part;
216
				} else {
217
					$out[] = $part;
218
				}
219
			}
220
		}
221
222
		if ($tag) {
223
			array_unshift($out, $tag);
224
		}
225
226
		return $out;
227
	}
228
229
	protected function matchExtends($selector, &$out, $from = 0, $initial=true) {
230
		foreach ($selector as $i => $part) {
231
			if ($i < $from) continue;
232
233
			if ($this->matchExtendsSingle($part, $origin)) {
234
				$before = array_slice($selector, 0, $i);
235
				$after = array_slice($selector, $i + 1);
236
237
				foreach ($origin as $new) {
238
					$k = 0;
239
240
					// remove shared parts
241
					if ($initial) {
242
						foreach ($before as $k => $val) {
243
							if (!isset($new[$k]) || $val != $new[$k]) {
244
								break;
245
							}
246
						}
247
					}
248
249
					$result = array_merge(
250
						$before,
251
						$k > 0 ? array_slice($new, $k) : $new,
252
						$after);
253
254
255
					if ($result == $selector) continue;
256
					$out[] = $result;
257
258
					// recursively check for more matches
259
					$this->matchExtends($result, $out, $i, false);
260
261
					// selector sequence merging
262
					if (!empty($before) && count($new) > 1) {
263
						$result2 = array_merge(
264
							array_slice($new, 0, -1),
265
							$k > 0 ? array_slice($before, $k) : $before,
266
							array_slice($new, -1),
267
							$after);
268
269
						$out[] = $result2;
270
					}
271
				}
272
			}
273
		}
274
	}
275
276
	protected function flattenSelectors($block, $parentKey = null) {
277
		if ($block->selectors) {
278
			$selectors = array();
279
			foreach ($block->selectors as $s) {
280
				$selectors[] = $s;
281
				if (!is_array($s)) continue;
282
				// check extends
283
				if (!empty($this->extendsMap)) {
0 ignored issues
show
Bug introduced by
The property extendsMap does not seem to exist. Did you mean extends?

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...
284
					$this->matchExtends($s, $selectors);
285
				}
286
			}
287
288
			$block->selectors = array();
289
			$placeholderSelector = false;
290
			foreach ($selectors as $selector) {
291
				if ($this->hasSelectorPlaceholder($selector)) {
292
					$placeholderSelector = true;
293
					continue;
294
				}
295
				$block->selectors[] = $this->compileSelector($selector);
296
			}
297
298
			if ($placeholderSelector && 0 == count($block->selectors) && null !== $parentKey) {
299
				unset($block->parent->children[$parentKey]);
300
				return;
301
			}
302
		}
303
304
		foreach ($block->children as $key => $child) {
305
			$this->flattenSelectors($child, $key);
306
		}
307
	}
308
309
	protected function compileRoot($rootBlock) {
310
		$this->pushEnv($rootBlock);
311
		$this->scope = $this->makeOutputBlock("root");
312
313
		$this->compileChildren($rootBlock->children, $this->scope);
314
		$this->flattenSelectors($this->scope);
315
316
		$this->popEnv();
317
	}
318
319
	protected function compileMedia($media) {
320
		$this->pushEnv($media);
321
		$parentScope = $this->mediaParent($this->scope);
322
323
		$this->scope = $this->makeOutputBlock("media", array(
324
			$this->compileMediaQuery($this->multiplyMedia($this->env)))
325
		);
326
327
		$parentScope->children[] = $this->scope;
328
329
		// top level properties in a media cause it to be wrapped
330
		$needsWrap = false;
331
		foreach ($media->children as $child) {
332
			$type = $child[0];
333
			if ($type !== 'block' && $type !== 'media' && $type !== 'directive') {
334
				$needsWrap = true;
335
				break;
336
			}
337
		}
338
339
		if ($needsWrap) {
340
			$wrapped = (object)array(
341
				"selectors" => array(),
342
				"children" => $media->children
343
			);
344
			$media->children = array(array("block", $wrapped));
345
		}
346
347
		$this->compileChildren($media->children, $this->scope);
348
349
		$this->scope = $this->scope->parent;
350
		$this->popEnv();
351
	}
352
353 View Code Duplication
	protected function mediaParent($scope) {
354
		while (!empty($scope->parent)) {
355
			if (!empty($scope->type) && $scope->type != "media") {
356
				break;
357
			}
358
			$scope = $scope->parent;
359
		}
360
361
		return $scope;
362
	}
363
364
	// TODO refactor compileNestedBlock and compileMedia into same thing
365 View Code Duplication
	protected function compileNestedBlock($block, $selectors) {
366
		$this->pushEnv($block);
367
368
		$this->scope = $this->makeOutputBlock($block->type, $selectors);
369
		$this->scope->parent->children[] = $this->scope;
370
		$this->compileChildren($block->children, $this->scope);
371
372
		$this->scope = $this->scope->parent;
373
		$this->popEnv();
374
	}
375
376
	/**
377
	 * Recursively compiles a block.
378
	 *
379
	 * A block is analogous to a CSS block in most cases. A single SCSS document
380
	 * is encapsulated in a block when parsed, but it does not have parent tags
381
	 * so all of its children appear on the root level when compiled.
382
	 *
383
	 * Blocks are made up of selectors and children.
384
	 *
385
	 * The children of a block are just all the blocks that are defined within.
386
	 *
387
	 * Compiling the block involves pushing a fresh environment on the stack,
388
	 * and iterating through the props, compiling each one.
389
	 *
390
	 * @see scss::compileChild()
391
	 *
392
	 * @param \StdClass $block
393
	 */
394
	protected function compileBlock($block) {
395
		$env = $this->pushEnv($block);
396
397
		$env->selectors =
398
			array_map(array($this, "evalSelector"), $block->selectors);
399
400
		$out = $this->makeOutputBlock(null, $this->multiplySelectors($env));
401
		$this->scope->children[] = $out;
402
		$this->compileChildren($block->children, $out);
403
404
		$this->popEnv();
405
	}
406
407
	// joins together .classes and #ids
408
	protected function flattenSelectorSingle($single) {
409
		$joined = array();
410
		foreach ($single as $part) {
411
			if (empty($joined) ||
412
				!is_string($part) ||
413
				preg_match('/[\[.:#%]/', $part))
414
			{
415
				$joined[] = $part;
416
				continue;
417
			}
418
419
			if (is_array(end($joined))) {
420
				$joined[] = $part;
421
			} else {
422
				$joined[count($joined) - 1] .= $part;
423
			}
424
		}
425
426
		return $joined;
427
	}
428
429
	// replaces all the interpolates
430
	protected function evalSelector($selector) {
431
		return array_map(array($this, "evalSelectorPart"), $selector);
432
	}
433
434
	protected function evalSelectorPart($piece) {
435 View Code Duplication
		foreach ($piece as &$p) {
436
			if (!is_array($p)) continue;
437
438
			switch ($p[0]) {
439
			case "interpolate":
440
				$p = $this->compileValue($p);
441
				break;
442
			case "string":
443
				$p = $this->compileValue($p);
444
				break;
445
			}
446
		}
447
448
		return $this->flattenSelectorSingle($piece);
449
	}
450
451
	// compiles to string
452
	// self(&) should have been replaced by now
453
	protected function compileSelector($selector) {
454
		if (!is_array($selector)) return $selector; // media and the like
455
456
		return implode(" ", array_map(
457
			array($this, "compileSelectorPart"), $selector));
458
	}
459
460
	protected function compileSelectorPart($piece) {
461 View Code Duplication
		foreach ($piece as &$p) {
462
			if (!is_array($p)) continue;
463
464
			switch ($p[0]) {
465
			case "self":
466
				$p = "&";
467
				break;
468
			default:
469
				$p = $this->compileValue($p);
470
				break;
471
			}
472
		}
473
474
		return implode($piece);
475
	}
476
477
	protected function hasSelectorPlaceholder($selector)
478
	{
479
		if (!is_array($selector)) return false;
480
481
		foreach ($selector as $parts) {
482
			foreach ($parts as $part) {
483
				if ('%' == $part[0]) {
484
					return true;
485
				}
486
			}
487
		}
488
489
		return false;
490
	}
491
492
	protected function compileChildren($stms, $out) {
493
		foreach ($stms as $stm) {
494
			$ret = $this->compileChild($stm, $out);
495
			if (!is_null($ret)) return $ret;
496
		}
497
	}
498
499
	protected function compileMediaQuery($queryList) {
500
		$out = "@media";
501
		$first = true;
502
		foreach ($queryList as $query){
503
			$parts = array();
504
			foreach ($query as $q) {
505
				switch ($q[0]) {
506
					case "mediaType":
507
						$parts[] = implode(" ", array_map(array($this, "compileValue"), array_slice($q, 1)));
508
						break;
509
					case "mediaExp":
510
						if (isset($q[2])) {
511
							$parts[] = "(". $this->compileValue($q[1]) . $this->formatter->assignSeparator . $this->compileValue($q[2]) . ")";
512
						} else {
513
							$parts[] = "(" . $this->compileValue($q[1]) . ")";
514
						}
515
						break;
516
				}
517
			}
518
			if (!empty($parts)) {
519
				if ($first) {
520
					$first = false;
521
					$out .= " ";
522
				} else {
523
					$out .= $this->formatter->tagSeparator;
524
				}
525
				$out .= implode(" and ", $parts);
526
			}
527
		}
528
		return $out;
529
	}
530
531
	// returns true if the value was something that could be imported
532
	protected function compileImport($rawPath, $out) {
533
		if ($rawPath[0] == "string") {
534
			$path = $this->compileStringContent($rawPath);
535
			if ($path = $this->findImport($path)) {
536
				$this->importFile($path, $out);
537
				return true;
538
			}
539
			return false;
540
		}
541
		if ($rawPath[0] == "list") {
542
			// handle a list of strings
543
			if (count($rawPath[2]) == 0) return false;
544
			foreach ($rawPath[2] as $path) {
545
				if ($path[0] != "string") return false;
546
			}
547
548
			foreach ($rawPath[2] as $path) {
549
				$this->compileImport($path, $out);
550
			}
551
552
			return true;
553
		}
554
555
		return false;
556
	}
557
558
	// return a value to halt execution
559
	protected function compileChild($child, $out) {
560
		$this->sourcePos = isset($child[-1]) ? $child[-1] : -1;
0 ignored issues
show
Bug introduced by
The property sourcePos 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...
561
		$this->sourceParser = isset($child[-2]) ? $child[-2] : $this->parser;
0 ignored issues
show
Bug introduced by
The property sourceParser does not seem to exist. Did you mean 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...
562
563
		switch ($child[0]) {
564 View Code Duplication
		case "import":
565
			list(,$rawPath) = $child;
566
			$rawPath = $this->reduce($rawPath);
567
			if (!$this->compileImport($rawPath, $out)) {
568
				$out->lines[] = "@import " . $this->compileValue($rawPath) . ";";
569
			}
570
			break;
571 View Code Duplication
		case "directive":
572
			list(, $directive) = $child;
573
			$s = "@" . $directive->name;
574
			if (!empty($directive->value)) {
575
				$s .= " " . $this->compileValue($directive->value);
576
			}
577
			$this->compileNestedBlock($directive, array($s));
578
			break;
579
		case "media":
580
			$this->compileMedia($child[1]);
581
			break;
582
		case "block":
583
			$this->compileBlock($child[1]);
584
			break;
585
		case "charset":
586
			$out->lines[] = "@charset ".$this->compileValue($child[1]).";";
587
			break;
588
		case "assign":
589
			list(,$name, $value) = $child;
590
			if ($name[0] == "var") {
591
				$isDefault = !empty($child[3]);
592
593
				if ($isDefault) {
594
					$existingValue = $this->get($name[1], true);
595
					$shouldSet = $existingValue === true || $existingValue == self::$null;
596
				}
597
598
				if (!$isDefault || $shouldSet) {
0 ignored issues
show
Bug introduced by
The variable $shouldSet 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...
599
					$this->set($name[1], $this->reduce($value));
600
				}
601
				break;
602
			}
603
604
			// if the value reduces to null from something else then
605
			// the property should be discarded
606
			if ($value[0] != "null") {
607
				$value = $this->reduce($value);
608
				if ($value[0] == "null") {
609
					break;
610
				}
611
			}
612
613
			$compiledValue = $this->compileValue($value);
614
			$out->lines[] = $this->formatter->property(
0 ignored issues
show
Bug introduced by
The method property cannot be called on $this->formatter (of type string).

Methods can only be called on objects. This check looks for methods being called on variables that have been inferred to never be objects.

Loading history...
615
				$this->compileValue($name),
616
				$compiledValue);
617
			break;
618
		case "comment":
619
			$out->lines[] = $child[1];
620
			break;
621
		case "mixin":
622
		case "function":
623
			list(,$block) = $child;
624
			$this->set(self::$namespaces[$block->type] . $block->name, $block);
625
			break;
626
		case "extend":
627
			list(, $selectors) = $child;
628
			foreach ($selectors as $sel) {
629
				// only use the first one
630
				$sel = current($this->evalSelector($sel));
631
				$this->pushExtends($sel, $out->selectors);
632
			}
633
			break;
634
		case "if":
635
			list(, $if) = $child;
636
			if ($this->isTruthy($this->reduce($if->cond, true))) {
637
				return $this->compileChildren($if->children, $out);
638
			} else {
639
				foreach ($if->cases as $case) {
640
					if ($case->type == "else" ||
641
						$case->type == "elseif" && $this->isTruthy($this->reduce($case->cond)))
642
					{
643
						return $this->compileChildren($case->children, $out);
644
					}
645
				}
646
			}
647
			break;
648
		case "return":
649
			return $this->reduce($child[1], true);
650
		case "each":
651
			list(,$each) = $child;
652
			$list = $this->coerceList($this->reduce($each->list));
653
			foreach ($list[2] as $item) {
654
				$this->pushEnv();
655
				$this->set($each->var, $item);
656
				// TODO: allow return from here
657
				$this->compileChildren($each->children, $out);
658
				$this->popEnv();
659
			}
660
			break;
661
		case "while":
662
			list(,$while) = $child;
663
			while ($this->isTruthy($this->reduce($while->cond, true))) {
664
				$ret = $this->compileChildren($while->children, $out);
665
				if ($ret) return $ret;
666
			}
667
			break;
668
		case "for":
669
			list(,$for) = $child;
670
			$start = $this->reduce($for->start, true);
671
			$start = $start[1];
672
			$end = $this->reduce($for->end, true);
673
			$end = $end[1];
674
			$d = $start < $end ? 1 : -1;
675
676
			while (true) {
677
				if ((!$for->until && $start - $d == $end) ||
678
					($for->until && $start == $end))
679
				{
680
					break;
681
				}
682
683
				$this->set($for->var, array("number", $start, ""));
684
				$start += $d;
685
686
				$ret = $this->compileChildren($for->children, $out);
687
				if ($ret) return $ret;
688
			}
689
690
			break;
691
		case "nestedprop":
692
			list(,$prop) = $child;
693
			$prefixed = array();
694
			$prefix = $this->compileValue($prop->prefix) . "-";
695
			foreach ($prop->children as $child) {
696
				if ($child[0] == "assign") {
697
					array_unshift($child[1][2], $prefix);
698
				}
699
				if ($child[0] == "nestedprop") {
700
					array_unshift($child[1]->prefix[2], $prefix);
701
				}
702
				$prefixed[] = $child;
703
			}
704
			$this->compileChildren($prefixed, $out);
705
			break;
706
		case "include": // including a mixin
707
			list(,$name, $argValues, $content) = $child;
708
			$mixin = $this->get(self::$namespaces["mixin"] . $name, false);
709
			if (!$mixin) {
710
				$this->throwError("Undefined mixin $name");
711
			}
712
713
			$callingScope = $this->env;
714
715
			// push scope, apply args
716
			$this->pushEnv();
717
			if ($this->env->depth > 0) {
718
				$this->env->depth--;
719
			}
720
721
			if (!is_null($content)) {
722
				$content->scope = $callingScope;
723
				$this->setRaw(self::$namespaces["special"] . "content", $content);
724
			}
725
726
			if (!is_null($mixin->args)) {
727
				$this->applyArguments($mixin->args, $argValues);
728
			}
729
730
			foreach ($mixin->children as $child) {
731
				$this->compileChild($child, $out);
732
			}
733
734
			$this->popEnv();
735
736
			break;
737
		case "mixin_content":
738
			$content = $this->get(self::$namespaces["special"] . "content");
739
			if (is_null($content)) {
740
				$this->throwError("Expected @content inside of mixin");
741
			}
742
743
			$strongTypes = array('include', 'block', 'for', 'while');
744
			foreach ($content->children as $child) {
745
				$this->storeEnv = (in_array($child[0], $strongTypes))
0 ignored issues
show
Bug introduced by
The property storeEnv 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...
746
					? null
747
					: $content->scope;
748
749
				$this->compileChild($child, $out);
750
			}
751
752
			unset($this->storeEnv);
753
			break;
754
		case "debug":
755
			list(,$value, $pos) = $child;
756
			$line = $this->parser->getLineNo($pos);
757
			$value = $this->compileValue($this->reduce($value, true));
758
			fwrite(STDERR, "Line $line DEBUG: $value\n");
759
			break;
760
		default:
761
			$this->throwError("unknown child type: $child[0]");
762
		}
763
	}
764
765
	protected function expToString($exp) {
766
		list(, $op, $left, $right, $inParens, $whiteLeft, $whiteRight) = $exp;
0 ignored issues
show
Unused Code introduced by
The assignment to $inParens is unused. Consider omitting it like so list($first,,$third).

This checks looks for assignemnts to variables using the list(...) function, where not all assigned variables are subsequently used.

Consider the following code example.

<?php

function returnThreeValues() {
    return array('a', 'b', 'c');
}

list($a, $b, $c) = returnThreeValues();

print $a . " - " . $c;

Only the variables $a and $c are used. There was no need to assign $b.

Instead, the list call could have been.

list($a,, $c) = returnThreeValues();
Loading history...
767
		$content = array($this->reduce($left));
768
		if ($whiteLeft) $content[] = " ";
769
		$content[] = $op;
770
		if ($whiteRight) $content[] = " ";
771
		$content[] = $this->reduce($right);
772
		return array("string", "", $content);
773
	}
774
775
	protected function isTruthy($value) {
776
		return $value != self::$false && $value != self::$null;
777
	}
778
779
	// should $value cause its operand to eval
780
	protected function shouldEval($value) {
781
		switch ($value[0]) {
782
		case "exp":
783
			if ($value[1] == "/") {
784
				return $this->shouldEval($value[2], $value[3]);
0 ignored issues
show
Unused Code introduced by
The call to scssc::shouldEval() has too many arguments starting with $value[3].

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...
785
			}
786
		case "var":
787
		case "fncall":
788
			return true;
789
		}
790
		return false;
791
	}
792
793
	protected function reduce($value, $inExp = false) {
794
		list($type) = $value;
795
		switch ($type) {
796
			case "exp":
797
				list(, $op, $left, $right, $inParens) = $value;
798
				$opName = isset(self::$operatorNames[$op]) ? self::$operatorNames[$op] : $op;
799
800
				$inExp = $inExp || $this->shouldEval($left) || $this->shouldEval($right);
801
802
				$left = $this->reduce($left, true);
803
				$right = $this->reduce($right, true);
804
805
				// only do division in special cases
806
				if ($opName == "div" && !$inParens && !$inExp) {
807
					if ($left[0] != "color" && $right[0] != "color") {
808
						return $this->expToString($value);
809
					}
810
				}
811
812
				$left = $this->coerceForExpression($left);
813
				$right = $this->coerceForExpression($right);
814
815
				$ltype = $left[0];
816
				$rtype = $right[0];
817
818
				// this tries:
819
				// 1. op_[op name]_[left type]_[right type]
820
				// 2. op_[left type]_[right type] (passing the op as first arg
821
				// 3. op_[op name]
822
				$fn = "op_${opName}_${ltype}_${rtype}";
823
				if (is_callable(array($this, $fn)) ||
824
					(($fn = "op_${ltype}_${rtype}") &&
825
						is_callable(array($this, $fn)) &&
826
						$passOp = true) ||
827
					(($fn = "op_${opName}") &&
828
						is_callable(array($this, $fn)) &&
829
						$genOp = true))
830
				{
831
					$unitChange = false;
832
					if (!isset($genOp) &&
833
						$left[0] == "number" && $right[0] == "number")
834
					{
835
						if ($opName == "mod" && $right[2] != "") {
836
							$this->throwError("Cannot modulo by a number with units: $right[1]$right[2].");
837
						}
838
839
						$unitChange = true;
840
						$emptyUnit = $left[2] == "" || $right[2] == "";
841
						$targetUnit = "" != $left[2] ? $left[2] : $right[2];
842
843
						if ($opName != "mul") {
844
							$left[2] = "" != $left[2] ? $left[2] : $targetUnit;
845
							$right[2] = "" != $right[2] ? $right[2] : $targetUnit;
846
						}
847
848
						if ($opName != "mod") {
849
							$left = $this->normalizeNumber($left);
850
							$right = $this->normalizeNumber($right);
851
						}
852
853
						if ($opName == "div" && !$emptyUnit && $left[2] == $right[2]) {
854
							$targetUnit = "";
855
						}
856
857
						if ($opName == "mul") {
858
							$left[2] = "" != $left[2] ? $left[2] : $right[2];
859
							$right[2] = "" != $right[2] ? $right[2] : $left[2];
860
						} elseif ($opName == "div" && $left[2] == $right[2]) {
861
							$left[2] = "";
862
							$right[2] = "";
863
						}
864
					}
865
866
					$shouldEval = $inParens || $inExp;
867
					if (isset($passOp)) {
868
						$out = $this->$fn($op, $left, $right, $shouldEval);
869
					} else {
870
						$out = $this->$fn($left, $right, $shouldEval);
871
					}
872
873
					if (!is_null($out)) {
874
						if ($unitChange && $out[0] == "number") {
875
							$out = $this->coerceUnit($out, $targetUnit);
0 ignored issues
show
Bug introduced by
The variable $targetUnit 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...
876
						}
877
						return $out;
878
					}
879
				}
880
881
				return $this->expToString($value);
882
			case "unary":
883
				list(, $op, $exp, $inParens) = $value;
884
				$inExp = $inExp || $this->shouldEval($exp);
885
886
				$exp = $this->reduce($exp);
887 View Code Duplication
				if ($exp[0] == "number") {
888
					switch ($op) {
889
					case "+":
890
						return $exp;
891
					case "-":
892
						$exp[1] *= -1;
893
						return $exp;
894
					}
895
				}
896
897
				if ($op == "not") {
898
					if ($inExp || $inParens) {
899
						if ($exp == self::$false) {
900
							return self::$true;
901
						} else {
902
							return self::$false;
903
						}
904
					} else {
905
						$op = $op . " ";
906
					}
907
				}
908
909
				return array("string", "", array($op, $exp));
910
			case "var":
911
				list(, $name) = $value;
912
				return $this->reduce($this->get($name));
913
			case "list":
914
				foreach ($value[2] as &$item) {
915
					$item = $this->reduce($item);
916
				}
917
				return $value;
918
			case "string":
919
				foreach ($value[2] as &$item) {
920
					if (is_array($item)) {
921
						$item = $this->reduce($item);
922
					}
923
				}
924
				return $value;
925
			case "interpolate":
926
				$value[1] = $this->reduce($value[1]);
927
				return $value;
928
			case "fncall":
929
				list(,$name, $argValues) = $value;
930
931
				// user defined function?
932
				$func = $this->get(self::$namespaces["function"] . $name, false);
933
				if ($func) {
934
					$this->pushEnv();
935
936
					// set the args
937
					if (isset($func->args)) {
938
						$this->applyArguments($func->args, $argValues);
939
					}
940
941
					// throw away lines and children
942
					$tmp = (object)array(
943
						"lines" => array(),
944
						"children" => array()
945
					);
946
					$ret = $this->compileChildren($func->children, $tmp);
947
					$this->popEnv();
948
949
					return is_null($ret) ? self::$defaultValue : $ret;
950
				}
951
952
				// built in function
953
				if ($this->callBuiltin($name, $argValues, $returnValue)) {
954
					return $returnValue;
955
				}
956
957
				// need to flatten the arguments into a list
958
				$listArgs = array();
959
				foreach ((array)$argValues as $arg) {
960
					if (empty($arg[0])) {
961
						$listArgs[] = $this->reduce($arg[1]);
962
					}
963
				}
964
				return array("function", $name, array("list", ",", $listArgs));
965
			default:
966
				return $value;
967
		}
968
	}
969
970
	public function normalizeValue($value) {
971
		$value = $this->coerceForExpression($this->reduce($value));
972
		list($type) = $value;
973
974
		switch ($type) {
975
		case "list":
976
			$value = $this->extractInterpolation($value);
977
			if ($value[0] != "list") {
978
				return array("keyword", $this->compileValue($value));
979
			}
980
			foreach ($value[2] as $key => $item) {
981
				$value[2][$key] = $this->normalizeValue($item);
982
			}
983
			return $value;
984
		case "number":
985
			return $this->normalizeNumber($value);
986
		default:
987
			return $value;
988
		}
989
	}
990
991
	// just does physical lengths for now
992
	protected function normalizeNumber($number) {
993
		list(, $value, $unit) = $number;
994
		if (isset(self::$unitTable["in"][$unit])) {
995
			$conv = self::$unitTable["in"][$unit];
996
			return array("number", $value / $conv, "in");
997
		}
998
		return $number;
999
	}
1000
1001
	// $number should be normalized
1002
	protected function coerceUnit($number, $unit) {
1003
		list(, $value, $baseUnit) = $number;
1004
		if (isset(self::$unitTable[$baseUnit][$unit])) {
1005
			$value = $value * self::$unitTable[$baseUnit][$unit];
1006
		}
1007
1008
		return array("number", $value, $unit);
1009
	}
1010
1011
	protected function op_add_number_number($left, $right) {
1012
		return array("number", $left[1] + $right[1], $left[2]);
1013
	}
1014
1015
	protected function op_mul_number_number($left, $right) {
1016
		return array("number", $left[1] * $right[1], $left[2]);
1017
	}
1018
1019
	protected function op_sub_number_number($left, $right) {
1020
		return array("number", $left[1] - $right[1], $left[2]);
1021
	}
1022
1023
	protected function op_div_number_number($left, $right) {
1024
		return array("number", $left[1] / $right[1], $left[2]);
1025
	}
1026
1027
	protected function op_mod_number_number($left, $right) {
1028
		return array("number", $left[1] % $right[1], $left[2]);
1029
	}
1030
1031
	// adding strings
1032
	protected function op_add($left, $right) {
1033 View Code Duplication
		if ($strLeft = $this->coerceString($left)) {
1034
			if ($right[0] == "string") {
1035
				$right[1] = "";
1036
			}
1037
			$strLeft[2][] = $right;
1038
			return $strLeft;
1039
		}
1040
1041 View Code Duplication
		if ($strRight = $this->coerceString($right)) {
1042
			if ($left[0] == "string") {
1043
				$left[1] = "";
1044
			}
1045
			array_unshift($strRight[2], $left);
1046
			return $strRight;
1047
		}
1048
	}
1049
1050
	protected function op_and($left, $right, $shouldEval) {
1051
		if (!$shouldEval) return;
1052
		if ($left != self::$false) return $right;
1053
		return $left;
1054
	}
1055
1056
	protected function op_or($left, $right, $shouldEval) {
1057
		if (!$shouldEval) return;
1058
		if ($left != self::$false) return $left;
1059
		return $right;
1060
	}
1061
1062
	protected function op_color_color($op, $left, $right) {
1063
		$out = array('color');
1064
		foreach (range(1, 3) as $i) {
1065
			$lval = isset($left[$i]) ? $left[$i] : 0;
1066
			$rval = isset($right[$i]) ? $right[$i] : 0;
1067
			switch ($op) {
1068
			case '+':
1069
				$out[] = $lval + $rval;
1070
				break;
1071
			case '-':
1072
				$out[] = $lval - $rval;
1073
				break;
1074
			case '*':
1075
				$out[] = $lval * $rval;
1076
				break;
1077
			case '%':
1078
				$out[] = $lval % $rval;
1079
				break;
1080 View Code Duplication
			case '/':
1081
				if ($rval == 0) {
1082
					$this->throwError("color: Can't divide by zero");
1083
				}
1084
				$out[] = $lval / $rval;
1085
				break;
1086
			case "==":
1087
				return $this->op_eq($left, $right);
1088
			case "!=":
1089
				return $this->op_neq($left, $right);
1090
			default:
1091
				$this->throwError("color: unknown op $op");
1092
			}
1093
		}
1094
1095
		if (isset($left[4])) $out[4] = $left[4];
1096
		elseif (isset($right[4])) $out[4] = $right[4];
1097
1098
		return $this->fixColor($out);
1099
	}
1100
1101
	protected function op_color_number($op, $left, $right) {
1102
		$value = $right[1];
1103
		return $this->op_color_color($op, $left,
1104
			array("color", $value, $value, $value));
1105
	}
1106
1107
	protected function op_number_color($op, $left, $right) {
1108
		$value = $left[1];
1109
		return $this->op_color_color($op,
1110
			array("color", $value, $value, $value), $right);
1111
	}
1112
1113
	protected function op_eq($left, $right) {
1114
		if (($lStr = $this->coerceString($left)) && ($rStr = $this->coerceString($right))) {
1115
			$lStr[1] = "";
1116
			$rStr[1] = "";
1117
			return $this->toBool($this->compileValue($lStr) == $this->compileValue($rStr));
1118
		}
1119
1120
		return $this->toBool($left == $right);
1121
	}
1122
1123
	protected function op_neq($left, $right) {
1124
		return $this->toBool($left != $right);
1125
	}
1126
1127
	protected function op_gte_number_number($left, $right) {
1128
		return $this->toBool($left[1] >= $right[1]);
1129
	}
1130
1131
	protected function op_gt_number_number($left, $right) {
1132
		return $this->toBool($left[1] > $right[1]);
1133
	}
1134
1135
	protected function op_lte_number_number($left, $right) {
1136
		return $this->toBool($left[1] <= $right[1]);
1137
	}
1138
1139
	protected function op_lt_number_number($left, $right) {
1140
		return $this->toBool($left[1] < $right[1]);
1141
	}
1142
1143
	public function toBool($thing) {
1144
		return $thing ? self::$true : self::$false;
1145
	}
1146
1147
	/**
1148
	 * Compiles a primitive value into a CSS property value.
1149
	 *
1150
	 * Values in scssphp are typed by being wrapped in arrays, their format is
1151
	 * typically:
1152
	 *
1153
	 *     array(type, contents [, additional_contents]*)
1154
	 *
1155
	 * The input is expected to be reduced. This function will not work on
1156
	 * things like expressions and variables.
1157
	 *
1158
	 * @param array $value
1159
	 */
1160
	protected function compileValue($value) {
1161
		$value = $this->reduce($value);
1162
1163
		list($type) = $value;
1164
		switch ($type) {
1165
		case "keyword":
1166
			return $value[1];
1167
		case "color":
1168
			// [1] - red component (either number for a %)
1169
			// [2] - green component
1170
			// [3] - blue component
1171
			// [4] - optional alpha component
1172
			list(, $r, $g, $b) = $value;
1173
1174
			$r = round($r);
1175
			$g = round($g);
1176
			$b = round($b);
1177
1178 View Code Duplication
			if (count($value) == 5 && $value[4] != 1) { // rgba
1179
				return 'rgba('.$r.', '.$g.', '.$b.', '.$value[4].')';
1180
			}
1181
1182
			$h = sprintf("#%02x%02x%02x", $r, $g, $b);
1183
1184
			// Converting hex color to short notation (e.g. #003399 to #039)
1185 View Code Duplication
			if ($h[1] === $h[2] && $h[3] === $h[4] && $h[5] === $h[6]) {
1186
				$h = '#' . $h[1] . $h[3] . $h[5];
1187
			}
1188
1189
			return $h;
1190
		case "number":
1191
			return round($value[1], $this->numberPrecision) . $value[2];
1192
		case "string":
1193
			return $value[1] . $this->compileStringContent($value) . $value[1];
1194
		case "function":
1195
			$args = !empty($value[2]) ? $this->compileValue($value[2]) : "";
1196
			return "$value[1]($args)";
1197
		case "list":
1198
			$value = $this->extractInterpolation($value);
1199
			if ($value[0] != "list") return $this->compileValue($value);
1200
1201
			list(, $delim, $items) = $value;
1202
1203
			$filtered = array();
1204
			foreach ($items as $item) {
1205
				if ($item[0] == "null") continue;
1206
				$filtered[] = $this->compileValue($item);
1207
			}
1208
1209
			return implode("$delim ", $filtered);
1210
		case "interpolated": # node created by extractInterpolation
1211
			list(, $interpolate, $left, $right) = $value;
1212
			list(,, $whiteLeft, $whiteRight) = $interpolate;
1213
1214
			$left = count($left[2]) > 0 ?
1215
				$this->compileValue($left).$whiteLeft : "";
1216
1217
			$right = count($right[2]) > 0 ?
1218
				$whiteRight.$this->compileValue($right) : "";
1219
1220
			return $left.$this->compileValue($interpolate).$right;
1221
1222
		case "interpolate": # raw parse node
1223
			list(, $exp) = $value;
1224
1225
			// strip quotes if it's a string
1226
			$reduced = $this->reduce($exp);
1227
			switch ($reduced[0]) {
1228
				case "string":
1229
					$reduced = array("keyword",
1230
						$this->compileStringContent($reduced));
1231
					break;
1232
				case "null":
1233
					$reduced = array("keyword", "");
1234
			}
1235
1236
			return $this->compileValue($reduced);
1237
		case "null":
1238
			return "null";
1239
		default:
1240
			$this->throwError("unknown value type: $type");
1241
		}
1242
	}
1243
1244
	protected function compileStringContent($string) {
1245
		$parts = array();
1246
		foreach ($string[2] as $part) {
1247
			if (is_array($part)) {
1248
				$parts[] = $this->compileValue($part);
1249
			} else {
1250
				$parts[] = $part;
1251
			}
1252
		}
1253
1254
		return implode($parts);
1255
	}
1256
1257
	// doesn't need to be recursive, compileValue will handle that
1258
	protected function extractInterpolation($list) {
1259
		$items = $list[2];
1260
		foreach ($items as $i => $item) {
1261
			if ($item[0] == "interpolate") {
1262
				$before = array("list", $list[1], array_slice($items, 0, $i));
1263
				$after = array("list", $list[1], array_slice($items, $i + 1));
1264
				return array("interpolated", $item, $before, $after);
1265
			}
1266
		}
1267
		return $list;
1268
	}
1269
1270
	// find the final set of selectors
1271
	protected function multiplySelectors($env) {
1272
		$envs = array();
1273
		while (null !== $env) {
1274
			if (!empty($env->selectors)) {
1275
				$envs[] = $env;
1276
			}
1277
			$env = $env->parent;
1278
		};
1279
1280
		$selectors = array();
1281
		$parentSelectors = array(array());
1282
		while ($env = array_pop($envs)) {
1283
			$selectors = array();
1284
			foreach ($env->selectors as $selector) {
1285
				foreach ($parentSelectors as $parent) {
1286
					$selectors[] = $this->joinSelectors($parent, $selector);
1287
				}
1288
			}
1289
			$parentSelectors = $selectors;
1290
		}
1291
1292
		return $selectors;
1293
	}
1294
1295
	// looks for & to replace, or append parent before child
1296
	protected function joinSelectors($parent, $child) {
1297
		$setSelf = false;
1298
		$out = array();
1299
		foreach ($child as $part) {
1300
			$newPart = array();
1301
			foreach ($part as $p) {
1302
				if ($p == self::$selfSelector) {
1303
					$setSelf = true;
1304
					foreach ($parent as $i => $parentPart) {
1305
						if ($i > 0) {
1306
							$out[] = $newPart;
1307
							$newPart = array();
1308
						}
1309
1310
						foreach ($parentPart as $pp) {
1311
							$newPart[] = $pp;
1312
						}
1313
					}
1314
				} else {
1315
					$newPart[] = $p;
1316
				}
1317
			}
1318
1319
			$out[] = $newPart;
1320
		}
1321
1322
		return $setSelf ? $out : array_merge($parent, $child);
1323
	}
1324
1325 View Code Duplication
	protected function multiplyMedia($env, $childQueries = null) {
1326
		if (is_null($env) ||
1327
			!empty($env->block->type) && $env->block->type != "media")
1328
		{
1329
			return $childQueries;
1330
		}
1331
1332
		// plain old block, skip
1333
		if (empty($env->block->type)) {
1334
			return $this->multiplyMedia($env->parent, $childQueries);
1335
		}
1336
1337
		$parentQueries = $env->block->queryList;
1338
		if ($childQueries == null) {
1339
			$childQueries = $parentQueries;
1340
		} else {
1341
			$originalQueries = $childQueries;
1342
			$childQueries = array();
1343
1344
			foreach ($parentQueries as $parentQuery){
1345
				foreach ($originalQueries as $childQuery) {
1346
					$childQueries []= array_merge($parentQuery, $childQuery);
1347
				}
1348
			}
1349
		}
1350
1351
		return $this->multiplyMedia($env->parent, $childQueries);
1352
	}
1353
1354
	// convert something to list
1355
	protected function coerceList($item, $delim = ",") {
1356
		if (!is_null($item) && $item[0] == "list") {
1357
			return $item;
1358
		}
1359
1360
		return array("list", $delim, is_null($item) ? array(): array($item));
1361
	}
1362
1363
	protected function applyArguments($argDef, $argValues) {
1364
		$hasVariable = false;
1365
		$args = array();
1366
		foreach ($argDef as $i => $arg) {
1367
			list($name, $default, $isVariable) = $argDef[$i];
1368
			$args[$name] = array($i, $name, $default, $isVariable);
1369
			$hasVariable |= $isVariable;
1370
		}
1371
1372
		$keywordArgs = array();
1373
		$deferredKeywordArgs = array();
1374
		$remaining = array();
1375
		// assign the keyword args
1376
		foreach ((array) $argValues as $arg) {
1377
			if (!empty($arg[0])) {
1378
				if (!isset($args[$arg[0][1]])) {
1379
					if ($hasVariable) {
1380
						$deferredKeywordArgs[$arg[0][1]] = $arg[1];
1381
					} else {
1382
						$this->throwError("Mixin or function doesn't have an argument named $%s.", $arg[0][1]);
1383
					}
1384
				} elseif ($args[$arg[0][1]][0] < count($remaining)) {
1385
					$this->throwError("The argument $%s was passed both by position and by name.", $arg[0][1]);
1386
				} else {
1387
					$keywordArgs[$arg[0][1]] = $arg[1];
1388
				}
1389
			} elseif (count($keywordArgs)) {
1390
				$this->throwError('Positional arguments must come before keyword arguments.');
1391
			} elseif ($arg[2] == true) {
1392
				$val = $this->reduce($arg[1], true);
1393
				if ($val[0] == "list") {
1394
					foreach ($val[2] as $name => $item) {
1395
						if (!is_numeric($name)) {
1396
							$keywordArgs[$name] = $item;
1397
						} else {
1398
							$remaining[] = $item;
1399
						}
1400
					}
1401
				} else {
1402
					$remaining[] = $val;
1403
				}
1404
			} else {
1405
				$remaining[] = $arg[1];
1406
			}
1407
		}
1408
1409
		foreach ($args as $arg) {
1410
			list($i, $name, $default, $isVariable) = $arg;
1411
			if ($isVariable) {
1412
				$val = array("list", ",", array());
1413
				for ($count = count($remaining); $i < $count; $i++) {
1414
					$val[2][] = $remaining[$i];
1415
				}
1416
				foreach ($deferredKeywordArgs as $itemName => $item) {
1417
					$val[2][$itemName] = $item;
1418
				}
1419
			} elseif (isset($remaining[$i])) {
1420
				$val = $remaining[$i];
1421
			} elseif (isset($keywordArgs[$name])) {
1422
				$val = $keywordArgs[$name];
1423
			} elseif (!empty($default)) {
1424
				$val = $default;
1425
			} else {
1426
				$this->throwError("Missing argument $name");
1427
			}
1428
1429
			$this->set($name, $this->reduce($val, true), true);
0 ignored issues
show
Bug introduced by
The variable $val 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...
1430
		}
1431
	}
1432
1433
	protected function pushEnv($block=null) {
1434
		$env = new stdClass;
1435
		$env->parent = $this->env;
1436
		$env->store = array();
1437
		$env->block = $block;
1438
		$env->depth = isset($this->env->depth) ? $this->env->depth + 1 : 0;
1439
1440
		$this->env = $env;
1441
		return $env;
1442
	}
1443
1444
	protected function normalizeName($name) {
1445
		return str_replace("-", "_", $name);
1446
	}
1447
1448
	protected function getStoreEnv() {
1449
		return isset($this->storeEnv) ? $this->storeEnv : $this->env;
1450
	}
1451
1452
	protected function set($name, $value, $shadow=false) {
1453
		$name = $this->normalizeName($name);
1454
1455
		if ($shadow) {
1456
			$this->setRaw($name, $value);
1457
		} else {
1458
			$this->setExisting($name, $value);
1459
		}
1460
	}
1461
1462
	protected function setExisting($name, $value, $env = null) {
1463
		if (is_null($env)) $env = $this->getStoreEnv();
1464
1465
		if (isset($env->store[$name]) || is_null($env->parent)) {
1466
			$env->store[$name] = $value;
1467
		} else {
1468
			$this->setExisting($name, $value, $env->parent);
1469
		}
1470
	}
1471
1472
	protected function setRaw($name, $value) {
1473
		$env = $this->getStoreEnv();
1474
		$env->store[$name] = $value;
1475
	}
1476
1477
	public function get($name, $defaultValue = null, $env = null) {
1478
		$name = $this->normalizeName($name);
1479
1480
		if (is_null($env)) $env = $this->getStoreEnv();
1481
		if (is_null($defaultValue)) $defaultValue = self::$defaultValue;
1482
1483 View Code Duplication
		if (isset($env->store[$name])) {
1484
			return $env->store[$name];
1485
		} elseif (isset($env->parent)) {
1486
			return $this->get($name, $defaultValue, $env->parent);
1487
		}
1488
1489
		return $defaultValue; // found nothing
1490
	}
1491
1492
	protected function popEnv() {
1493
		$env = $this->env;
1494
		$this->env = $this->env->parent;
1495
		return $env;
1496
	}
1497
1498
	public function getParsedFiles() {
1499
		return $this->parsedFiles;
1500
	}
1501
1502
	public function addImportPath($path) {
1503
		$this->importPaths[] = $path;
1504
	}
1505
1506
	public function setImportPaths($path) {
1507
		$this->importPaths = (array)$path;
1508
	}
1509
1510
	public function setNumberPrecision($numberPrecision) {
1511
		$this->numberPrecision = $numberPrecision;
1512
	}
1513
1514
	public function setFormatter($formatterName) {
1515
		$this->formatter = $formatterName;
1516
	}
1517
1518
	public function registerFunction($name, $func) {
1519
		$this->userFunctions[$this->normalizeName($name)] = $func;
1520
	}
1521
1522
	public function unregisterFunction($name) {
1523
		unset($this->userFunctions[$this->normalizeName($name)]);
1524
	}
1525
1526
	protected function importFile($path, $out) {
1527
		// see if tree is cached
1528
		$realPath = realpath($path);
1529
		if (isset($this->importCache[$realPath])) {
1530
			$tree = $this->importCache[$realPath];
1531
		} else {
1532
			$code = file_get_contents($path);
1533
			$parser = new scss_parser($path, false);
1534
			$tree = $parser->parse($code);
1535
			$this->parsedFiles[] = $path;
1536
1537
			$this->importCache[$realPath] = $tree;
1538
		}
1539
1540
		$pi = pathinfo($path);
1541
		array_unshift($this->importPaths, $pi['dirname']);
1542
		$this->compileChildren($tree->children, $out);
1543
		array_shift($this->importPaths);
1544
	}
1545
1546
	// results the file path for an import url if it exists
1547
	public function findImport($url) {
1548
		$urls = array();
1549
1550
		// for "normal" scss imports (ignore vanilla css and external requests)
1551
		if (!preg_match('/\.css|^http:\/\/$/', $url)) {
1552
			// try both normal and the _partial filename
1553
			$urls = array($url, preg_replace('/[^\/]+$/', '_\0', $url));
1554
		}
1555
1556
		foreach ($this->importPaths as $dir) {
1557
			if (is_string($dir)) {
1558
				// check urls for normal import paths
1559
				foreach ($urls as $full) {
1560
					$full = $dir .
1561
						(!empty($dir) && substr($dir, -1) != '/' ? '/' : '') .
1562
						$full;
1563
1564
					if ($this->fileExists($file = $full.'.scss') ||
1565
						$this->fileExists($file = $full))
1566
					{
1567
						return $file;
1568
					}
1569
				}
1570
			} else {
1571
				// check custom callback for import path
1572
				$file = call_user_func($dir,$url,$this);
1573
				if ($file !== null) {
1574
					return $file;
1575
				}
1576
			}
1577
		}
1578
1579
		return null;
1580
	}
1581
1582
	protected function fileExists($name) {
1583
		return is_file($name);
1584
	}
1585
1586
	protected function callBuiltin($name, $args, &$returnValue) {
1587
		// try a lib function
1588
		$name = $this->normalizeName($name);
1589
		$libName = "lib_".$name;
1590
		$f = array($this, $libName);
1591
		$prototype = isset(self::$$libName) ? self::$$libName : null;
1592
1593
		if (is_callable($f)) {
1594
			$sorted = $this->sortArgs($prototype, $args);
1595
			foreach ($sorted as &$val) {
1596
				$val = $this->reduce($val, true);
1597
			}
1598
			$returnValue = call_user_func($f, $sorted, $this);
1599
		} elseif (isset($this->userFunctions[$name])) {
1600
			// see if we can find a user function
1601
			$fn = $this->userFunctions[$name];
1602
1603
			foreach ($args as &$val) {
1604
				$val = $this->reduce($val[1], true);
1605
			}
1606
1607
			$returnValue = call_user_func($fn, $args, $this);
1608
		}
1609
1610
		if (isset($returnValue)) {
1611
			// coerce a php value into a scss one
1612
			if (is_numeric($returnValue)) {
1613
				$returnValue = array('number', $returnValue, "");
1614
			} elseif (is_bool($returnValue)) {
1615
				$returnValue = $returnValue ? self::$true : self::$false;
1616
			} elseif (!is_array($returnValue)) {
1617
				$returnValue = array('keyword', $returnValue);
1618
			}
1619
1620
			return true;
1621
		}
1622
1623
		return false;
1624
	}
1625
1626
	// sorts any keyword arguments
1627
	// TODO: merge with apply arguments
1628
	protected function sortArgs($prototype, $args) {
1629
		$keyArgs = array();
1630
		$posArgs = array();
1631
1632
		foreach ($args as $arg) {
1633
			list($key, $value) = $arg;
1634
			$key = $key[1];
1635
			if (empty($key)) {
1636
				$posArgs[] = $value;
1637
			} else {
1638
				$keyArgs[$key] = $value;
1639
			}
1640
		}
1641
1642
		if (is_null($prototype)) return $posArgs;
1643
1644
		$finalArgs = array();
1645
		foreach ($prototype as $i => $names) {
1646
			if (isset($posArgs[$i])) {
1647
				$finalArgs[] = $posArgs[$i];
1648
				continue;
1649
			}
1650
1651
			$set = false;
1652
			foreach ((array)$names as $name) {
1653
				if (isset($keyArgs[$name])) {
1654
					$finalArgs[] = $keyArgs[$name];
1655
					$set = true;
1656
					break;
1657
				}
1658
			}
1659
1660
			if (!$set) {
1661
				$finalArgs[] = null;
1662
			}
1663
		}
1664
1665
		return $finalArgs;
1666
	}
1667
1668
	protected function coerceForExpression($value) {
1669
		if ($color = $this->coerceColor($value)) {
1670
			return $color;
1671
		}
1672
1673
		return $value;
1674
	}
1675
1676
	protected function coerceColor($value) {
1677
		switch ($value[0]) {
1678
		case "color": return $value;
1679
		case "keyword":
1680
			$name = $value[1];
1681
			if (isset(self::$cssColors[$name])) {
1682
				@list($r, $g, $b, $a) = explode(',', self::$cssColors[$name]);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition here. This can introduce security issues, and is generally not recommended.

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
1683
				return isset($a)
1684
					? array('color', (int) $r, (int) $g, (int) $b, (int) $a)
1685
					: array('color', (int) $r, (int) $g, (int) $b);
1686
			}
1687
			return null;
1688
		}
1689
1690
		return null;
1691
	}
1692
1693 View Code Duplication
	protected function coerceString($value) {
1694
		switch ($value[0]) {
1695
		case "string":
1696
			return $value;
1697
		case "keyword":
1698
			return array("string", "", array($value[1]));
1699
		}
1700
		return null;
1701
	}
1702
1703
	public function assertList($value) {
1704
		if ($value[0] != "list")
1705
			$this->throwError("expecting list");
1706
		return $value;
1707
	}
1708
1709
	public function assertColor($value) {
1710
		if ($color = $this->coerceColor($value)) return $color;
1711
		$this->throwError("expecting color");
1712
	}
1713
1714
	public function assertNumber($value) {
1715
		if ($value[0] != "number")
1716
			$this->throwError("expecting number");
1717
		return $value[1];
1718
	}
1719
1720
	protected function coercePercent($value) {
1721
		if ($value[0] == "number") {
1722
			if ($value[2] == "%") {
1723
				return $value[1] / 100;
1724
			}
1725
			return $value[1];
1726
		}
1727
		return 0;
1728
	}
1729
1730
	// make sure a color's components don't go out of bounds
1731 View Code Duplication
	protected function fixColor($c) {
1732
		foreach (range(1, 3) as $i) {
1733
			if ($c[$i] < 0) $c[$i] = 0;
1734
			if ($c[$i] > 255) $c[$i] = 255;
1735
		}
1736
1737
		return $c;
1738
	}
1739
1740
	public function toHSL($red, $green, $blue) {
1741
		$r = $red / 255;
1742
		$g = $green / 255;
1743
		$b = $blue / 255;
1744
1745
		$min = min($r, $g, $b);
1746
		$max = max($r, $g, $b);
1747
		$d = $max - $min;
1748
		$l = ($min + $max) / 2;
1749
1750
		if ($min == $max) {
1751
			$s = $h = 0;
1752
		} else {
1753
			if ($l < 0.5)
1754
				$s = $d / (2 * $l);
1755
			else
1756
				$s = $d / (2 - 2 * $l);
1757
1758
			if ($r == $max)
1759
				$h = 60 * ($g - $b) / $d;
1760
			elseif ($g == $max)
1761
				$h = 60 * ($b - $r) / $d + 120;
1762
			elseif ($b == $max)
1763
				$h = 60 * ($r - $g) / $d + 240;
1764
		}
1765
1766
		return array('hsl', fmod($h, 360), $s * 100, $l * 100);
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...
1767
	}
1768
1769
	public function hueToRGB($m1, $m2, $h) {
1770
		if ($h < 0)
1771
			$h += 1;
1772
		elseif ($h > 1)
1773
			$h -= 1;
1774
1775 View Code Duplication
		if ($h * 6 < 1)
1776
			return $m1 + ($m2 - $m1) * $h * 6;
1777
1778
		if ($h * 2 < 1)
1779
			return $m2;
1780
1781 View Code Duplication
		if ($h * 3 < 2)
1782
			return $m1 + ($m2 - $m1) * (2/3 - $h) * 6;
1783
1784
		return $m1;
1785
	}
1786
1787
	// H from 0 to 360, S and L from 0 to 100
1788
	public function toRGB($hue, $saturation, $lightness) {
1789
		if ($hue < 0) {
1790
			$hue += 360;
1791
		}
1792
1793
		$h = $hue / 360;
1794
		$s = min(100, max(0, $saturation)) / 100;
1795
		$l = min(100, max(0, $lightness)) / 100;
1796
1797
		$m2 = $l <= 0.5 ? $l * ($s + 1) : $l + $s - $l * $s;
1798
		$m1 = $l * 2 - $m2;
1799
1800
		$r = $this->hueToRGB($m1, $m2, $h + 1/3) * 255;
1801
		$g = $this->hueToRGB($m1, $m2, $h) * 255;
1802
		$b = $this->hueToRGB($m1, $m2, $h - 1/3) * 255;
1803
1804
		$out = array('color', $r, $g, $b);
1805
		return $out;
1806
	}
1807
1808
	// Built in functions
1809
1810
	protected static $lib_if = array("condition", "if-true", "if-false");
1811
	protected function lib_if($args) {
1812
		list($cond,$t, $f) = $args;
1813
		if ($cond == self::$false) return $f;
1814
		return $t;
1815
	}
1816
1817
	protected static $lib_index = array("list", "value");
1818
	protected function lib_index($args) {
1819
		list($list, $value) = $args;
1820
		$list = $this->assertList($list);
1821
1822
		$values = array();
1823
		foreach ($list[2] as $item) {
1824
			$values[] = $this->normalizeValue($item);
1825
		}
1826
		$key = array_search($this->normalizeValue($value), $values);
1827
1828
		return false === $key ? false : $key + 1;
1829
	}
1830
1831
	protected static $lib_rgb = array("red", "green", "blue");
1832
	protected function lib_rgb($args) {
1833
		list($r,$g,$b) = $args;
1834
		return array("color", $r[1], $g[1], $b[1]);
1835
	}
1836
1837
	protected static $lib_rgba = array(
1838
		array("red", "color"),
1839
		"green", "blue", "alpha");
1840
	protected function lib_rgba($args) {
1841
		if ($color = $this->coerceColor($args[0])) {
1842
			$num = is_null($args[1]) ? $args[3] : $args[1];
1843
			$alpha = $this->assertNumber($num);
1844
			$color[4] = $alpha;
1845
			return $color;
1846
		}
1847
1848
		list($r,$g,$b, $a) = $args;
1849
		return array("color", $r[1], $g[1], $b[1], $a[1]);
1850
	}
1851
1852
	// helper function for adjust_color, change_color, and scale_color
1853
	protected function alter_color($args, $fn) {
1854
		$color = $this->assertColor($args[0]);
1855
1856
		foreach (array(1,2,3,7) as $i) {
1857
			if (!is_null($args[$i])) {
1858
				$val = $this->assertNumber($args[$i]);
1859
				$ii = $i == 7 ? 4 : $i; // alpha
1860
				$color[$ii] =
1861
					$this->$fn(isset($color[$ii]) ? $color[$ii] : 0, $val, $i);
1862
			}
1863
		}
1864
1865
		if (!is_null($args[4]) || !is_null($args[5]) || !is_null($args[6])) {
1866
			$hsl = $this->toHSL($color[1], $color[2], $color[3]);
1867
			foreach (array(4,5,6) as $i) {
1868
				if (!is_null($args[$i])) {
1869
					$val = $this->assertNumber($args[$i]);
1870
					$hsl[$i - 3] = $this->$fn($hsl[$i - 3], $val, $i);
1871
				}
1872
			}
1873
1874
			$rgb = $this->toRGB($hsl[1], $hsl[2], $hsl[3]);
1875
			if (isset($color[4])) $rgb[4] = $color[4];
1876
			$color = $rgb;
1877
		}
1878
1879
		return $color;
1880
	}
1881
1882
	protected static $lib_adjust_color = array(
1883
		"color", "red", "green", "blue",
1884
		"hue", "saturation", "lightness", "alpha"
1885
	);
1886
	protected function adjust_color_helper($base, $alter, $i) {
1887
		return $base += $alter;
1888
	}
1889
	protected function lib_adjust_color($args) {
1890
		return $this->alter_color($args, "adjust_color_helper");
1891
	}
1892
1893
	protected static $lib_change_color = array(
1894
		"color", "red", "green", "blue",
1895
		"hue", "saturation", "lightness", "alpha"
1896
	);
1897
	protected function change_color_helper($base, $alter, $i) {
1898
		return $alter;
1899
	}
1900
	protected function lib_change_color($args) {
1901
		return $this->alter_color($args, "change_color_helper");
1902
	}
1903
1904
	protected static $lib_scale_color = array(
1905
		"color", "red", "green", "blue",
1906
		"hue", "saturation", "lightness", "alpha"
1907
	);
1908
	protected function scale_color_helper($base, $scale, $i) {
1909
		// 1,2,3 - rgb
1910
		// 4, 5, 6 - hsl
1911
		// 7 - a
1912
		switch ($i) {
1913
		case 1:
1914
		case 2:
1915
		case 3:
1916
			$max = 255; break;
1917
		case 4:
1918
			$max = 360; break;
1919
		case 7:
1920
			$max = 1; break;
1921
		default:
1922
			$max = 100;
1923
		}
1924
1925
		$scale = $scale / 100;
1926
		if ($scale < 0) {
1927
			return $base * $scale + $base;
1928
		} else {
1929
			return ($max - $base) * $scale + $base;
1930
		}
1931
	}
1932
	protected function lib_scale_color($args) {
1933
		return $this->alter_color($args, "scale_color_helper");
1934
	}
1935
1936
	protected static $lib_ie_hex_str = array("color");
1937
	protected function lib_ie_hex_str($args) {
1938
		$color = $this->coerceColor($args[0]);
1939
		$color[4] = isset($color[4]) ? round(255*$color[4]) : 255;
1940
1941
		return sprintf('#%02X%02X%02X%02X', $color[4], $color[1], $color[2], $color[3]);
1942
	}
1943
1944
	protected static $lib_red = array("color");
1945
	protected function lib_red($args) {
1946
		$color = $this->coerceColor($args[0]);
1947
		return $color[1];
1948
	}
1949
1950
	protected static $lib_green = array("color");
1951
	protected function lib_green($args) {
1952
		$color = $this->coerceColor($args[0]);
1953
		return $color[2];
1954
	}
1955
1956
	protected static $lib_blue = array("color");
1957
	protected function lib_blue($args) {
1958
		$color = $this->coerceColor($args[0]);
1959
		return $color[3];
1960
	}
1961
1962
	protected static $lib_alpha = array("color");
1963
	protected function lib_alpha($args) {
1964
		if ($color = $this->coerceColor($args[0])) {
1965
			return isset($color[4]) ? $color[4] : 1;
1966
		}
1967
1968
		// this might be the IE function, so return value unchanged
1969
		return null;
1970
	}
1971
1972
	protected static $lib_opacity = array("color");
1973
	protected function lib_opacity($args) {
1974
		$value = $args[0];
1975
		if ($value[0] === 'number') return null;
1976
		return $this->lib_alpha($args);
1977
	}
1978
1979
	// mix two colors
1980
	protected static $lib_mix = array("color-1", "color-2", "weight");
1981
	protected function lib_mix($args) {
1982
		list($first, $second, $weight) = $args;
1983
		$first = $this->assertColor($first);
1984
		$second = $this->assertColor($second);
1985
1986
		if (is_null($weight)) {
1987
			$weight = 0.5;
1988
		} else {
1989
			$weight = $this->coercePercent($weight);
1990
		}
1991
1992
		$firstAlpha = isset($first[4]) ? $first[4] : 1;
1993
		$secondAlpha = isset($second[4]) ? $second[4] : 1;
1994
1995
		$w = $weight * 2 - 1;
1996
		$a = $firstAlpha - $secondAlpha;
1997
1998
		$w1 = (($w * $a == -1 ? $w : ($w + $a)/(1 + $w * $a)) + 1) / 2.0;
1999
		$w2 = 1.0 - $w1;
2000
2001
		$new = array('color',
2002
			$w1 * $first[1] + $w2 * $second[1],
2003
			$w1 * $first[2] + $w2 * $second[2],
2004
			$w1 * $first[3] + $w2 * $second[3],
2005
		);
2006
2007
		if ($firstAlpha != 1.0 || $secondAlpha != 1.0) {
2008
			$new[] = $firstAlpha * $weight + $secondAlpha * ($weight - 1);
2009
		}
2010
2011
		return $this->fixColor($new);
2012
	}
2013
2014
	protected static $lib_hsl = array("hue", "saturation", "lightness");
2015
	protected function lib_hsl($args) {
2016
		list($h, $s, $l) = $args;
2017
		return $this->toRGB($h[1], $s[1], $l[1]);
2018
	}
2019
2020
	protected static $lib_hsla = array("hue", "saturation",
2021
		"lightness", "alpha");
2022
	protected function lib_hsla($args) {
2023
		list($h, $s, $l, $a) = $args;
2024
		$color = $this->toRGB($h[1], $s[1], $l[1]);
2025
		$color[4] = $a[1];
2026
		return $color;
2027
	}
2028
2029
	protected static $lib_hue = array("color");
2030
	protected function lib_hue($args) {
2031
		$color = $this->assertColor($args[0]);
2032
		$hsl = $this->toHSL($color[1], $color[2], $color[3]);
2033
		return array("number", $hsl[1], "deg");
2034
	}
2035
2036
	protected static $lib_saturation = array("color");
2037
	protected function lib_saturation($args) {
2038
		$color = $this->assertColor($args[0]);
2039
		$hsl = $this->toHSL($color[1], $color[2], $color[3]);
2040
		return array("number", $hsl[2], "%");
2041
	}
2042
2043
	protected static $lib_lightness = array("color");
2044
	protected function lib_lightness($args) {
2045
		$color = $this->assertColor($args[0]);
2046
		$hsl = $this->toHSL($color[1], $color[2], $color[3]);
2047
		return array("number", $hsl[3], "%");
2048
	}
2049
2050
	protected function adjustHsl($color, $idx, $amount) {
2051
		$hsl = $this->toHSL($color[1], $color[2], $color[3]);
2052
		$hsl[$idx] += $amount;
2053
		$out = $this->toRGB($hsl[1], $hsl[2], $hsl[3]);
2054
		if (isset($color[4])) $out[4] = $color[4];
2055
		return $out;
2056
	}
2057
2058
	protected static $lib_adjust_hue = array("color", "degrees");
2059
	protected function lib_adjust_hue($args) {
2060
		$color = $this->assertColor($args[0]);
2061
		$degrees = $this->assertNumber($args[1]);
2062
		return $this->adjustHsl($color, 1, $degrees);
2063
	}
2064
2065
	protected static $lib_lighten = array("color", "amount");
2066
	protected function lib_lighten($args) {
2067
		$color = $this->assertColor($args[0]);
2068
		$amount = 100*$this->coercePercent($args[1]);
2069
		return $this->adjustHsl($color, 3, $amount);
2070
	}
2071
2072
	protected static $lib_darken = array("color", "amount");
2073
	protected function lib_darken($args) {
2074
		$color = $this->assertColor($args[0]);
2075
		$amount = 100*$this->coercePercent($args[1]);
2076
		return $this->adjustHsl($color, 3, -$amount);
2077
	}
2078
2079
	protected static $lib_saturate = array("color", "amount");
2080
	protected function lib_saturate($args) {
2081
		$value = $args[0];
2082
		if ($value[0] === 'number') return null;
2083
		$color = $this->assertColor($value);
2084
		$amount = 100*$this->coercePercent($args[1]);
2085
		return $this->adjustHsl($color, 2, $amount);
2086
	}
2087
2088
	protected static $lib_desaturate = array("color", "amount");
2089
	protected function lib_desaturate($args) {
2090
		$color = $this->assertColor($args[0]);
2091
		$amount = 100*$this->coercePercent($args[1]);
2092
		return $this->adjustHsl($color, 2, -$amount);
2093
	}
2094
2095
	protected static $lib_grayscale = array("color");
2096
	protected function lib_grayscale($args) {
2097
		$value = $args[0];
2098
		if ($value[0] === 'number') return null;
2099
		return $this->adjustHsl($this->assertColor($value), 2, -100);
2100
	}
2101
2102
	protected static $lib_complement = array("color");
2103
	protected function lib_complement($args) {
2104
		return $this->adjustHsl($this->assertColor($args[0]), 1, 180);
2105
	}
2106
2107
	protected static $lib_invert = array("color");
2108
	protected function lib_invert($args) {
2109
		$value = $args[0];
2110
		if ($value[0] === 'number') return null;
2111
		$color = $this->assertColor($value);
2112
		$color[1] = 255 - $color[1];
2113
		$color[2] = 255 - $color[2];
2114
		$color[3] = 255 - $color[3];
2115
		return $color;
2116
	}
2117
2118
	// increases opacity by amount
2119
	protected static $lib_opacify = array("color", "amount");
2120 View Code Duplication
	protected function lib_opacify($args) {
2121
		$color = $this->assertColor($args[0]);
2122
		$amount = $this->coercePercent($args[1]);
2123
2124
		$color[4] = (isset($color[4]) ? $color[4] : 1) + $amount;
2125
		$color[4] = min(1, max(0, $color[4]));
2126
		return $color;
2127
	}
2128
2129
	protected static $lib_fade_in = array("color", "amount");
2130
	protected function lib_fade_in($args) {
2131
		return $this->lib_opacify($args);
2132
	}
2133
2134
	// decreases opacity by amount
2135
	protected static $lib_transparentize = array("color", "amount");
2136 View Code Duplication
	protected function lib_transparentize($args) {
2137
		$color = $this->assertColor($args[0]);
2138
		$amount = $this->coercePercent($args[1]);
2139
2140
		$color[4] = (isset($color[4]) ? $color[4] : 1) - $amount;
2141
		$color[4] = min(1, max(0, $color[4]));
2142
		return $color;
2143
	}
2144
2145
	protected static $lib_fade_out = array("color", "amount");
2146
	protected function lib_fade_out($args) {
2147
		return $this->lib_transparentize($args);
2148
	}
2149
2150
	protected static $lib_unquote = array("string");
2151
	protected function lib_unquote($args) {
2152
		$str = $args[0];
2153
		if ($str[0] == "string") $str[1] = "";
2154
		return $str;
2155
	}
2156
2157
	protected static $lib_quote = array("string");
2158
	protected function lib_quote($args) {
2159
		$value = $args[0];
2160
		if ($value[0] == "string" && !empty($value[1]))
2161
			return $value;
2162
		return array("string", '"', array($value));
2163
	}
2164
2165
	protected static $lib_percentage = array("value");
2166
	protected function lib_percentage($args) {
2167
		return array("number",
2168
			$this->coercePercent($args[0]) * 100,
2169
			"%");
2170
	}
2171
2172
	protected static $lib_round = array("value");
2173
	protected function lib_round($args) {
2174
		$num = $args[0];
2175
		$num[1] = round($num[1]);
2176
		return $num;
2177
	}
2178
2179
	protected static $lib_floor = array("value");
2180
	protected function lib_floor($args) {
2181
		$num = $args[0];
2182
		$num[1] = floor($num[1]);
2183
		return $num;
2184
	}
2185
2186
	protected static $lib_ceil = array("value");
2187
	protected function lib_ceil($args) {
2188
		$num = $args[0];
2189
		$num[1] = ceil($num[1]);
2190
		return $num;
2191
	}
2192
2193
	protected static $lib_abs = array("value");
2194
	protected function lib_abs($args) {
2195
		$num = $args[0];
2196
		$num[1] = abs($num[1]);
2197
		return $num;
2198
	}
2199
2200 View Code Duplication
	protected function lib_min($args) {
2201
		$numbers = $this->getNormalizedNumbers($args);
2202
		$min = null;
2203
		foreach ($numbers as $key => $number) {
2204
			if (null === $min || $number[1] <= $min[1]) {
2205
				$min = array($key, $number[1]);
2206
			}
2207
		}
2208
2209
		return $args[$min[0]];
2210
	}
2211
2212 View Code Duplication
	protected function lib_max($args) {
2213
		$numbers = $this->getNormalizedNumbers($args);
2214
		$max = null;
2215
		foreach ($numbers as $key => $number) {
2216
			if (null === $max || $number[1] >= $max[1]) {
2217
				$max = array($key, $number[1]);
2218
			}
2219
		}
2220
2221
		return $args[$max[0]];
2222
	}
2223
2224
	protected function getNormalizedNumbers($args) {
2225
		$unit = null;
2226
		$originalUnit = null;
2227
		$numbers = array();
2228
		foreach ($args as $key => $item) {
2229
			if ('number' != $item[0]) {
2230
				$this->throwError("%s is not a number", $item[0]);
2231
			}
2232
			$number = $this->normalizeNumber($item);
2233
2234
			if (null === $unit) {
2235
				$unit = $number[2];
2236
				$originalUnit = $item[2];
2237
			} elseif ($unit !== $number[2]) {
2238
				$this->throwError('Incompatible units: "%s" and "%s".', $originalUnit, $item[2]);
2239
			}
2240
2241
			$numbers[$key] = $number;
2242
		}
2243
2244
		return $numbers;
2245
	}
2246
2247
	protected static $lib_length = array("list");
2248
	protected function lib_length($args) {
2249
		$list = $this->coerceList($args[0]);
2250
		return count($list[2]);
2251
	}
2252
2253
	protected static $lib_nth = array("list", "n");
2254
	protected function lib_nth($args) {
2255
		$list = $this->coerceList($args[0]);
2256
		$n = $this->assertNumber($args[1]) - 1;
2257
		return isset($list[2][$n]) ? $list[2][$n] : self::$defaultValue;
2258
	}
2259
2260
	protected function listSeparatorForJoin($list1, $sep) {
2261
		if (is_null($sep)) return $list1[1];
2262
		switch ($this->compileValue($sep)) {
2263
		case "comma":
2264
			return ",";
2265
		case "space":
2266
			return "";
2267
		default:
2268
			return $list1[1];
2269
		}
2270
	}
2271
2272
	protected static $lib_join = array("list1", "list2", "separator");
2273
	protected function lib_join($args) {
2274
		list($list1, $list2, $sep) = $args;
2275
		$list1 = $this->coerceList($list1, " ");
2276
		$list2 = $this->coerceList($list2, " ");
2277
		$sep = $this->listSeparatorForJoin($list1, $sep);
2278
		return array("list", $sep, array_merge($list1[2], $list2[2]));
2279
	}
2280
2281
	protected static $lib_append = array("list", "val", "separator");
2282
	protected function lib_append($args) {
2283
		list($list1, $value, $sep) = $args;
2284
		$list1 = $this->coerceList($list1, " ");
2285
		$sep = $this->listSeparatorForJoin($list1, $sep);
2286
		return array("list", $sep, array_merge($list1[2], array($value)));
2287
	}
2288
2289
	protected function lib_zip($args) {
2290
		foreach ($args as $arg) {
2291
			$this->assertList($arg);
2292
		}
2293
2294
		$lists = array();
2295
		$firstList = array_shift($args);
2296
		foreach ($firstList[2] as $key => $item) {
2297
			$list = array("list", "", array($item));
2298
			foreach ($args as $arg) {
2299 View Code Duplication
				if (isset($arg[2][$key])) {
2300
					$list[2][] = $arg[2][$key];
2301
				} else {
2302
					break 2;
2303
				}
2304
			}
2305
			$lists[] = $list;
2306
		}
2307
2308
		return array("list", ",", $lists);
2309
	}
2310
2311
	protected static $lib_type_of = array("value");
2312
	protected function lib_type_of($args) {
2313
		$value = $args[0];
2314
		switch ($value[0]) {
2315
		case "keyword":
2316
			if ($value == self::$true || $value == self::$false) {
2317
				return "bool";
2318
			}
2319
2320
			if ($this->coerceColor($value)) {
2321
				return "color";
2322
			}
2323
2324
			return "string";
2325
		default:
2326
			return $value[0];
2327
		}
2328
	}
2329
2330
	protected static $lib_unit = array("number");
2331
	protected function lib_unit($args) {
2332
		$num = $args[0];
2333
		if ($num[0] == "number") {
2334
			return array("string", '"', array($num[2]));
2335
		}
2336
		return "";
2337
	}
2338
2339
	protected static $lib_unitless = array("number");
2340
	protected function lib_unitless($args) {
2341
		$value = $args[0];
2342
		return $value[0] == "number" && empty($value[2]);
2343
	}
2344
2345
	protected static $lib_comparable = array("number-1", "number-2");
2346
	protected function lib_comparable($args) {
2347
		list($number1, $number2) = $args;
2348
		if (!isset($number1[0]) || $number1[0] != "number" || !isset($number2[0]) || $number2[0] != "number") {
2349
			$this->throwError('Invalid argument(s) for "comparable"');
2350
		}
2351
2352
		$number1 = $this->normalizeNumber($number1);
2353
		$number2 = $this->normalizeNumber($number2);
2354
2355
		return $number1[2] == $number2[2] || $number1[2] == "" || $number2[2] == "";
2356
	}
2357
2358
	/**
2359
	 * Workaround IE7's content counter bug.
2360
	 *
2361
	 * @param array $args
2362
	 */
2363
	protected function lib_counter($args) {
2364
		$list = array_map(array($this, 'compileValue'), $args);
2365
		return array('string', '', array('counter(' . implode(',', $list) . ')'));
2366
	}
2367
2368
	public function throwError($msg = null) {
2369
		if (func_num_args() > 1) {
2370
			$msg = call_user_func_array("sprintf", func_get_args());
2371
		}
2372
2373
		if ($this->sourcePos >= 0 && isset($this->sourceParser)) {
2374
			$this->sourceParser->throwParseError($msg, $this->sourcePos);
0 ignored issues
show
Bug introduced by
The property sourceParser does not seem to exist. Did you mean 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...
2375
		}
2376
2377
		throw new Exception($msg);
2378
	}
2379
2380
	/**
2381
	 * CSS Colors
2382
	 *
2383
	 * @see https://www.w3.org/TR/css-color-4/
2384
	 */
2385
	static protected $cssColors = array(
2386
		'aliceblue' => '240,248,255',
2387
		'antiquewhite' => '250,235,215',
2388
		'aqua' => '0,255,255',
2389
		'aquamarine' => '127,255,212',
2390
		'azure' => '240,255,255',
2391
		'beige' => '245,245,220',
2392
		'bisque' => '255,228,196',
2393
		'black' => '0,0,0',
2394
		'blanchedalmond' => '255,235,205',
2395
		'blue' => '0,0,255',
2396
		'blueviolet' => '138,43,226',
2397
		'brown' => '165,42,42',
2398
		'burlywood' => '222,184,135',
2399
		'cadetblue' => '95,158,160',
2400
		'chartreuse' => '127,255,0',
2401
		'chocolate' => '210,105,30',
2402
		'coral' => '255,127,80',
2403
		'cornflowerblue' => '100,149,237',
2404
		'cornsilk' => '255,248,220',
2405
		'crimson' => '220,20,60',
2406
		'cyan' => '0,255,255',
2407
		'darkblue' => '0,0,139',
2408
		'darkcyan' => '0,139,139',
2409
		'darkgoldenrod' => '184,134,11',
2410
		'darkgray' => '169,169,169',
2411
		'darkgreen' => '0,100,0',
2412
		'darkgrey' => '169,169,169',
2413
		'darkkhaki' => '189,183,107',
2414
		'darkmagenta' => '139,0,139',
2415
		'darkolivegreen' => '85,107,47',
2416
		'darkorange' => '255,140,0',
2417
		'darkorchid' => '153,50,204',
2418
		'darkred' => '139,0,0',
2419
		'darksalmon' => '233,150,122',
2420
		'darkseagreen' => '143,188,143',
2421
		'darkslateblue' => '72,61,139',
2422
		'darkslategray' => '47,79,79',
2423
		'darkslategrey' => '47,79,79',
2424
		'darkturquoise' => '0,206,209',
2425
		'darkviolet' => '148,0,211',
2426
		'deeppink' => '255,20,147',
2427
		'deepskyblue' => '0,191,255',
2428
		'dimgray' => '105,105,105',
2429
		'dimgrey' => '105,105,105',
2430
		'dodgerblue' => '30,144,255',
2431
		'firebrick' => '178,34,34',
2432
		'floralwhite' => '255,250,240',
2433
		'forestgreen' => '34,139,34',
2434
		'fuchsia' => '255,0,255',
2435
		'gainsboro' => '220,220,220',
2436
		'ghostwhite' => '248,248,255',
2437
		'gold' => '255,215,0',
2438
		'goldenrod' => '218,165,32',
2439
		'gray' => '128,128,128',
2440
		'green' => '0,128,0',
2441
		'greenyellow' => '173,255,47',
2442
		'grey' => '128,128,128',
2443
		'honeydew' => '240,255,240',
2444
		'hotpink' => '255,105,180',
2445
		'indianred' => '205,92,92',
2446
		'indigo' => '75,0,130',
2447
		'ivory' => '255,255,240',
2448
		'khaki' => '240,230,140',
2449
		'lavender' => '230,230,250',
2450
		'lavenderblush' => '255,240,245',
2451
		'lawngreen' => '124,252,0',
2452
		'lemonchiffon' => '255,250,205',
2453
		'lightblue' => '173,216,230',
2454
		'lightcoral' => '240,128,128',
2455
		'lightcyan' => '224,255,255',
2456
		'lightgoldenrodyellow' => '250,250,210',
2457
		'lightgray' => '211,211,211',
2458
		'lightgreen' => '144,238,144',
2459
		'lightgrey' => '211,211,211',
2460
		'lightpink' => '255,182,193',
2461
		'lightsalmon' => '255,160,122',
2462
		'lightseagreen' => '32,178,170',
2463
		'lightskyblue' => '135,206,250',
2464
		'lightslategray' => '119,136,153',
2465
		'lightslategrey' => '119,136,153',
2466
		'lightsteelblue' => '176,196,222',
2467
		'lightyellow' => '255,255,224',
2468
		'lime' => '0,255,0',
2469
		'limegreen' => '50,205,50',
2470
		'linen' => '250,240,230',
2471
		'magenta' => '255,0,255',
2472
		'maroon' => '128,0,0',
2473
		'mediumaquamarine' => '102,205,170',
2474
		'mediumblue' => '0,0,205',
2475
		'mediumorchid' => '186,85,211',
2476
		'mediumpurple' => '147,112,219',
2477
		'mediumseagreen' => '60,179,113',
2478
		'mediumslateblue' => '123,104,238',
2479
		'mediumspringgreen' => '0,250,154',
2480
		'mediumturquoise' => '72,209,204',
2481
		'mediumvioletred' => '199,21,133',
2482
		'midnightblue' => '25,25,112',
2483
		'mintcream' => '245,255,250',
2484
		'mistyrose' => '255,228,225',
2485
		'moccasin' => '255,228,181',
2486
		'navajowhite' => '255,222,173',
2487
		'navy' => '0,0,128',
2488
		'oldlace' => '253,245,230',
2489
		'olive' => '128,128,0',
2490
		'olivedrab' => '107,142,35',
2491
		'orange' => '255,165,0',
2492
		'orangered' => '255,69,0',
2493
		'orchid' => '218,112,214',
2494
		'palegoldenrod' => '238,232,170',
2495
		'palegreen' => '152,251,152',
2496
		'paleturquoise' => '175,238,238',
2497
		'palevioletred' => '219,112,147',
2498
		'papayawhip' => '255,239,213',
2499
		'peachpuff' => '255,218,185',
2500
		'peru' => '205,133,63',
2501
		'pink' => '255,192,203',
2502
		'plum' => '221,160,221',
2503
		'powderblue' => '176,224,230',
2504
		'purple' => '128,0,128',
2505
		'rebeccapurple' => '102,51,153',
2506
		'red' => '255,0,0',
2507
		'rosybrown' => '188,143,143',
2508
		'royalblue' => '65,105,225',
2509
		'saddlebrown' => '139,69,19',
2510
		'salmon' => '250,128,114',
2511
		'sandybrown' => '244,164,96',
2512
		'seagreen' => '46,139,87',
2513
		'seashell' => '255,245,238',
2514
		'sienna' => '160,82,45',
2515
		'silver' => '192,192,192',
2516
		'skyblue' => '135,206,235',
2517
		'slateblue' => '106,90,205',
2518
		'slategray' => '112,128,144',
2519
		'slategrey' => '112,128,144',
2520
		'snow' => '255,250,250',
2521
		'springgreen' => '0,255,127',
2522
		'steelblue' => '70,130,180',
2523
		'tan' => '210,180,140',
2524
		'teal' => '0,128,128',
2525
		'thistle' => '216,191,216',
2526
		'tomato' => '255,99,71',
2527
		'transparent' => '0,0,0,0',
2528
		'turquoise' => '64,224,208',
2529
		'violet' => '238,130,238',
2530
		'wheat' => '245,222,179',
2531
		'white' => '255,255,255',
2532
		'whitesmoke' => '245,245,245',
2533
		'yellow' => '255,255,0',
2534
		'yellowgreen' => '154,205,50'
2535
	);
2536
}
2537
2538
/**
2539
 * SCSS parser
2540
 *
2541
 * @author Leaf Corcoran <[email protected]>
2542
 */
2543
class scss_parser {
2544
	static protected $precedence = array(
2545
		"or" => 0,
2546
		"and" => 1,
2547
2548
		'==' => 2,
2549
		'!=' => 2,
2550
		'<=' => 2,
2551
		'>=' => 2,
2552
		'=' => 2,
2553
		'<' => 3,
2554
		'>' => 2,
2555
2556
		'+' => 3,
2557
		'-' => 3,
2558
		'*' => 4,
2559
		'/' => 4,
2560
		'%' => 4,
2561
	);
2562
2563
	static protected $operators = array("+", "-", "*", "/", "%",
2564
		"==", "!=", "<=", ">=", "<", ">", "and", "or");
2565
2566
	static protected $operatorStr;
2567
	static protected $whitePattern;
2568
	static protected $commentMulti;
2569
2570
	static protected $commentSingle = "//";
2571
	static protected $commentMultiLeft = "/*";
2572
	static protected $commentMultiRight = "*/";
2573
2574
	public function __construct($sourceName = null, $rootParser = true) {
2575
		$this->sourceName = $sourceName;
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...
2576
		$this->rootParser = $rootParser;
0 ignored issues
show
Bug introduced by
The property rootParser 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...
2577
2578
		if (empty(self::$operatorStr)) {
2579
			self::$operatorStr = $this->makeOperatorStr(self::$operators);
2580
2581
			$commentSingle = $this->preg_quote(self::$commentSingle);
2582
			$commentMultiLeft = $this->preg_quote(self::$commentMultiLeft);
2583
			$commentMultiRight = $this->preg_quote(self::$commentMultiRight);
2584
			self::$commentMulti = $commentMultiLeft.'.*?'.$commentMultiRight;
2585
			self::$whitePattern = '/'.$commentSingle.'[^\n]*\s*|('.self::$commentMulti.')\s*|\s+/Ais';
2586
		}
2587
	}
2588
2589
	static protected function makeOperatorStr($operators) {
2590
		return '('.implode('|', array_map(array('scss_parser','preg_quote'),
2591
			$operators)).')';
2592
	}
2593
2594
	public function parse($buffer) {
2595
		$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...
2596
		$this->env = null;
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...
2597
		$this->inParens = false;
0 ignored issues
show
Bug introduced by
The property inParens 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...
2598
		$this->pushBlock(null); // root block
2599
		$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...
2600
		$this->insertComments = true;
0 ignored issues
show
Bug introduced by
The property insertComments 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...
2601
2602
		$this->buffer = $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...
2603
2604
		$this->whitespace();
2605
		while (false !== $this->parseChunk());
2606
2607
		if ($this->count != strlen($this->buffer))
2608
			$this->throwParseError();
2609
2610
		if (!empty($this->env->parent)) {
2611
			$this->throwParseError("unclosed block");
2612
		}
2613
2614
		$this->env->isRoot = true;
2615
		return $this->env;
2616
	}
2617
2618
	/**
2619
	 * Parse a single chunk off the head of the buffer and append it to the
2620
	 * current parse environment.
2621
	 *
2622
	 * Returns false when the buffer is empty, or when there is an error.
2623
	 *
2624
	 * This function is called repeatedly until the entire document is
2625
	 * parsed.
2626
	 *
2627
	 * This parser is most similar to a recursive descent parser. Single
2628
	 * functions represent discrete grammatical rules for the language, and
2629
	 * they are able to capture the text that represents those rules.
2630
	 *
2631
	 * Consider the function scssc::keyword(). (All parse functions are
2632
	 * structured the same.)
2633
	 *
2634
	 * The function takes a single reference argument. When calling the
2635
	 * function it will attempt to match a keyword on the head of the buffer.
2636
	 * If it is successful, it will place the keyword in the referenced
2637
	 * argument, advance the position in the buffer, and return true. If it
2638
	 * fails then it won't advance the buffer and it will return false.
2639
	 *
2640
	 * All of these parse functions are powered by scssc::match(), which behaves
2641
	 * the same way, but takes a literal regular expression. Sometimes it is
2642
	 * more convenient to use match instead of creating a new function.
2643
	 *
2644
	 * Because of the format of the functions, to parse an entire string of
2645
	 * grammatical rules, you can chain them together using &&.
2646
	 *
2647
	 * But, if some of the rules in the chain succeed before one fails, then
2648
	 * the buffer position will be left at an invalid state. In order to
2649
	 * avoid this, scssc::seek() is used to remember and set buffer positions.
2650
	 *
2651
	 * Before parsing a chain, use $s = $this->seek() to remember the current
2652
	 * position into $s. Then if a chain fails, use $this->seek($s) to
2653
	 * go back where we started.
2654
	 *
2655
	 * @return boolean
2656
	 */
2657
	protected function parseChunk() {
2658
		$s = $this->seek();
2659
2660
		// the directives
2661
		if (isset($this->buffer[$this->count]) && $this->buffer[$this->count] == "@") {
2662
			if ($this->literal("@media") && $this->mediaQueryList($mediaQueryList) && $this->literal("{")) {
2663
				$media = $this->pushSpecialBlock("media");
2664
				$media->queryList = $mediaQueryList[2];
2665
				return true;
2666
			} else {
2667
				$this->seek($s);
2668
			}
2669
2670
			if ($this->literal("@mixin") &&
2671
				$this->keyword($mixinName) &&
2672
				($this->argumentDef($args) || true) &&
2673
				$this->literal("{"))
2674
			{
2675
				$mixin = $this->pushSpecialBlock("mixin");
2676
				$mixin->name = $mixinName;
2677
				$mixin->args = $args;
2678
				return true;
2679
			} else {
2680
				$this->seek($s);
2681
			}
2682
2683
			if ($this->literal("@include") &&
2684
				$this->keyword($mixinName) &&
2685
				($this->literal("(") &&
2686
					($this->argValues($argValues) || true) &&
2687
					$this->literal(")") || true) &&
2688
				($this->end() ||
2689
					$this->literal("{") && $hasBlock = true))
2690
			{
2691
				$child = array("include",
2692
					$mixinName, isset($argValues) ? $argValues : null, null);
2693
2694
				if (!empty($hasBlock)) {
2695
					$include = $this->pushSpecialBlock("include");
2696
					$include->child = $child;
2697
				} else {
2698
					$this->append($child, $s);
2699
				}
2700
2701
				return true;
2702
			} else {
2703
				$this->seek($s);
2704
			}
2705
2706
			if ($this->literal("@import") &&
2707
				$this->valueList($importPath) &&
2708
				$this->end())
2709
			{
2710
				$this->append(array("import", $importPath), $s);
2711
				return true;
2712
			} else {
2713
				$this->seek($s);
2714
			}
2715
2716
			if ($this->literal("@extend") &&
2717
				$this->selectors($selector) &&
2718
				$this->end())
2719
			{
2720
				$this->append(array("extend", $selector), $s);
2721
				return true;
2722
			} else {
2723
				$this->seek($s);
2724
			}
2725
2726 View Code Duplication
			if ($this->literal("@function") &&
2727
				$this->keyword($fnName) &&
2728
				$this->argumentDef($args) &&
2729
				$this->literal("{"))
2730
			{
2731
				$func = $this->pushSpecialBlock("function");
2732
				$func->name = $fnName;
2733
				$func->args = $args;
2734
				return true;
2735
			} else {
2736
				$this->seek($s);
2737
			}
2738
2739
			if ($this->literal("@return") && $this->valueList($retVal) && $this->end()) {
2740
				$this->append(array("return", $retVal), $s);
2741
				return true;
2742
			} else {
2743
				$this->seek($s);
2744
			}
2745
2746
			if ($this->literal("@each") &&
2747
				$this->variable($varName) &&
2748
				$this->literal("in") &&
2749
				$this->valueList($list) &&
2750
				$this->literal("{"))
2751
			{
2752
				$each = $this->pushSpecialBlock("each");
2753
				$each->var = $varName[1];
2754
				$each->list = $list;
2755
				return true;
2756
			} else {
2757
				$this->seek($s);
2758
			}
2759
2760
			if ($this->literal("@while") &&
2761
				$this->expression($cond) &&
2762
				$this->literal("{"))
2763
			{
2764
				$while = $this->pushSpecialBlock("while");
2765
				$while->cond = $cond;
2766
				return true;
2767
			} else {
2768
				$this->seek($s);
2769
			}
2770
2771
			if ($this->literal("@for") &&
2772
				$this->variable($varName) &&
2773
				$this->literal("from") &&
2774
				$this->expression($start) &&
2775
				($this->literal("through") ||
2776
					($forUntil = true && $this->literal("to"))) &&
0 ignored issues
show
Comprehensibility introduced by
Consider adding parentheses for clarity. Current Interpretation: $forUntil = (true && $this->literal('to')), Probably Intended Meaning: ($forUntil = true) && $this->literal('to')
Loading history...
2777
				$this->expression($end) &&
2778
				$this->literal("{"))
2779
			{
2780
				$for = $this->pushSpecialBlock("for");
2781
				$for->var = $varName[1];
2782
				$for->start = $start;
2783
				$for->end = $end;
2784
				$for->until = isset($forUntil);
2785
				return true;
2786
			} else {
2787
				$this->seek($s);
2788
			}
2789
2790 View Code Duplication
			if ($this->literal("@if") && $this->valueList($cond) && $this->literal("{")) {
2791
				$if = $this->pushSpecialBlock("if");
2792
				$if->cond = $cond;
2793
				$if->cases = array();
2794
				return true;
2795
			} else {
2796
				$this->seek($s);
2797
			}
2798
2799 View Code Duplication
			if (($this->literal("@debug") || $this->literal("@warn")) &&
2800
				$this->valueList($value) &&
2801
				$this->end()) {
2802
				$this->append(array("debug", $value, $s), $s);
2803
				return true;
2804
			} else {
2805
				$this->seek($s);
2806
			}
2807
2808
			if ($this->literal("@content") && $this->end()) {
2809
				$this->append(array("mixin_content"), $s);
2810
				return true;
2811
			} else {
2812
				$this->seek($s);
2813
			}
2814
2815
			$last = $this->last();
2816
			if (!is_null($last) && $last[0] == "if") {
2817
				list(, $if) = $last;
2818
				if ($this->literal("@else")) {
2819
					if ($this->literal("{")) {
2820
						$else = $this->pushSpecialBlock("else");
2821
					} elseif ($this->literal("if") && $this->valueList($cond) && $this->literal("{")) {
2822
						$else = $this->pushSpecialBlock("elseif");
2823
						$else->cond = $cond;
2824
					}
2825
2826
					if (isset($else)) {
2827
						$else->dontAppend = true;
2828
						$if->cases[] = $else;
2829
						return true;
2830
					}
2831
				}
2832
2833
				$this->seek($s);
2834
			}
2835
2836
			if ($this->literal("@charset") &&
2837
				$this->valueList($charset) && $this->end())
2838
			{
2839
				$this->append(array("charset", $charset), $s);
2840
				return true;
2841
			} else {
2842
				$this->seek($s);
2843
			}
2844
2845
			// doesn't match built in directive, do generic one
2846
			if ($this->literal("@", false) && $this->keyword($dirName) &&
2847
				($this->openString("{", $dirValue) || true) &&
2848
				$this->literal("{"))
2849
			{
2850
				$directive = $this->pushSpecialBlock("directive");
2851
				$directive->name = $dirName;
2852
				if (isset($dirValue)) $directive->value = $dirValue;
2853
				return true;
2854
			}
2855
2856
			$this->seek($s);
2857
			return false;
2858
		}
2859
2860
		// property shortcut
2861
		// captures most properties before having to parse a selector
2862
		if ($this->keyword($name, false) &&
2863
			$this->literal(": ") &&
2864
			$this->valueList($value) &&
2865
			$this->end())
2866
		{
2867
			$name = array("string", "", array($name));
2868
			$this->append(array("assign", $name, $value), $s);
2869
			return true;
2870
		} else {
2871
			$this->seek($s);
2872
		}
2873
2874
		// variable assigns
2875
		if ($this->variable($name) &&
2876
			$this->literal(":") &&
2877
			$this->valueList($value) && $this->end())
2878
		{
2879
			// check for !default
2880
			$defaultVar = $value[0] == "list" && $this->stripDefault($value);
2881
			$this->append(array("assign", $name, $value, $defaultVar), $s);
2882
			return true;
2883
		} else {
2884
			$this->seek($s);
2885
		}
2886
2887
		// misc
2888
		if ($this->literal("-->")) {
2889
			return true;
2890
		}
2891
2892
		// opening css block
2893
		$oldComments = $this->insertComments;
2894
		$this->insertComments = false;
2895 View Code Duplication
		if ($this->selectors($selectors) && $this->literal("{")) {
2896
			$this->pushBlock($selectors);
2897
			$this->insertComments = $oldComments;
2898
			return true;
2899
		} else {
2900
			$this->seek($s);
2901
		}
2902
		$this->insertComments = $oldComments;
2903
2904
		// property assign, or nested assign
2905
		if ($this->propertyName($name) && $this->literal(":")) {
2906
			$foundSomething = false;
2907
			if ($this->valueList($value)) {
2908
				$this->append(array("assign", $name, $value), $s);
2909
				$foundSomething = true;
2910
			}
2911
2912
			if ($this->literal("{")) {
2913
				$propBlock = $this->pushSpecialBlock("nestedprop");
2914
				$propBlock->prefix = $name;
2915
				$foundSomething = true;
2916
			} elseif ($foundSomething) {
2917
				$foundSomething = $this->end();
2918
			}
2919
2920
			if ($foundSomething) {
2921
				return true;
2922
			}
2923
2924
			$this->seek($s);
2925
		} else {
2926
			$this->seek($s);
2927
		}
2928
2929
		// closing a block
2930
		if ($this->literal("}")) {
2931
			$block = $this->popBlock();
2932
			if (isset($block->type) && $block->type == "include") {
2933
				$include = $block->child;
2934
				unset($block->child);
2935
				$include[3] = $block;
2936
				$this->append($include, $s);
2937
			} elseif (empty($block->dontAppend)) {
2938
				$type = isset($block->type) ? $block->type : "block";
2939
				$this->append(array($type, $block), $s);
2940
			}
2941
			return true;
2942
		}
2943
2944
		// extra stuff
2945
		if ($this->literal(";") ||
2946
			$this->literal("<!--"))
2947
		{
2948
			return true;
2949
		}
2950
2951
		return false;
2952
	}
2953
2954
	protected function stripDefault(&$value) {
2955
		$def = end($value[2]);
2956 View Code Duplication
		if ($def[0] == "keyword" && $def[1] == "!default") {
2957
			array_pop($value[2]);
2958
			$value = $this->flattenList($value);
2959
			return true;
2960
		}
2961
2962
		if ($def[0] == "list") {
2963
			return $this->stripDefault($value[2][count($value[2]) - 1]);
2964
		}
2965
2966
		return false;
2967
	}
2968
2969
	protected function literal($what, $eatWhitespace = null) {
2970
		if (is_null($eatWhitespace)) $eatWhitespace = $this->eatWhiteDefault;
2971
2972
		// shortcut on single letter
2973 View Code Duplication
		if (!isset($what[1]) && isset($this->buffer[$this->count])) {
2974
			if ($this->buffer[$this->count] == $what) {
2975
				if (!$eatWhitespace) {
2976
					$this->count++;
2977
					return true;
2978
				}
2979
				// goes below...
2980
			} else {
2981
				return false;
2982
			}
2983
		}
2984
2985
		return $this->match($this->preg_quote($what), $m, $eatWhitespace);
2986
	}
2987
2988
	// tree builders
2989
2990
	protected function pushBlock($selectors) {
2991
		$b = new stdClass;
2992
		$b->parent = $this->env; // not sure if we need this yet
2993
2994
		$b->selectors = $selectors;
2995
		$b->children = array();
2996
2997
		$this->env = $b;
2998
		return $b;
2999
	}
3000
3001
	protected function pushSpecialBlock($type) {
3002
		$block = $this->pushBlock(null);
3003
		$block->type = $type;
3004
		return $block;
3005
	}
3006
3007
	protected function popBlock() {
3008
		if (empty($this->env->parent)) {
3009
			$this->throwParseError("unexpected }");
3010
		}
3011
3012
		$old = $this->env;
3013
		$this->env = $this->env->parent;
3014
		unset($old->parent);
3015
		return $old;
3016
	}
3017
3018
	protected function append($statement, $pos=null) {
3019
		if ($pos !== null) {
3020
			$statement[-1] = $pos;
3021
			if (!$this->rootParser) $statement[-2] = $this;
3022
		}
3023
		$this->env->children[] = $statement;
3024
	}
3025
3026
	// last child that was appended
3027
	protected function last() {
3028
		$i = count($this->env->children) - 1;
3029
		if (isset($this->env->children[$i]))
3030
			return $this->env->children[$i];
3031
	}
3032
3033
	// high level parsers (they return parts of ast)
3034
3035
	protected function mediaQueryList(&$out) {
3036
		return $this->genericList($out, "mediaQuery", ",", false);
3037
	}
3038
3039
	protected function mediaQuery(&$out) {
3040
		$s = $this->seek();
0 ignored issues
show
Unused Code introduced by
$s 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...
3041
3042
		$expressions = null;
3043
		$parts = array();
3044
3045
		if (($this->literal("only") && ($only = true) || $this->literal("not") && ($not = true) || true) && $this->mixedKeyword($mediaType)) {
3046
			$prop = array("mediaType");
3047
			if (isset($only)) $prop[] = array("keyword", "only");
3048
			if (isset($not)) $prop[] = array("keyword", "not");
3049
			$media = array("list", "", array());
3050
			foreach ((array)$mediaType as $type) {
3051
				if (is_array($type)) {
3052
					$media[2][] = $type;
3053
				} else {
3054
					$media[2][] = array("keyword", $type);
3055
				}
3056
			}
3057
			$prop[] = $media;
3058
			$parts[] = $prop;
3059
		}
3060
3061 View Code Duplication
		if (empty($parts) || $this->literal("and")) {
3062
			$this->genericList($expressions, "mediaExpression", "and", false);
3063
			if (is_array($expressions)) $parts = array_merge($parts, $expressions[2]);
3064
		}
3065
3066
		$out = $parts;
3067
		return true;
3068
	}
3069
3070
	protected function mediaExpression(&$out) {
3071
		$s = $this->seek();
3072
		$value = null;
3073
		if ($this->literal("(") &&
3074
			$this->expression($feature) &&
3075
			($this->literal(":") && $this->expression($value) || true) &&
3076
			$this->literal(")"))
3077
		{
3078
			$out = array("mediaExp", $feature);
3079
			if ($value) $out[] = $value;
3080
			return true;
3081
		}
3082
3083
		$this->seek($s);
3084
		return false;
3085
	}
3086
3087
	protected function argValues(&$out) {
3088
		if ($this->genericList($list, "argValue", ",", false)) {
3089
			$out = $list[2];
3090
			return true;
3091
		}
3092
		return false;
3093
	}
3094
3095
	protected function argValue(&$out) {
3096
		$s = $this->seek();
3097
3098
		$keyword = null;
3099
		if (!$this->variable($keyword) || !$this->literal(":")) {
3100
			$this->seek($s);
3101
			$keyword = null;
3102
		}
3103
3104
		if ($this->genericList($value, "expression")) {
3105
			$out = array($keyword, $value, false);
3106
			$s = $this->seek();
3107
			if ($this->literal("...")) {
3108
				$out[2] = true;
3109
			} else {
3110
				$this->seek($s);
3111
			}
3112
			return true;
3113
		}
3114
3115
		return false;
3116
	}
3117
3118
3119
	protected function valueList(&$out) {
3120
		return $this->genericList($out, "spaceList", ",");
3121
	}
3122
3123
	protected function spaceList(&$out) {
3124
		return $this->genericList($out, "expression");
3125
	}
3126
3127 View Code Duplication
	protected function genericList(&$out, $parseItem, $delim="", $flatten=true) {
3128
		$s = $this->seek();
3129
		$items = array();
3130
		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...
3131
			$items[] = $value;
3132
			if ($delim) {
3133
				if (!$this->literal($delim)) break;
3134
			}
3135
		}
3136
3137
		if (count($items) == 0) {
3138
			$this->seek($s);
3139
			return false;
3140
		}
3141
3142
		if ($flatten && count($items) == 1) {
3143
			$out = $items[0];
3144
		} else {
3145
			$out = array("list", $delim, $items);
3146
		}
3147
3148
		return true;
3149
	}
3150
3151
	protected function expression(&$out) {
3152
		$s = $this->seek();
3153
3154
		if ($this->literal("(")) {
3155
			if ($this->literal(")")) {
3156
				$out = array("list", "", array());
3157
				return true;
3158
			}
3159
3160
			if ($this->valueList($out) && $this->literal(')') && $out[0] == "list") {
3161
				return true;
3162
			}
3163
3164
			$this->seek($s);
3165
		}
3166
3167
		if ($this->value($lhs)) {
3168
			$out = $this->expHelper($lhs, 0);
3169
			return true;
3170
		}
3171
3172
		return false;
3173
	}
3174
3175
	protected function expHelper($lhs, $minP) {
3176
		$opstr = self::$operatorStr;
3177
3178
		$ss = $this->seek();
3179
		$whiteBefore = isset($this->buffer[$this->count - 1]) &&
3180
			ctype_space($this->buffer[$this->count - 1]);
3181
		while ($this->match($opstr, $m) && self::$precedence[$m[1]] >= $minP) {
3182
			$whiteAfter = isset($this->buffer[$this->count - 1]) &&
3183
				ctype_space($this->buffer[$this->count - 1]);
3184
3185
			$op = $m[1];
3186
3187
			// don't turn negative numbers into expressions
3188
			if ($op == "-" && $whiteBefore) {
3189
				if (!$whiteAfter) break;
3190
			}
3191
3192
			if (!$this->value($rhs)) break;
3193
3194
			// peek and see if rhs belongs to next operator
3195
			if ($this->peek($opstr, $next) && self::$precedence[$next[1]] > self::$precedence[$op]) {
3196
				$rhs = $this->expHelper($rhs, self::$precedence[$next[1]]);
3197
			}
3198
3199
			$lhs = array("exp", $op, $lhs, $rhs, $this->inParens, $whiteBefore, $whiteAfter);
3200
			$ss = $this->seek();
3201
			$whiteBefore = isset($this->buffer[$this->count - 1]) &&
3202
				ctype_space($this->buffer[$this->count - 1]);
3203
		}
3204
3205
		$this->seek($ss);
3206
		return $lhs;
3207
	}
3208
3209
	protected function value(&$out) {
3210
		$s = $this->seek();
3211
3212 View Code Duplication
		if ($this->literal("not", false) && $this->whitespace() && $this->value($inner)) {
3213
			$out = array("unary", "not", $inner, $this->inParens);
3214
			return true;
3215
		} else {
3216
			$this->seek($s);
3217
		}
3218
3219 View Code Duplication
		if ($this->literal("+") && $this->value($inner)) {
3220
			$out = array("unary", "+", $inner, $this->inParens);
3221
			return true;
3222
		} else {
3223
			$this->seek($s);
3224
		}
3225
3226
		// negation
3227
		if ($this->literal("-", false) &&
3228
			($this->variable($inner) ||
3229
			$this->unit($inner) ||
3230
			$this->parenValue($inner)))
3231
		{
3232
			$out = array("unary", "-", $inner, $this->inParens);
3233
			return true;
3234
		} else {
3235
			$this->seek($s);
3236
		}
3237
3238
		if ($this->parenValue($out)) return true;
3239
		if ($this->interpolation($out)) return true;
3240
		if ($this->variable($out)) return true;
3241
		if ($this->color($out)) return true;
3242
		if ($this->unit($out)) return true;
3243
		if ($this->string($out)) return true;
3244
		if ($this->func($out)) return true;
3245
		if ($this->progid($out)) return true;
3246
3247
		if ($this->keyword($keyword)) {
3248
			if ($keyword == "null") {
3249
				$out = array("null");
3250
			} else {
3251
				$out = array("keyword", $keyword);
3252
			}
3253
			return true;
3254
		}
3255
3256
		return false;
3257
	}
3258
3259
	// value wrappen in parentheses
3260
	protected function parenValue(&$out) {
3261
		$s = $this->seek();
3262
3263
		$inParens = $this->inParens;
3264 View Code Duplication
		if ($this->literal("(") &&
3265
			($this->inParens = true) && $this->expression($exp) &&
3266
			$this->literal(")"))
3267
		{
3268
			$out = $exp;
3269
			$this->inParens = $inParens;
3270
			return true;
3271
		} else {
3272
			$this->inParens = $inParens;
3273
			$this->seek($s);
3274
		}
3275
3276
		return false;
3277
	}
3278
3279
	protected function progid(&$out) {
3280
		$s = $this->seek();
3281
		if ($this->literal("progid:", false) &&
3282
			$this->openString("(", $fn) &&
3283
			$this->literal("("))
3284
		{
3285
			$this->openString(")", $args, "(");
3286
			if ($this->literal(")")) {
3287
				$out = array("string", "", array(
3288
					"progid:", $fn, "(", $args, ")"
3289
				));
3290
				return true;
3291
			}
3292
		}
3293
3294
		$this->seek($s);
3295
		return false;
3296
	}
3297
3298
	protected function func(&$func) {
3299
		$s = $this->seek();
3300
3301
		if ($this->keyword($name, false) &&
3302
			$this->literal("("))
3303
		{
3304
			if ($name == "alpha" && $this->argumentList($args)) {
3305
				$func = array("function", $name, array("string", "", $args));
3306
				return true;
3307
			}
3308
3309
			if ($name != "expression" && !preg_match("/^(-[a-z]+-)?calc$/", $name)) {
3310
				$ss = $this->seek();
3311
				if ($this->argValues($args) && $this->literal(")")) {
3312
					$func = array("fncall", $name, $args);
3313
					return true;
3314
				}
3315
				$this->seek($ss);
3316
			}
3317
3318
			if (($this->openString(")", $str, "(") || true ) &&
3319
				$this->literal(")"))
3320
			{
3321
				$args = array();
3322
				if (!empty($str)) {
3323
					$args[] = array(null, array("string", "", array($str)));
3324
				}
3325
3326
				$func = array("fncall", $name, $args);
3327
				return true;
3328
			}
3329
		}
3330
3331
		$this->seek($s);
3332
		return false;
3333
	}
3334
3335
	protected function argumentList(&$out) {
3336
		$s = $this->seek();
3337
		$this->literal("(");
3338
3339
		$args = array();
3340
		while ($this->keyword($var)) {
3341
			$ss = $this->seek();
0 ignored issues
show
Unused Code introduced by
$ss 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...
3342
3343
			if ($this->literal("=") && $this->expression($exp)) {
3344
				$args[] = array("string", "", array($var."="));
3345
				$arg = $exp;
3346
			} else {
3347
				break;
3348
			}
3349
3350
			$args[] = $arg;
3351
3352
			if (!$this->literal(",")) break;
3353
3354
			$args[] = array("string", "", array(", "));
3355
		}
3356
3357
		if (!$this->literal(")") || !count($args)) {
3358
			$this->seek($s);
3359
			return false;
3360
		}
3361
3362
		$out = $args;
3363
		return true;
3364
	}
3365
3366
	protected function argumentDef(&$out) {
3367
		$s = $this->seek();
3368
		$this->literal("(");
3369
3370
		$args = array();
3371
		while ($this->variable($var)) {
3372
			$arg = array($var[1], null, false);
3373
3374
			$ss = $this->seek();
3375
			if ($this->literal(":") && $this->genericList($defaultVal, "expression")) {
3376
				$arg[1] = $defaultVal;
3377
			} else {
3378
				$this->seek($ss);
3379
			}
3380
3381
			$ss = $this->seek();
3382
			if ($this->literal("...")) {
3383
				$sss = $this->seek();
3384
				if (!$this->literal(")")) {
3385
					$this->throwParseError("... has to be after the final argument");
3386
				}
3387
				$arg[2] = true;
3388
				$this->seek($sss);
3389
			} else {
3390
				$this->seek($ss);
3391
			}
3392
3393
			$args[] = $arg;
3394
			if (!$this->literal(",")) break;
3395
		}
3396
3397
		if (!$this->literal(")")) {
3398
			$this->seek($s);
3399
			return false;
3400
		}
3401
3402
		$out = $args;
3403
		return true;
3404
	}
3405
3406
	protected function color(&$out) {
3407
		$color = array('color');
3408
3409
		if ($this->match('(#([0-9a-f]{6})|#([0-9a-f]{3}))', $m)) {
3410
			if (isset($m[3])) {
3411
				$num = $m[3];
3412
				$width = 16;
3413
			} else {
3414
				$num = $m[2];
3415
				$width = 256;
3416
			}
3417
3418
			$num = hexdec($num);
3419
			foreach (array(3,2,1) as $i) {
3420
				$t = $num % $width;
3421
				$num /= $width;
3422
3423
				$color[$i] = $t * (256/$width) + $t * floor(16/$width);
3424
			}
3425
3426
			$out = $color;
3427
			return true;
3428
		}
3429
3430
		return false;
3431
	}
3432
3433
	protected function unit(&$unit) {
3434 View Code Duplication
		if ($this->match('([0-9]*(\.)?[0-9]+)([%a-zA-Z]+)?', $m)) {
3435
			$unit = array("number", $m[1], empty($m[3]) ? "" : $m[3]);
3436
			return true;
3437
		}
3438
		return false;
3439
	}
3440
3441
	protected function string(&$out) {
3442
		$s = $this->seek();
3443 View Code Duplication
		if ($this->literal('"', false)) {
3444
			$delim = '"';
3445
		} elseif ($this->literal("'", false)) {
3446
			$delim = "'";
3447
		} else {
3448
			return false;
3449
		}
3450
3451
		$content = array();
3452
		$oldWhite = $this->eatWhiteDefault;
3453
		$this->eatWhiteDefault = false;
3454
3455 View Code Duplication
		while ($this->matchString($m, $delim)) {
3456
			$content[] = $m[1];
3457
			if ($m[2] == "#{") {
3458
				$this->count -= strlen($m[2]);
3459
				if ($this->interpolation($inter, false)) {
3460
					$content[] = $inter;
3461
				} else {
3462
					$this->count += strlen($m[2]);
3463
					$content[] = "#{"; // ignore it
3464
				}
3465
			} elseif ($m[2] == '\\') {
3466
				$content[] = $m[2];
3467
				if ($this->literal($delim, false)) {
3468
					$content[] = $delim;
3469
				}
3470
			} else {
3471
				$this->count -= strlen($delim);
3472
				break; // delim
3473
			}
3474
		}
3475
3476
		$this->eatWhiteDefault = $oldWhite;
3477
3478 View Code Duplication
		if ($this->literal($delim)) {
3479
			$out = array("string", $delim, $content);
3480
			return true;
3481
		}
3482
3483
		$this->seek($s);
3484
		return false;
3485
	}
3486
3487
	protected function mixedKeyword(&$out) {
3488
		$s = $this->seek();
0 ignored issues
show
Unused Code introduced by
$s 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...
3489
3490
		$parts = array();
3491
3492
		$oldWhite = $this->eatWhiteDefault;
3493
		$this->eatWhiteDefault = false;
3494
3495
		while (true) {
3496
			if ($this->keyword($key)) {
3497
				$parts[] = $key;
3498
				continue;
3499
			}
3500
3501
			if ($this->interpolation($inter)) {
3502
				$parts[] = $inter;
3503
				continue;
3504
			}
3505
3506
			break;
3507
		}
3508
3509
		$this->eatWhiteDefault = $oldWhite;
3510
3511
		if (count($parts) == 0) return false;
3512
3513
		if ($this->eatWhiteDefault) {
3514
			$this->whitespace();
3515
		}
3516
3517
		$out = $parts;
3518
		return true;
3519
	}
3520
3521
	// an unbounded string stopped by $end
3522
	protected function openString($end, &$out, $nestingOpen=null) {
3523
		$oldWhite = $this->eatWhiteDefault;
3524
		$this->eatWhiteDefault = false;
3525
3526
		$stop = array("'", '"', "#{", $end);
3527
		$stop = array_map(array($this, "preg_quote"), $stop);
3528
		$stop[] = self::$commentMulti;
3529
3530
		$patt = '(.*?)('.implode("|", $stop).')';
3531
3532
		$nestingLevel = 0;
3533
3534
		$content = array();
3535
		while ($this->match($patt, $m, false)) {
3536
			if (isset($m[1]) && $m[1] !== '') {
3537
				$content[] = $m[1];
3538
				if ($nestingOpen) {
3539
					$nestingLevel += substr_count($m[1], $nestingOpen);
3540
				}
3541
			}
3542
3543
			$tok = $m[2];
3544
3545
			$this->count-= strlen($tok);
3546
			if ($tok == $end) {
3547
				if ($nestingLevel == 0) {
3548
					break;
3549
				} else {
3550
					$nestingLevel--;
3551
				}
3552
			}
3553
3554 View Code Duplication
			if (($tok == "'" || $tok == '"') && $this->string($str)) {
3555
				$content[] = $str;
3556
				continue;
3557
			}
3558
3559
			if ($tok == "#{" && $this->interpolation($inter)) {
3560
				$content[] = $inter;
3561
				continue;
3562
			}
3563
3564
			$content[] = $tok;
3565
			$this->count+= strlen($tok);
3566
		}
3567
3568
		$this->eatWhiteDefault = $oldWhite;
3569
3570
		if (count($content) == 0) return false;
3571
3572
		// trim the end
3573 View Code Duplication
		if (is_string(end($content))) {
3574
			$content[count($content) - 1] = rtrim(end($content));
3575
		}
3576
3577
		$out = array("string", "", $content);
3578
		return true;
3579
	}
3580
3581
	// $lookWhite: save information about whitespace before and after
3582
	protected function interpolation(&$out, $lookWhite=true) {
3583
		$oldWhite = $this->eatWhiteDefault;
3584
		$this->eatWhiteDefault = true;
3585
3586
		$s = $this->seek();
3587
		if ($this->literal("#{") && $this->valueList($value) && $this->literal("}", false)) {
3588
3589
			// TODO: don't error if out of bounds
3590
3591
			if ($lookWhite) {
3592
				$left = preg_match('/\s/', $this->buffer[$s - 1]) ? " " : "";
3593
				$right = preg_match('/\s/', $this->buffer[$this->count]) ? " ": "";
3594
			} else {
3595
				$left = $right = false;
3596
			}
3597
3598
			$out = array("interpolate", $value, $left, $right);
3599
			$this->eatWhiteDefault = $oldWhite;
3600
			if ($this->eatWhiteDefault) $this->whitespace();
3601
			return true;
3602
		}
3603
3604
		$this->seek($s);
3605
		$this->eatWhiteDefault = $oldWhite;
3606
		return false;
3607
	}
3608
3609
	// low level parsers
3610
3611
	// returns an array of parts or a string
3612
	protected function propertyName(&$out) {
3613
		$s = $this->seek();
0 ignored issues
show
Unused Code introduced by
$s 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...
3614
		$parts = array();
3615
3616
		$oldWhite = $this->eatWhiteDefault;
3617
		$this->eatWhiteDefault = false;
3618
3619
		while (true) {
3620
			if ($this->interpolation($inter)) {
3621
				$parts[] = $inter;
3622
			} elseif ($this->keyword($text)) {
3623
				$parts[] = $text;
3624
			} elseif (count($parts) == 0 && $this->match('[:.#]', $m, false)) {
3625
				// css hacks
3626
				$parts[] = $m[0];
3627
			} else {
3628
				break;
3629
			}
3630
		}
3631
3632
		$this->eatWhiteDefault = $oldWhite;
3633
		if (count($parts) == 0) return false;
3634
3635
		// match comment hack
3636
		if (preg_match(self::$whitePattern,
3637
			$this->buffer, $m, null, $this->count))
3638
		{
3639
			if (!empty($m[0])) {
3640
				$parts[] = $m[0];
3641
				$this->count += strlen($m[0]);
3642
			}
3643
		}
3644
3645
		$this->whitespace(); // get any extra whitespace
3646
3647
		$out = array("string", "", $parts);
3648
		return true;
3649
	}
3650
3651
	// comma separated list of selectors
3652 View Code Duplication
	protected function selectors(&$out) {
3653
		$s = $this->seek();
3654
		$selectors = array();
3655
		while ($this->selector($sel)) {
3656
			$selectors[] = $sel;
3657
			if (!$this->literal(",")) break;
3658
			while ($this->literal(",")); // ignore extra
3659
		}
3660
3661
		if (count($selectors) == 0) {
3662
			$this->seek($s);
3663
			return false;
3664
		}
3665
3666
		$out = $selectors;
3667
		return true;
3668
	}
3669
3670
	// whitespace separated list of selectorSingle
3671
	protected function selector(&$out) {
3672
		$selector = array();
3673
3674
		while (true) {
3675
			if ($this->match('[>+~]+', $m)) {
3676
				$selector[] = array($m[0]);
3677
			} elseif ($this->selectorSingle($part)) {
3678
				$selector[] = $part;
3679
				$this->whitespace();
3680
			} elseif ($this->match('\/[^\/]+\/', $m)) {
3681
				$selector[] = array($m[0]);
3682
			} else {
3683
				break;
3684
			}
3685
3686
		}
3687
3688
		if (count($selector) == 0) {
3689
			return false;
3690
		}
3691
3692
		$out = $selector;
3693
		return true;
3694
	}
3695
3696
	// the parts that make up
3697
	// div[yes=no]#something.hello.world:nth-child(-2n+1)%placeholder
3698
	protected function selectorSingle(&$out) {
3699
		$oldWhite = $this->eatWhiteDefault;
3700
		$this->eatWhiteDefault = false;
3701
3702
		$parts = array();
3703
3704
		if ($this->literal("*", false)) {
3705
			$parts[] = "*";
3706
		}
3707
3708
		while (true) {
3709
			// see if we can stop early
3710
			if ($this->match("\s*[{,]", $m)) {
3711
				$this->count--;
3712
				break;
3713
			}
3714
3715
			$s = $this->seek();
3716
			// self
3717
			if ($this->literal("&", false)) {
3718
				$parts[] = scssc::$selfSelector;
3719
				continue;
3720
			}
3721
3722
			if ($this->literal(".", false)) {
3723
				$parts[] = ".";
3724
				continue;
3725
			}
3726
3727
			if ($this->literal("|", false)) {
3728
				$parts[] = "|";
3729
				continue;
3730
			}
3731
3732
			// for keyframes
3733
			if ($this->unit($unit)) {
3734
				$parts[] = $unit;
3735
				continue;
3736
			}
3737
3738
			if ($this->keyword($name)) {
3739
				$parts[] = $name;
3740
				continue;
3741
			}
3742
3743
			if ($this->interpolation($inter)) {
3744
				$parts[] = $inter;
3745
				continue;
3746
			}
3747
3748
			if ($this->literal('%', false) && $this->placeholder($placeholder)) {
3749
				$parts[] = '%';
3750
				$parts[] = $placeholder;
3751
				continue;
3752
			}
3753
3754
			if ($this->literal("#", false)) {
3755
				$parts[] = "#";
3756
				continue;
3757
			}
3758
3759
			// a pseudo selector
3760
			if ($this->match("::?", $m) && $this->mixedKeyword($nameParts)) {
3761
				$parts[] = $m[0];
3762
				foreach ($nameParts as $sub) {
3763
					$parts[] = $sub;
3764
				}
3765
3766
				$ss = $this->seek();
3767
				if ($this->literal("(") &&
3768
					($this->openString(")", $str, "(") || true ) &&
3769
					$this->literal(")"))
3770
				{
3771
					$parts[] = "(";
3772
					if (!empty($str)) $parts[] = $str;
3773
					$parts[] = ")";
3774
				} else {
3775
					$this->seek($ss);
3776
				}
3777
3778
				continue;
3779
			} else {
3780
				$this->seek($s);
3781
			}
3782
3783
			// attribute selector
3784
			// TODO: replace with open string?
3785
			if ($this->literal("[", false)) {
3786
				$attrParts = array("[");
3787
				// keyword, string, operator
3788
				while (true) {
3789
					if ($this->literal("]", false)) {
3790
						$this->count--;
3791
						break; // get out early
3792
					}
3793
3794
					if ($this->match('\s+', $m)) {
3795
						$attrParts[] = " ";
3796
						continue;
3797
					}
3798
					if ($this->string($str)) {
3799
						$attrParts[] = $str;
3800
						continue;
3801
					}
3802
3803
					if ($this->keyword($word)) {
3804
						$attrParts[] = $word;
3805
						continue;
3806
					}
3807
3808
					if ($this->interpolation($inter, false)) {
3809
						$attrParts[] = $inter;
3810
						continue;
3811
					}
3812
3813
					// operator, handles attr namespace too
3814
					if ($this->match('[|-~\$\*\^=]+', $m)) {
3815
						$attrParts[] = $m[0];
3816
						continue;
3817
					}
3818
3819
					break;
3820
				}
3821
3822
				if ($this->literal("]", false)) {
3823
					$attrParts[] = "]";
3824
					foreach ($attrParts as $part) {
3825
						$parts[] = $part;
3826
					}
3827
					continue;
3828
				}
3829
				$this->seek($s);
3830
				// should just break here?
3831
			}
3832
3833
			break;
3834
		}
3835
3836
		$this->eatWhiteDefault = $oldWhite;
3837
3838
		if (count($parts) == 0) return false;
3839
3840
		$out = $parts;
3841
		return true;
3842
	}
3843
3844
	protected function variable(&$out) {
3845
		$s = $this->seek();
3846
		if ($this->literal("$", false) && $this->keyword($name)) {
3847
			$out = array("var", $name);
3848
			return true;
3849
		}
3850
		$this->seek($s);
3851
		return false;
3852
	}
3853
3854
	protected function keyword(&$word, $eatWhitespace = null) {
3855
		if ($this->match('([\w_\-\*!"\'\\\\][\w\-_"\'\\\\]*)',
3856
			$m, $eatWhitespace))
3857
		{
3858
			$word = $m[1];
3859
			return true;
3860
		}
3861
		return false;
3862
	}
3863
3864 View Code Duplication
	protected function placeholder(&$placeholder) {
3865
		if ($this->match('([\w\-_]+)', $m)) {
3866
			$placeholder = $m[1];
3867
			return true;
3868
		}
3869
		return false;
3870
	}
3871
3872
	// consume an end of statement delimiter
3873 View Code Duplication
	protected function end() {
3874
		if ($this->literal(';')) {
3875
			return true;
3876
		} elseif ($this->count == strlen($this->buffer) || $this->buffer[$this->count] == '}') {
3877
			// if there is end of file or a closing block next then we don't need a ;
3878
			return true;
3879
		}
3880
		return false;
3881
	}
3882
3883
	// advance counter to next occurrence of $what
3884
	// $until - don't include $what in advance
3885
	// $allowNewline, if string, will be used as valid char set
3886 View Code Duplication
	protected function to($what, &$out, $until = false, $allowNewline = false) {
3887
		if (is_string($allowNewline)) {
3888
			$validChars = $allowNewline;
3889
		} else {
3890
			$validChars = $allowNewline ? "." : "[^\n]";
3891
		}
3892
		if (!$this->match('('.$validChars.'*?)'.$this->preg_quote($what), $m, !$until)) return false;
3893
		if ($until) $this->count -= strlen($what); // give back $what
3894
		$out = $m[1];
3895
		return true;
3896
	}
3897
3898
	public function throwParseError($msg = "parse error", $count = null) {
3899
		$count = is_null($count) ? $this->count : $count;
3900
3901
		$line = $this->getLineNo($count);
3902
3903 View Code Duplication
		if (!empty($this->sourceName)) {
3904
			$loc = "$this->sourceName on line $line";
3905
		} else {
3906
			$loc = "line: $line";
3907
		}
3908
3909 View Code Duplication
		if ($this->peek("(.*?)(\n|$)", $m, $count)) {
3910
			throw new Exception("$msg: failed at `$m[1]` $loc");
3911
		} else {
3912
			throw new Exception("$msg: $loc");
3913
		}
3914
	}
3915
3916
	public function getLineNo($pos) {
3917
		return 1 + substr_count(substr($this->buffer, 0, $pos), "\n");
3918
	}
3919
3920
	/**
3921
	 * Match string looking for either ending delim, escape, or string interpolation
3922
	 *
3923
	 * {@internal This is a workaround for preg_match's 250K string match limit. }}
3924
	 *
3925
	 * @param array  $m     Matches (passed by reference)
3926
	 * @param string $delim Delimeter
3927
	 *
3928
	 * @return boolean True if match; false otherwise
3929
	 */
3930
	protected function matchString(&$m, $delim) {
3931
		$token = null;
3932
3933
		$end = strpos($this->buffer, "\n", $this->count);
3934
		if ($end === false) {
3935
			$end = strlen($this->buffer);
3936
		}
3937
3938
		// look for either ending delim, escape, or string interpolation
3939
		foreach (array('#{', '\\', $delim) as $lookahead) {
3940
			$pos = strpos($this->buffer, $lookahead, $this->count);
3941
			if ($pos !== false && $pos < $end) {
3942
				$end = $pos;
3943
				$token = $lookahead;
3944
			}
3945
		}
3946
3947
		if (!isset($token)) {
3948
			return false;
3949
		}
3950
3951
		$match = substr($this->buffer, $this->count, $end - $this->count);
3952
		$m = array(
3953
			$match . $token,
3954
			$match,
3955
			$token
3956
		);
3957
		$this->count = $end + strlen($token);
3958
3959
		return true;
3960
	}
3961
3962
	// try to match something on head of buffer
3963
	protected function match($regex, &$out, $eatWhitespace = null) {
3964
		if (is_null($eatWhitespace)) $eatWhitespace = $this->eatWhiteDefault;
3965
3966
		$r = '/'.$regex.'/Ais';
3967 View Code Duplication
		if (preg_match($r, $this->buffer, $out, null, $this->count)) {
3968
			$this->count += strlen($out[0]);
3969
			if ($eatWhitespace) $this->whitespace();
3970
			return true;
3971
		}
3972
		return false;
3973
	}
3974
3975
	// match some whitespace
3976
	protected function whitespace() {
3977
		$gotWhite = false;
3978 View Code Duplication
		while (preg_match(self::$whitePattern, $this->buffer, $m, null, $this->count)) {
3979
			if ($this->insertComments) {
3980
				if (isset($m[1]) && empty($this->commentsSeen[$this->count])) {
3981
					$this->append(array("comment", $m[1]));
3982
					$this->commentsSeen[$this->count] = true;
0 ignored issues
show
Bug introduced by
The property commentsSeen 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...
3983
				}
3984
			}
3985
			$this->count += strlen($m[0]);
3986
			$gotWhite = true;
3987
		}
3988
		return $gotWhite;
3989
	}
3990
3991 View Code Duplication
	protected function peek($regex, &$out, $from=null) {
3992
		if (is_null($from)) $from = $this->count;
3993
3994
		$r = '/'.$regex.'/Ais';
3995
		$result = preg_match($r, $this->buffer, $out, null, $from);
3996
3997
		return $result;
3998
	}
3999
4000
	protected function seek($where = null) {
4001
		if ($where === null) return $this->count;
4002
		else $this->count = $where;
4003
		return true;
4004
	}
4005
4006
	static function preg_quote($what) {
4007
		return preg_quote($what, '/');
4008
	}
4009
4010
	protected function show() {
4011
		if ($this->peek("(.*?)(\n|$)", $m, $this->count)) {
4012
			return $m[1];
4013
		}
4014
		return "";
4015
	}
4016
4017
	// turn list of length 1 into value type
4018
	protected function flattenList($value) {
4019 View Code Duplication
		if ($value[0] == "list" && count($value[2]) == 1) {
4020
			return $this->flattenList($value[2][0]);
4021
		}
4022
		return $value;
4023
	}
4024
}
4025
4026
/**
4027
 * SCSS base formatter
4028
 *
4029
 * @author Leaf Corcoran <[email protected]>
4030
 */
4031
class scss_formatter {
4032
	public $indentChar = "  ";
4033
4034
	public $break = "\n";
4035
	public $open = " {";
4036
	public $close = "}";
4037
	public $tagSeparator = ", ";
4038
	public $assignSeparator = ": ";
4039
4040
	public function __construct() {
4041
		$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...
4042
	}
4043
4044
	public function indentStr($n = 0) {
4045
		return str_repeat($this->indentChar, max($this->indentLevel + $n, 0));
4046
	}
4047
4048
	public function property($name, $value) {
4049
		return $name . $this->assignSeparator . $value . ";";
4050
	}
4051
4052
	protected function block($block) {
4053
		if (empty($block->lines) && empty($block->children)) return;
4054
4055
		$inner = $pre = $this->indentStr();
4056
4057 View Code Duplication
		if (!empty($block->selectors)) {
4058
			echo $pre .
4059
				implode($this->tagSeparator, $block->selectors) .
4060
				$this->open . $this->break;
4061
			$this->indentLevel++;
4062
			$inner = $this->indentStr();
4063
		}
4064
4065 View Code Duplication
		if (!empty($block->lines)) {
4066
			$glue = $this->break.$inner;
4067
			echo $inner . implode($glue, $block->lines);
4068
			if (!empty($block->children)) {
4069
				echo $this->break;
4070
			}
4071
		}
4072
4073
		foreach ($block->children as $child) {
4074
			$this->block($child);
4075
		}
4076
4077
		if (!empty($block->selectors)) {
4078
			$this->indentLevel--;
4079
			if (empty($block->children)) echo $this->break;
4080
			echo $pre . $this->close . $this->break;
4081
		}
4082
	}
4083
4084
	public function format($block) {
4085
		ob_start();
4086
		$this->block($block);
4087
		$out = ob_get_clean();
4088
4089
		return $out;
4090
	}
4091
}
4092
4093
/**
4094
 * SCSS nested formatter
4095
 *
4096
 * @author Leaf Corcoran <[email protected]>
4097
 */
4098
class scss_formatter_nested extends scss_formatter {
4099
	public $close = " }";
4100
4101
	// adjust the depths of all children, depth first
4102
	public function adjustAllChildren($block) {
4103
		// flatten empty nested blocks
4104
		$children = array();
4105
		foreach ($block->children as $i => $child) {
4106
			if (empty($child->lines) && empty($child->children)) {
4107
				if (isset($block->children[$i + 1])) {
4108
					$block->children[$i + 1]->depth = $child->depth;
4109
				}
4110
				continue;
4111
			}
4112
			$children[] = $child;
4113
		}
4114
4115
		$count = count($children);
4116
		for ($i = 0; $i < $count; $i++) {
4117
			$depth = $children[$i]->depth;
4118
			$j = $i + 1;
4119
			if (isset($children[$j]) && $depth < $children[$j]->depth) {
4120
				$childDepth = $children[$j]->depth;
4121
				for (; $j < $count; $j++) {
4122
					if ($depth < $children[$j]->depth && $childDepth >= $children[$j]->depth) {
4123
						$children[$j]->depth = $depth + 1;
4124
					}
4125
				}
4126
			}
4127
		}
4128
4129
		$block->children = $children;
4130
4131
		// make relative to parent
4132
		foreach ($block->children as $child) {
4133
			$this->adjustAllChildren($child);
4134
			$child->depth = $child->depth - $block->depth;
4135
		}
4136
	}
4137
4138
	protected function block($block) {
4139
		if ($block->type == "root") {
4140
			$this->adjustAllChildren($block);
4141
		}
4142
4143
		$inner = $pre = $this->indentStr($block->depth - 1);
4144 View Code Duplication
		if (!empty($block->selectors)) {
4145
			echo $pre .
4146
				implode($this->tagSeparator, $block->selectors) .
4147
				$this->open . $this->break;
4148
			$this->indentLevel++;
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...
4149
			$inner = $this->indentStr($block->depth - 1);
4150
		}
4151
4152 View Code Duplication
		if (!empty($block->lines)) {
4153
			$glue = $this->break.$inner;
4154
			echo $inner . implode($glue, $block->lines);
4155
			if (!empty($block->children)) echo $this->break;
4156
		}
4157
4158
		foreach ($block->children as $i => $child) {
4159
			// echo "*** block: ".$block->depth." child: ".$child->depth."\n";
4160
			$this->block($child);
4161
			if ($i < count($block->children) - 1) {
4162
				echo $this->break;
4163
4164
				if (isset($block->children[$i + 1])) {
4165
					$next = $block->children[$i + 1];
4166
					if ($next->depth == max($block->depth, 1) && $child->depth >= $next->depth) {
4167
						echo $this->break;
4168
					}
4169
				}
4170
			}
4171
		}
4172
4173
		if (!empty($block->selectors)) {
4174
			$this->indentLevel--;
4175
			echo $this->close;
4176
		}
4177
4178
		if ($block->type == "root") {
4179
			echo $this->break;
4180
		}
4181
	}
4182
}
4183
4184
/**
4185
 * SCSS compressed formatter
4186
 *
4187
 * @author Leaf Corcoran <[email protected]>
4188
 */
4189
class scss_formatter_compressed extends scss_formatter {
4190
	public $open = "{";
4191
	public $tagSeparator = ",";
4192
	public $assignSeparator = ":";
4193
	public $break = "";
4194
4195
	public function indentStr($n = 0) {
4196
		return "";
4197
	}
4198
}
4199
4200
/**
4201
 * SCSS server
4202
 *
4203
 * @author Leaf Corcoran <[email protected]>
4204
 */
4205
class scss_server {
4206
	/**
4207
	 * Join path components
4208
	 *
4209
	 * @param string $left  Path component, left of the directory separator
4210
	 * @param string $right Path component, right of the directory separator
4211
	 *
4212
	 * @return string
4213
	 */
4214
	protected function join($left, $right) {
4215
		return rtrim($left, '/\\') . DIRECTORY_SEPARATOR . ltrim($right, '/\\');
4216
	}
4217
4218
	/**
4219
	 * Get name of requested .scss file
4220
	 *
4221
	 * @return string|null
4222
	 */
4223
	protected function inputName() {
4224
		switch (true) {
4225
			case isset($_GET['p']):
4226
				return $_GET['p'];
4227
			case isset($_SERVER['PATH_INFO']):
4228
				return $_SERVER['PATH_INFO'];
4229
			case isset($_SERVER['DOCUMENT_URI']):
4230
				return substr($_SERVER['DOCUMENT_URI'], strlen($_SERVER['SCRIPT_NAME']));
4231
		}
4232
	}
4233
4234
	/**
4235
	 * Get path to requested .scss file
4236
	 *
4237
	 * @return string
4238
	 */
4239
	protected function findInput() {
4240
		if (($input = $this->inputName())
4241
			&& strpos($input, '..') === false
4242
			&& substr($input, -5) === '.scss'
4243
		) {
4244
			$name = $this->join($this->dir, $input);
0 ignored issues
show
Bug introduced by
The property dir 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...
4245
4246
			if (is_file($name) && is_readable($name)) {
4247
				return $name;
4248
			}
4249
		}
4250
4251
		return false;
4252
	}
4253
4254
	/**
4255
	 * Get path to cached .css file
4256
	 *
4257
	 * @return string
4258
	 */
4259
	protected function cacheName($fname) {
4260
		return $this->join($this->cacheDir, md5($fname) . '.css');
0 ignored issues
show
Bug introduced by
The property cacheDir 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...
4261
	}
4262
4263
	/**
4264
	 * Get path to cached imports
4265
	 *
4266
	 * @return string
4267
	 */
4268
	protected function importsCacheName($out) {
4269
		return $out . '.imports';
4270
	}
4271
4272
	/**
4273
	 * Determine whether .scss file needs to be re-compiled.
4274
	 *
4275
	 * @param string $in  Input path
4276
	 * @param string $out Output path
4277
	 *
4278
	 * @return boolean True if compile required.
4279
	 */
4280
	protected function needsCompile($in, $out) {
4281
		if (!is_file($out)) return true;
4282
4283
		$mtime = filemtime($out);
4284
		if (filemtime($in) > $mtime) return true;
4285
4286
		// look for modified imports
4287
		$icache = $this->importsCacheName($out);
4288
		if (is_readable($icache)) {
4289
			$imports = unserialize(file_get_contents($icache));
4290
			foreach ($imports as $import) {
4291
				if (filemtime($import) > $mtime) return true;
4292
			}
4293
		}
4294
		return false;
4295
	}
4296
4297
	/**
4298
	 * Compile .scss file
4299
	 *
4300
	 * @param string $in  Input path (.scss)
4301
	 * @param string $out Output path (.css)
4302
	 *
4303
	 * @return string
4304
	 */
4305
	protected function compile($in, $out) {
4306
		$start = microtime(true);
4307
		$css = $this->scss->compile(file_get_contents($in), $in);
0 ignored issues
show
Bug introduced by
The property scss 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...
4308
		$elapsed = round((microtime(true) - $start), 4);
4309
4310
		$v = scssc::$VERSION;
4311
		$t = date('r');
4312
		$css = "/* compiled by scssphp $v on $t (${elapsed}s) */\n\n" . $css;
4313
4314
		file_put_contents($out, $css);
4315
		file_put_contents($this->importsCacheName($out),
4316
			serialize($this->scss->getParsedFiles()));
4317
		return $css;
4318
	}
4319
4320
	/**
4321
	 * Compile requested scss and serve css.  Outputs HTTP response.
4322
	 *
4323
	 * @param string $salt Prefix a string to the filename for creating the cache name hash
4324
	 */
4325
	public function serve($salt = '') {
4326
		if ($input = $this->findInput()) {
4327
			$output = $this->cacheName($salt . $input);
4328
			header('Content-type: text/css');
4329
4330
			if ($this->needsCompile($input, $output)) {
4331
				try {
4332
					echo $this->compile($input, $output);
4333
				} catch (Exception $e) {
0 ignored issues
show
Unused Code introduced by
catch (\Exception $e) { ...->getMessage() . ' '; } does not seem to be reachable.

This check looks for unreachable code. It uses sophisticated control flow analysis techniques to find statements which will never be executed.

Unreachable code is most often the result of return, die or exit statements that have been added for debug purposes.

function fx() {
    try {
        doSomething();
        return true;
    }
    catch (\Exception $e) {
        return false;
    }

    return false;
}

In the above example, the last return false will never be executed, because a return statement has already been met in every possible execution path.

Loading history...
4334
					header('HTTP/1.1 500 Internal Server Error');
4335
					echo 'Parse error: ' . $e->getMessage() . "\n";
4336
				}
4337
			} else {
4338
				header('X-SCSS-Cache: true');
4339
				echo file_get_contents($output);
4340
			}
4341
4342
			return;
4343
		}
4344
4345
		header('HTTP/1.0 404 Not Found');
4346
		header('Content-type: text');
4347
		$v = scssc::$VERSION;
4348
		echo "/* INPUT NOT FOUND scss $v */\n";
4349
	}
4350
4351
	/**
4352
	 * Constructor
4353
	 *
4354
	 * @param string      $dir      Root directory to .scss files
4355
	 * @param string      $cacheDir Cache directory
0 ignored issues
show
Documentation introduced by
Should the type for parameter $cacheDir not be string|null?

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive.

Most often this is a case of a parameter that can be null in addition to its declared types.

Loading history...
4356
	 * @param \scssc|null $scss     SCSS compiler instance
4357
	 */
4358
	public function __construct($dir, $cacheDir=null, $scss=null) {
4359
		$this->dir = $dir;
4360
4361
		if (is_null($cacheDir)) {
4362
			$cacheDir = $this->join($dir, 'scss_cache');
4363
		}
4364
4365
		$this->cacheDir = $cacheDir;
4366
		if (!is_dir($this->cacheDir)) mkdir($this->cacheDir, 0755, true);
4367
4368
		if (is_null($scss)) {
4369
			$scss = new scssc();
4370
			$scss->setImportPaths($this->dir);
4371
		}
4372
		$this->scss = $scss;
4373
	}
4374
4375
	/**
4376
	 * Helper method to serve compiled scss
4377
	 *
4378
	 * @param string $path Root path
4379
	 */
4380
	static public function serveFrom($path) {
4381
		$server = new self($path);
4382
		$server->serve();
4383
	}
4384
}
4385